From 5d2b0fa72458971001b24e25c37827b7811d5a46 Mon Sep 17 00:00:00 2001 From: Johann Duscher Date: Thu, 31 Oct 2024 14:34:31 +0100 Subject: [PATCH 1/3] fix(SqliteRepository): Fix exception caused by invalid SQL string. --- .../CollectionFile/Database/CardRepository.cs | 13 ++--- .../CollectionFile/Database/ColRepository.cs | 11 +++-- .../Database/GraveRepository.cs | 4 +- .../CollectionFile/Database/NoteRepository.cs | 13 ++--- .../Database/RevLogRepository.cs | 9 ++-- .../Database/SqliteRepository.cs | 48 ++++++++++++++----- 6 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/AnkiNet/CollectionFile/Database/CardRepository.cs b/src/AnkiNet/CollectionFile/Database/CardRepository.cs index c9081b5..9f58587 100644 --- a/src/AnkiNet/CollectionFile/Database/CardRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/CardRepository.cs @@ -17,13 +17,14 @@ public CardRepository(SqliteConnection connection) : base(connection) "[factor], [reps], [lapses], [left], [odue]," + "[odid], [flags], [data]"; - protected override string GetValues(card i) + protected override IReadOnlyList GetValues(card i) { - return - $"{i.id},{i.nid},{i.did},{i.ord},{i.mod}," + - $"{i.usn},{i.type},{i.queue},{i.due},{i.ivl}," + - $"{i.factor},{i.reps},{i.lapses},{i.left},{i.odue}," + - $"{i.odid},{i.flags},'{i.data}'"; + return [ + i.id, i.nid, i.did, i.ord, i.mod, + i.usn, i.type, i.queue, i.due, i.ivl, + i.factor, i.reps, i.lapses, i.left, i.odue, + i.odid, i.flags, i.data + ]; } protected override card Map(SqliteDataReader reader) diff --git a/src/AnkiNet/CollectionFile/Database/ColRepository.cs b/src/AnkiNet/CollectionFile/Database/ColRepository.cs index 786663d..5a411c5 100644 --- a/src/AnkiNet/CollectionFile/Database/ColRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/ColRepository.cs @@ -16,12 +16,13 @@ public ColRepository(SqliteConnection connection) : base(connection) "[dty], [usn], [ls], [conf], [models], " + "[decks], [dconf], [tags]"; - protected override string GetValues(col i) + protected override IReadOnlyList GetValues(col i) { - return - $"{i.id},{i.crt},{i.mod},{i.scm},{i.ver}," + - $"{i.dty},{i.usn},{i.ls},'{i.conf}','{i.models}'," + - $"'{i.decks}','{i.dconf}','{i.tags}'"; + return [ + i.id, i.crt, i.mod, i.scm, i.ver, + i.dty, i.usn, i.ls, i.conf, i.models, + i.decks, i.dconf, i.tags + ]; } protected override col Map(SqliteDataReader reader) diff --git a/src/AnkiNet/CollectionFile/Database/GraveRepository.cs b/src/AnkiNet/CollectionFile/Database/GraveRepository.cs index c1cc985..409c234 100644 --- a/src/AnkiNet/CollectionFile/Database/GraveRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/GraveRepository.cs @@ -13,9 +13,9 @@ public GraveRepository(SqliteConnection connection) : base(connection) protected override string Columns => "[usn], [oid], [type]"; - protected override string GetValues(grave i) + protected override IReadOnlyList GetValues(grave i) { - return $"{i.usn},{i.oid},{i.type}"; + return [i.usn, i.oid, i.type]; } protected override grave Map(SqliteDataReader reader) diff --git a/src/AnkiNet/CollectionFile/Database/NoteRepository.cs b/src/AnkiNet/CollectionFile/Database/NoteRepository.cs index 7450fc0..de86b39 100644 --- a/src/AnkiNet/CollectionFile/Database/NoteRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/NoteRepository.cs @@ -17,13 +17,14 @@ public NoteRepository(SqliteConnection connection) : base(connection) "[flds], [sfld], [csum], " + "[flags], [data]"; - protected override string GetValues(note i) + protected override IReadOnlyList GetValues(note i) { - return - $"{i.id},'{i.guid}',{i.mid}," + - $"{i.mod},{i.usn},'{i.tags}'," + - $"'{i.flds}','{i.sfld}',{i.csum}," + - $"{i.flags},'{i.data}'"; + return [ + i.id, i.guid, i.mid, + i.mod, i.usn, i.tags, + i.flds, i.sfld, i.csum, + i.flags, i.data + ]; } protected override note Map(SqliteDataReader reader) diff --git a/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs b/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs index 34cf628..06934c1 100644 --- a/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs @@ -15,11 +15,12 @@ public RevLogRepository(SqliteConnection connection) : base(connection) "[id], [cid], [usn], [ease], [ivl], " + "[lastIvl], [factor], [time], [type]"; - protected override string GetValues(revLog i) + protected override IReadOnlyList GetValues(revLog i) { - return - $"{i.id},{i.cid},{i.usn},{i.ease},{i.ivl}," + - $"{i.lastIvl},{i.factor},{i.time},{i.type}"; + return [ + i.id, i.cid, i.usn, i.ease, i.ivl, + i.lastIvl, i.factor, i.time, i.type + ]; } protected override revLog Map(SqliteDataReader reader) diff --git a/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs b/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs index fff07b8..7d41654 100644 --- a/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs @@ -8,7 +8,9 @@ internal abstract class SqliteRepository protected abstract string TableName { get; } protected abstract string Columns { get; } - protected abstract string GetValues(T item); + + protected abstract IReadOnlyList GetValues(T item); + protected abstract T Map(SqliteDataReader reader); @@ -42,29 +44,53 @@ public async Task> ReadAll() return result; } - public async Task Add(List items) + public async Task Add(IReadOnlyList items) { - if (!items.Any()) + if (items.Count == 0) { return; } - var writeSqlQuery = $@" - INSERT INTO {TableName} - ({Columns}) - VALUES "; + string writeSqlQuery; + { + var values = items.Select((item, itemIndex) => + { + var itemValueCount = GetValues(item).Length; - var values = items.Select(i => $"({GetValues(i)})"); - writeSqlQuery += string.Join(',', values); + var @params = Enumerable.Range(0, itemValueCount) + .Select(paramIndex => ParamName(itemIndex, paramIndex)); + + return $"({string.Join(',', @params)})"; + }); + + writeSqlQuery = $"INSERT INTO {TableName} ({Columns}) VALUES {string.Join(',', values)}"; + } try { - using var command = new SqliteCommand(writeSqlQuery, _connection); - var i = await command.ExecuteNonQueryAsync(); + await using var command = new SqliteCommand(writeSqlQuery, _connection); + + foreach (var (item, itemIndex) in items.Select((item, itemIndex) => (item, itemIndex))) + { + var itemValues = GetValues(item); + foreach (var (itemValue, paramIndex) in itemValues.Select((itemValue, paramIndex) => (itemValue, paramIndex))) + { + var paramName = ParamName(itemIndex, paramIndex); + command.Parameters.AddWithValue(paramName, itemValue); + } + } + + var numberOfItemsInserted = await command.ExecuteNonQueryAsync(); } catch (Exception e) { throw new IOException($"Cannot Add {typeof(T).Name}", e); } + + #region Helper function(s) + + static string ParamName(int itemIndex, int paramIndex) => $"@p{itemIndex}_{paramIndex}"; + + #endregion } } \ No newline at end of file From 809432d9f85e6f4783c3cc7f124dd028e2fee3f7 Mon Sep 17 00:00:00 2001 From: Johann Duscher Date: Thu, 31 Oct 2024 15:06:54 +0100 Subject: [PATCH 2/3] refactor(SqliteRepository): Return `Columns` as list of strings. --- .../CollectionFile/Database/CardRepository.cs | 12 +++++++----- .../CollectionFile/Database/ColRepository.cs | 10 ++++++---- .../CollectionFile/Database/GraveRepository.cs | 3 ++- .../CollectionFile/Database/NoteRepository.cs | 12 +++++++----- .../CollectionFile/Database/RevLogRepository.cs | 8 +++++--- .../CollectionFile/Database/SqliteRepository.cs | 17 +++++++---------- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/AnkiNet/CollectionFile/Database/CardRepository.cs b/src/AnkiNet/CollectionFile/Database/CardRepository.cs index 9f58587..48872db 100644 --- a/src/AnkiNet/CollectionFile/Database/CardRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/CardRepository.cs @@ -11,11 +11,13 @@ public CardRepository(SqliteConnection connection) : base(connection) protected override string TableName => "[cards]"; - protected override string Columns => - "[id], [nid], [did], [ord], [mod], " + - "[usn], [type], [queue], [due], [ivl], " + - "[factor], [reps], [lapses], [left], [odue]," + - "[odid], [flags], [data]"; + protected override IReadOnlyList Columns { get; } = + [ + "[id]", "[nid]", "[did]", "[ord]", "[mod]", + "[usn]", "[type]", "[queue]", "[due]", "[ivl]", + "[factor]", "[reps]", "[lapses]", "[left]", "[odue]", + "[odid]", "[flags]", "[data]" + ]; protected override IReadOnlyList GetValues(card i) { diff --git a/src/AnkiNet/CollectionFile/Database/ColRepository.cs b/src/AnkiNet/CollectionFile/Database/ColRepository.cs index 5a411c5..3bd79c4 100644 --- a/src/AnkiNet/CollectionFile/Database/ColRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/ColRepository.cs @@ -11,10 +11,12 @@ public ColRepository(SqliteConnection connection) : base(connection) protected override string TableName => "[col]"; - protected override string Columns => - "[id], [crt], [mod], [scm], [ver], " + - "[dty], [usn], [ls], [conf], [models], " + - "[decks], [dconf], [tags]"; + protected override IReadOnlyList Columns { get; } = + [ + "[id]", "[crt]", "[mod]", "[scm]", "[ver]", + "[dty]", "[usn]", "[ls]", "[conf]", "[models]", + "[decks]", "[dconf]", "[tags]" + ]; protected override IReadOnlyList GetValues(col i) { diff --git a/src/AnkiNet/CollectionFile/Database/GraveRepository.cs b/src/AnkiNet/CollectionFile/Database/GraveRepository.cs index 409c234..eab3752 100644 --- a/src/AnkiNet/CollectionFile/Database/GraveRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/GraveRepository.cs @@ -11,7 +11,8 @@ public GraveRepository(SqliteConnection connection) : base(connection) protected override string TableName => "[graves]"; - protected override string Columns => "[usn], [oid], [type]"; + protected override IReadOnlyList Columns { get; } = + ["[usn]", "[oid]", "[type]"]; protected override IReadOnlyList GetValues(grave i) { diff --git a/src/AnkiNet/CollectionFile/Database/NoteRepository.cs b/src/AnkiNet/CollectionFile/Database/NoteRepository.cs index de86b39..4c04bce 100644 --- a/src/AnkiNet/CollectionFile/Database/NoteRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/NoteRepository.cs @@ -11,11 +11,13 @@ public NoteRepository(SqliteConnection connection) : base(connection) protected override string TableName => "[notes]"; - protected override string Columns => - "[id], [guid], [mid], " + - "[mod], [usn], [tags], " + - "[flds], [sfld], [csum], " + - "[flags], [data]"; + protected override IReadOnlyList Columns { get; } = + [ + "[id]", "[guid]", "[mid]", + "[mod]", "[usn]", "[tags]", + "[flds]", "[sfld]", "[csum]", + "[flags]", "[data]" + ]; protected override IReadOnlyList GetValues(note i) { diff --git a/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs b/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs index 06934c1..4c86a51 100644 --- a/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/RevLogRepository.cs @@ -11,9 +11,11 @@ public RevLogRepository(SqliteConnection connection) : base(connection) protected override string TableName => "[revlog]"; - protected override string Columns => - "[id], [cid], [usn], [ease], [ivl], " + - "[lastIvl], [factor], [time], [type]"; + protected override IReadOnlyList Columns { get; } = + [ + "[id]", "[cid]", "[usn]", "[ease]", "[ivl]", + "[lastIvl]", "[factor]", "[time]", "[type]" + ]; protected override IReadOnlyList GetValues(revLog i) { diff --git a/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs b/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs index 7d41654..eba415f 100644 --- a/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs +++ b/src/AnkiNet/CollectionFile/Database/SqliteRepository.cs @@ -7,7 +7,7 @@ internal abstract class SqliteRepository private readonly SqliteConnection _connection; protected abstract string TableName { get; } - protected abstract string Columns { get; } + protected abstract IReadOnlyList Columns { get; } protected abstract IReadOnlyList GetValues(T item); @@ -23,12 +23,12 @@ public async Task> ReadAll() { var result = new List(); - var readAllSqlQuery = $"SELECT {Columns} FROM {TableName}"; + var readAllSqlQuery = $"SELECT {string.Join(",", Columns)} FROM {TableName}"; try { - using var command = new SqliteCommand(readAllSqlQuery, _connection); - using var reader = await command.ExecuteReaderAsync(); + await using var command = new SqliteCommand(readAllSqlQuery, _connection); + await using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { @@ -53,17 +53,14 @@ public async Task Add(IReadOnlyList items) string writeSqlQuery; { - var values = items.Select((item, itemIndex) => + var values = Enumerable.Range(0, items.Count).Select(itemIndex => { - var itemValueCount = GetValues(item).Length; - - var @params = Enumerable.Range(0, itemValueCount) - .Select(paramIndex => ParamName(itemIndex, paramIndex)); + var @params = Enumerable.Range(0, Columns.Count).Select(paramIndex => ParamName(itemIndex, paramIndex)); return $"({string.Join(',', @params)})"; }); - writeSqlQuery = $"INSERT INTO {TableName} ({Columns}) VALUES {string.Join(',', values)}"; + writeSqlQuery = $"INSERT INTO {TableName} ({string.Join(",", Columns)}) VALUES {string.Join(',', values)}"; } try From ccd5d78ea4314755da3e2aee346e676fdb0e967f Mon Sep 17 00:00:00 2001 From: Johann Duscher Date: Thu, 31 Oct 2024 15:37:09 +0100 Subject: [PATCH 3/3] fix(CollectionMapper): Enable usage of Anki.NET with `net8.0` projects. --- .../CollectionFile/Mapper/CollectionMapper.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/AnkiNet/CollectionFile/Mapper/CollectionMapper.cs b/src/AnkiNet/CollectionFile/Mapper/CollectionMapper.cs index 5862d02..f66f563 100644 --- a/src/AnkiNet/CollectionFile/Mapper/CollectionMapper.cs +++ b/src/AnkiNet/CollectionFile/Mapper/CollectionMapper.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using AnkiNet.CollectionFile.Database.Model; using AnkiNet.CollectionFile.Model; using AnkiNet.CollectionFile.Model.Json; @@ -7,12 +8,17 @@ namespace AnkiNet.CollectionFile.Mapper; internal static class CollectionMapper { + private static readonly JsonSerializerOptions SerializerOptions = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + }; + public static Collection FromDb(col col) { - var configuration = JsonSerializer.Deserialize(col.conf); - var models = JsonSerializer.Deserialize>(col.models); - var decks = JsonSerializer.Deserialize>(col.decks); - var decksConfiguration = JsonSerializer.Deserialize>(col.dconf); + var configuration = JsonSerializer.Deserialize(col.conf, SerializerOptions); + var models = JsonSerializer.Deserialize>(col.models, SerializerOptions); + var decks = JsonSerializer.Deserialize>(col.decks, SerializerOptions); + var decksConfiguration = JsonSerializer.Deserialize>(col.dconf, SerializerOptions); return new Collection( col.id, @@ -33,10 +39,10 @@ public static Collection FromDb(col col) public static col ToDb(Collection collection) { - var conf = JsonSerializer.Serialize(collection.Configuration); - var models = JsonSerializer.Serialize(collection.Models); - var decks = JsonSerializer.Serialize(collection.Decks); - var dconf = JsonSerializer.Serialize(collection.DecksConfiguration); + var conf = JsonSerializer.Serialize(collection.Configuration, SerializerOptions); + var models = JsonSerializer.Serialize(collection.Models, SerializerOptions); + var decks = JsonSerializer.Serialize(collection.Decks, SerializerOptions); + var dconf = JsonSerializer.Serialize(collection.DecksConfiguration, SerializerOptions); return new col( collection.Id,