using Microsoft.Data.Sqlite; using Scry.Core.Models; namespace Scry.Core.Data; /// /// SQLite database for storing card data and perceptual hashes. /// Schema mirrors Scryfall's data model: oracles (game cards), sets, and cards (printings). /// public class CardDatabase : IDisposable { private readonly SqliteConnection _connection; private readonly string _dbPath; public CardDatabase(string dbPath) { _dbPath = dbPath; _connection = new SqliteConnection($"Data Source={dbPath}"); _connection.Open(); Initialize(); } private void Initialize() { using var cmd = _connection.CreateCommand(); cmd.CommandText = """ -- Abstract game cards (oracle) CREATE TABLE IF NOT EXISTS oracles ( id TEXT PRIMARY KEY, name TEXT NOT NULL, mana_cost TEXT, cmc REAL, type_line TEXT, oracle_text TEXT, colors TEXT, color_identity TEXT, keywords TEXT, reserved INTEGER DEFAULT 0, legalities TEXT, power TEXT, toughness TEXT ); CREATE INDEX IF NOT EXISTS idx_oracles_name ON oracles(name); -- MTG sets CREATE TABLE IF NOT EXISTS sets ( id TEXT PRIMARY KEY, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, set_type TEXT, released_at TEXT, card_count INTEGER, icon_svg_uri TEXT, digital INTEGER DEFAULT 0, parent_set_code TEXT, block TEXT ); CREATE INDEX IF NOT EXISTS idx_sets_code ON sets(code); -- Card printings with hashes CREATE TABLE IF NOT EXISTS cards ( id TEXT PRIMARY KEY, oracle_id TEXT NOT NULL, set_id TEXT NOT NULL, set_code TEXT, name TEXT NOT NULL, collector_number TEXT, rarity TEXT, artist TEXT, illustration_id TEXT, image_uri TEXT, hash BLOB, lang TEXT DEFAULT 'en', prices_usd REAL, prices_usd_foil REAL, FOREIGN KEY (oracle_id) REFERENCES oracles(id), FOREIGN KEY (set_id) REFERENCES sets(id) ); CREATE INDEX IF NOT EXISTS idx_cards_oracle_id ON cards(oracle_id); CREATE INDEX IF NOT EXISTS idx_cards_set_id ON cards(set_id); CREATE INDEX IF NOT EXISTS idx_cards_name ON cards(name); CREATE INDEX IF NOT EXISTS idx_cards_set_code ON cards(set_code); -- Metadata for tracking sync state CREATE TABLE IF NOT EXISTS metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); """; cmd.ExecuteNonQuery(); } #region Metadata public async Task GetMetadataAsync(string key, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT value FROM metadata WHERE key = $key"; cmd.Parameters.AddWithValue("$key", key); var result = await cmd.ExecuteScalarAsync(ct); return result as string; } public async Task SetMetadataAsync(string key, string value, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO metadata (key, value) VALUES ($key, $value) """; cmd.Parameters.AddWithValue("$key", key); cmd.Parameters.AddWithValue("$value", value); await cmd.ExecuteNonQueryAsync(ct); } #endregion #region Oracles public async Task InsertOracleAsync(Oracle oracle, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO oracles (id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness) VALUES ($id, $name, $mana_cost, $cmc, $type_line, $oracle_text, $colors, $color_identity, $keywords, $reserved, $legalities, $power, $toughness) """; cmd.Parameters.AddWithValue("$id", oracle.Id); cmd.Parameters.AddWithValue("$name", oracle.Name); cmd.Parameters.AddWithValue("$mana_cost", oracle.ManaCost ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$cmc", oracle.Cmc ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$type_line", oracle.TypeLine ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$oracle_text", oracle.OracleText ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$colors", oracle.Colors ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$color_identity", oracle.ColorIdentity ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$keywords", oracle.Keywords ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$reserved", oracle.Reserved ? 1 : 0); cmd.Parameters.AddWithValue("$legalities", oracle.Legalities ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$power", oracle.Power ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$toughness", oracle.Toughness ?? (object)DBNull.Value); await cmd.ExecuteNonQueryAsync(ct); } public async Task InsertOracleBatchAsync(IEnumerable oracles, CancellationToken ct = default) { await using var transaction = await _connection.BeginTransactionAsync(ct); try { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO oracles (id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness) VALUES ($id, $name, $mana_cost, $cmc, $type_line, $oracle_text, $colors, $color_identity, $keywords, $reserved, $legalities, $power, $toughness) """; var idParam = cmd.Parameters.Add("$id", SqliteType.Text); var nameParam = cmd.Parameters.Add("$name", SqliteType.Text); var manaCostParam = cmd.Parameters.Add("$mana_cost", SqliteType.Text); var cmcParam = cmd.Parameters.Add("$cmc", SqliteType.Real); var typeLineParam = cmd.Parameters.Add("$type_line", SqliteType.Text); var oracleTextParam = cmd.Parameters.Add("$oracle_text", SqliteType.Text); var colorsParam = cmd.Parameters.Add("$colors", SqliteType.Text); var colorIdentityParam = cmd.Parameters.Add("$color_identity", SqliteType.Text); var keywordsParam = cmd.Parameters.Add("$keywords", SqliteType.Text); var reservedParam = cmd.Parameters.Add("$reserved", SqliteType.Integer); var legalitiesParam = cmd.Parameters.Add("$legalities", SqliteType.Text); var powerParam = cmd.Parameters.Add("$power", SqliteType.Text); var toughnessParam = cmd.Parameters.Add("$toughness", SqliteType.Text); foreach (var oracle in oracles) { ct.ThrowIfCancellationRequested(); idParam.Value = oracle.Id; nameParam.Value = oracle.Name; manaCostParam.Value = oracle.ManaCost ?? (object)DBNull.Value; cmcParam.Value = oracle.Cmc ?? (object)DBNull.Value; typeLineParam.Value = oracle.TypeLine ?? (object)DBNull.Value; oracleTextParam.Value = oracle.OracleText ?? (object)DBNull.Value; colorsParam.Value = oracle.Colors ?? (object)DBNull.Value; colorIdentityParam.Value = oracle.ColorIdentity ?? (object)DBNull.Value; keywordsParam.Value = oracle.Keywords ?? (object)DBNull.Value; reservedParam.Value = oracle.Reserved ? 1 : 0; legalitiesParam.Value = oracle.Legalities ?? (object)DBNull.Value; powerParam.Value = oracle.Power ?? (object)DBNull.Value; toughnessParam.Value = oracle.Toughness ?? (object)DBNull.Value; await cmd.ExecuteNonQueryAsync(ct); } await transaction.CommitAsync(ct); } catch { await transaction.RollbackAsync(ct); throw; } } public async Task GetOracleByIdAsync(string id, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness FROM oracles WHERE id = $id """; cmd.Parameters.AddWithValue("$id", id); await using var reader = await cmd.ExecuteReaderAsync(ct); if (await reader.ReadAsync(ct)) { return ReadOracle(reader); } return null; } public async Task GetOracleByNameAsync(string name, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness FROM oracles WHERE name = $name COLLATE NOCASE """; cmd.Parameters.AddWithValue("$name", name); await using var reader = await cmd.ExecuteReaderAsync(ct); if (await reader.ReadAsync(ct)) { return ReadOracle(reader); } return null; } public async Task> GetExistingOracleIdsAsync(CancellationToken ct = default) { var ids = new HashSet(); await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT id FROM oracles"; await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { ids.Add(reader.GetString(0)); } return ids; } public async Task GetOracleCountAsync(CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM oracles"; var result = await cmd.ExecuteScalarAsync(ct); return Convert.ToInt32(result); } private static Oracle ReadOracle(SqliteDataReader reader) => new() { Id = reader.GetString(0), Name = reader.GetString(1), ManaCost = reader.IsDBNull(2) ? null : reader.GetString(2), Cmc = reader.IsDBNull(3) ? null : reader.GetDouble(3), TypeLine = reader.IsDBNull(4) ? null : reader.GetString(4), OracleText = reader.IsDBNull(5) ? null : reader.GetString(5), Colors = reader.IsDBNull(6) ? null : reader.GetString(6), ColorIdentity = reader.IsDBNull(7) ? null : reader.GetString(7), Keywords = reader.IsDBNull(8) ? null : reader.GetString(8), Reserved = reader.GetInt32(9) != 0, Legalities = reader.IsDBNull(10) ? null : reader.GetString(10), Power = reader.IsDBNull(11) ? null : reader.GetString(11), Toughness = reader.IsDBNull(12) ? null : reader.GetString(12), }; #endregion #region Sets public async Task InsertSetAsync(Set set, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO sets (id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block) VALUES ($id, $code, $name, $set_type, $released_at, $card_count, $icon_svg_uri, $digital, $parent_set_code, $block) """; cmd.Parameters.AddWithValue("$id", set.Id); cmd.Parameters.AddWithValue("$code", set.Code); cmd.Parameters.AddWithValue("$name", set.Name); cmd.Parameters.AddWithValue("$set_type", set.SetType ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$released_at", set.ReleasedAt ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$card_count", set.CardCount ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$icon_svg_uri", set.IconSvgUri ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$digital", set.Digital ? 1 : 0); cmd.Parameters.AddWithValue("$parent_set_code", set.ParentSetCode ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$block", set.Block ?? (object)DBNull.Value); await cmd.ExecuteNonQueryAsync(ct); } public async Task InsertSetBatchAsync(IEnumerable sets, CancellationToken ct = default) { await using var transaction = await _connection.BeginTransactionAsync(ct); try { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO sets (id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block) VALUES ($id, $code, $name, $set_type, $released_at, $card_count, $icon_svg_uri, $digital, $parent_set_code, $block) """; var idParam = cmd.Parameters.Add("$id", SqliteType.Text); var codeParam = cmd.Parameters.Add("$code", SqliteType.Text); var nameParam = cmd.Parameters.Add("$name", SqliteType.Text); var setTypeParam = cmd.Parameters.Add("$set_type", SqliteType.Text); var releasedAtParam = cmd.Parameters.Add("$released_at", SqliteType.Text); var cardCountParam = cmd.Parameters.Add("$card_count", SqliteType.Integer); var iconSvgUriParam = cmd.Parameters.Add("$icon_svg_uri", SqliteType.Text); var digitalParam = cmd.Parameters.Add("$digital", SqliteType.Integer); var parentSetCodeParam = cmd.Parameters.Add("$parent_set_code", SqliteType.Text); var blockParam = cmd.Parameters.Add("$block", SqliteType.Text); foreach (var set in sets) { ct.ThrowIfCancellationRequested(); idParam.Value = set.Id; codeParam.Value = set.Code; nameParam.Value = set.Name; setTypeParam.Value = set.SetType ?? (object)DBNull.Value; releasedAtParam.Value = set.ReleasedAt ?? (object)DBNull.Value; cardCountParam.Value = set.CardCount ?? (object)DBNull.Value; iconSvgUriParam.Value = set.IconSvgUri ?? (object)DBNull.Value; digitalParam.Value = set.Digital ? 1 : 0; parentSetCodeParam.Value = set.ParentSetCode ?? (object)DBNull.Value; blockParam.Value = set.Block ?? (object)DBNull.Value; await cmd.ExecuteNonQueryAsync(ct); } await transaction.CommitAsync(ct); } catch { await transaction.RollbackAsync(ct); throw; } } public async Task GetSetByIdAsync(string id, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block FROM sets WHERE id = $id """; cmd.Parameters.AddWithValue("$id", id); await using var reader = await cmd.ExecuteReaderAsync(ct); if (await reader.ReadAsync(ct)) { return ReadSet(reader); } return null; } public async Task GetSetByCodeAsync(string code, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block FROM sets WHERE code = $code COLLATE NOCASE """; cmd.Parameters.AddWithValue("$code", code); await using var reader = await cmd.ExecuteReaderAsync(ct); if (await reader.ReadAsync(ct)) { return ReadSet(reader); } return null; } public async Task> GetExistingSetIdsAsync(CancellationToken ct = default) { var ids = new HashSet(); await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT id FROM sets"; await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { ids.Add(reader.GetString(0)); } return ids; } public async Task GetSetCountAsync(CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM sets"; var result = await cmd.ExecuteScalarAsync(ct); return Convert.ToInt32(result); } private static Set ReadSet(SqliteDataReader reader) => new() { Id = reader.GetString(0), Code = reader.GetString(1), Name = reader.GetString(2), SetType = reader.IsDBNull(3) ? null : reader.GetString(3), ReleasedAt = reader.IsDBNull(4) ? null : reader.GetString(4), CardCount = reader.IsDBNull(5) ? null : reader.GetInt32(5), IconSvgUri = reader.IsDBNull(6) ? null : reader.GetString(6), Digital = reader.GetInt32(7) != 0, ParentSetCode = reader.IsDBNull(8) ? null : reader.GetString(8), Block = reader.IsDBNull(9) ? null : reader.GetString(9), }; #endregion #region Cards (Printings) public async Task InsertCardAsync(Card card, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO cards (id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil) VALUES ($id, $oracle_id, $set_id, $set_code, $name, $collector_number, $rarity, $artist, $illustration_id, $image_uri, $hash, $lang, $prices_usd, $prices_usd_foil) """; cmd.Parameters.AddWithValue("$id", card.Id); cmd.Parameters.AddWithValue("$oracle_id", card.OracleId); cmd.Parameters.AddWithValue("$set_id", card.SetId); cmd.Parameters.AddWithValue("$set_code", card.SetCode ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$name", card.Name); cmd.Parameters.AddWithValue("$collector_number", card.CollectorNumber ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$rarity", card.Rarity ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$artist", card.Artist ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$illustration_id", card.IllustrationId ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$image_uri", card.ImageUri ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$hash", card.Hash ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$lang", card.Lang ?? "en"); cmd.Parameters.AddWithValue("$prices_usd", card.PricesUsd ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("$prices_usd_foil", card.PricesUsdFoil ?? (object)DBNull.Value); await cmd.ExecuteNonQueryAsync(ct); } public async Task InsertCardBatchAsync(IEnumerable cards, CancellationToken ct = default) { await using var transaction = await _connection.BeginTransactionAsync(ct); try { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT OR REPLACE INTO cards (id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil) VALUES ($id, $oracle_id, $set_id, $set_code, $name, $collector_number, $rarity, $artist, $illustration_id, $image_uri, $hash, $lang, $prices_usd, $prices_usd_foil) """; var idParam = cmd.Parameters.Add("$id", SqliteType.Text); var oracleIdParam = cmd.Parameters.Add("$oracle_id", SqliteType.Text); var setIdParam = cmd.Parameters.Add("$set_id", SqliteType.Text); var setCodeParam = cmd.Parameters.Add("$set_code", SqliteType.Text); var nameParam = cmd.Parameters.Add("$name", SqliteType.Text); var collectorNumberParam = cmd.Parameters.Add("$collector_number", SqliteType.Text); var rarityParam = cmd.Parameters.Add("$rarity", SqliteType.Text); var artistParam = cmd.Parameters.Add("$artist", SqliteType.Text); var illustrationIdParam = cmd.Parameters.Add("$illustration_id", SqliteType.Text); var imageUriParam = cmd.Parameters.Add("$image_uri", SqliteType.Text); var hashParam = cmd.Parameters.Add("$hash", SqliteType.Blob); var langParam = cmd.Parameters.Add("$lang", SqliteType.Text); var pricesUsdParam = cmd.Parameters.Add("$prices_usd", SqliteType.Real); var pricesUsdFoilParam = cmd.Parameters.Add("$prices_usd_foil", SqliteType.Real); foreach (var card in cards) { ct.ThrowIfCancellationRequested(); idParam.Value = card.Id; oracleIdParam.Value = card.OracleId; setIdParam.Value = card.SetId; setCodeParam.Value = card.SetCode ?? (object)DBNull.Value; nameParam.Value = card.Name; collectorNumberParam.Value = card.CollectorNumber ?? (object)DBNull.Value; rarityParam.Value = card.Rarity ?? (object)DBNull.Value; artistParam.Value = card.Artist ?? (object)DBNull.Value; illustrationIdParam.Value = card.IllustrationId ?? (object)DBNull.Value; imageUriParam.Value = card.ImageUri ?? (object)DBNull.Value; hashParam.Value = card.Hash ?? (object)DBNull.Value; langParam.Value = card.Lang ?? "en"; pricesUsdParam.Value = card.PricesUsd ?? (object)DBNull.Value; pricesUsdFoilParam.Value = card.PricesUsdFoil ?? (object)DBNull.Value; await cmd.ExecuteNonQueryAsync(ct); } await transaction.CommitAsync(ct); } catch { await transaction.RollbackAsync(ct); throw; } } public async Task GetCardByIdAsync(string id, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil FROM cards WHERE id = $id """; cmd.Parameters.AddWithValue("$id", id); await using var reader = await cmd.ExecuteReaderAsync(ct); if (await reader.ReadAsync(ct)) { return ReadCard(reader); } return null; } public async Task> GetCardsByOracleIdAsync(string oracleId, CancellationToken ct = default) { var cards = new List(); await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil FROM cards WHERE oracle_id = $oracle_id """; cmd.Parameters.AddWithValue("$oracle_id", oracleId); await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { cards.Add(ReadCard(reader)); } return cards; } public async Task> GetCardsByNameAsync(string name, CancellationToken ct = default) { var cards = new List(); await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil FROM cards WHERE name = $name COLLATE NOCASE """; cmd.Parameters.AddWithValue("$name", name); await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { cards.Add(ReadCard(reader)); } return cards; } public async Task> GetAllCardsAsync(CancellationToken ct = default) { var cards = new List(); await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil FROM cards """; await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { cards.Add(ReadCard(reader)); } return cards; } public async Task> GetCardsWithHashAsync(CancellationToken ct = default) { var cards = new List(); await using var cmd = _connection.CreateCommand(); // Join with oracles and sets to get denormalized data cmd.CommandText = """ SELECT c.id, c.oracle_id, c.set_id, c.set_code, c.name, c.collector_number, c.rarity, c.artist, c.illustration_id, c.image_uri, c.hash, c.lang, c.prices_usd, c.prices_usd_foil, o.mana_cost, o.type_line, o.oracle_text, o.power, o.toughness, s.name as set_name FROM cards c LEFT JOIN oracles o ON c.oracle_id = o.id LEFT JOIN sets s ON c.set_id = s.id WHERE c.hash IS NOT NULL """; await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { cards.Add(ReadCardWithOracle(reader)); } return cards; } public async Task> GetExistingCardIdsAsync(CancellationToken ct = default) { var ids = new HashSet(); await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT id FROM cards"; await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { ids.Add(reader.GetString(0)); } return ids; } public async Task> GetExistingCardNamesAsync(CancellationToken ct = default) { var names = new HashSet(StringComparer.OrdinalIgnoreCase); await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT DISTINCT name FROM cards"; await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { names.Add(reader.GetString(0)); } return names; } public async Task GetCardCountAsync(CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM cards"; var result = await cmd.ExecuteScalarAsync(ct); return Convert.ToInt32(result); } public async Task DeleteCardByIdAsync(string id, CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = "DELETE FROM cards WHERE id = $id"; cmd.Parameters.AddWithValue("$id", id); await cmd.ExecuteNonQueryAsync(ct); } public async Task ClearCardsAsync(CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = "DELETE FROM cards"; await cmd.ExecuteNonQueryAsync(ct); } public async Task ClearAllAsync(CancellationToken ct = default) { await using var cmd = _connection.CreateCommand(); cmd.CommandText = """ DELETE FROM cards; DELETE FROM oracles; DELETE FROM sets; DELETE FROM metadata; """; await cmd.ExecuteNonQueryAsync(ct); } private static Card ReadCard(SqliteDataReader reader) => new() { Id = reader.GetString(0), OracleId = reader.GetString(1), SetId = reader.GetString(2), SetCode = reader.IsDBNull(3) ? null : reader.GetString(3), Name = reader.GetString(4), CollectorNumber = reader.IsDBNull(5) ? null : reader.GetString(5), Rarity = reader.IsDBNull(6) ? null : reader.GetString(6), Artist = reader.IsDBNull(7) ? null : reader.GetString(7), IllustrationId = reader.IsDBNull(8) ? null : reader.GetString(8), ImageUri = reader.IsDBNull(9) ? null : reader.GetString(9), Hash = reader.IsDBNull(10) ? null : (byte[])reader.GetValue(10), Lang = reader.IsDBNull(11) ? null : reader.GetString(11), PricesUsd = reader.IsDBNull(12) ? null : (decimal)reader.GetDouble(12), PricesUsdFoil = reader.IsDBNull(13) ? null : (decimal)reader.GetDouble(13), }; /// /// Reads a card with joined Oracle and Set data (columns 14-19). /// private static Card ReadCardWithOracle(SqliteDataReader reader) => new() { Id = reader.GetString(0), OracleId = reader.GetString(1), SetId = reader.GetString(2), SetCode = reader.IsDBNull(3) ? null : reader.GetString(3), Name = reader.GetString(4), CollectorNumber = reader.IsDBNull(5) ? null : reader.GetString(5), Rarity = reader.IsDBNull(6) ? null : reader.GetString(6), Artist = reader.IsDBNull(7) ? null : reader.GetString(7), IllustrationId = reader.IsDBNull(8) ? null : reader.GetString(8), ImageUri = reader.IsDBNull(9) ? null : reader.GetString(9), Hash = reader.IsDBNull(10) ? null : (byte[])reader.GetValue(10), Lang = reader.IsDBNull(11) ? null : reader.GetString(11), PricesUsd = reader.IsDBNull(12) ? null : (decimal)reader.GetDouble(12), PricesUsdFoil = reader.IsDBNull(13) ? null : (decimal)reader.GetDouble(13), // Denormalized Oracle fields (from JOIN) ManaCost = reader.IsDBNull(14) ? null : reader.GetString(14), TypeLine = reader.IsDBNull(15) ? null : reader.GetString(15), OracleText = reader.IsDBNull(16) ? null : reader.GetString(16), Power = reader.IsDBNull(17) ? null : reader.GetString(17), Toughness = reader.IsDBNull(18) ? null : reader.GetString(18), SetName = reader.IsDBNull(19) ? null : reader.GetString(19), }; #endregion public void Dispose() { _connection.Dispose(); } }