/** * Local SQLite database for offline card hash caching. * Syncs with Convex and provides offline recognition support. */ import * as SQLite from "expo-sqlite"; import type { CardHashEntry } from "../recognition"; const DB_NAME = "scry_cache.db"; let db: SQLite.SQLiteDatabase | null = null; /** * Initialize the local database. */ export async function initDatabase(): Promise { if (db) return; db = await SQLite.openDatabaseAsync(DB_NAME); // Create tables await db.execAsync(` CREATE TABLE IF NOT EXISTS cards ( scryfall_id TEXT PRIMARY KEY, oracle_id TEXT NOT NULL, name TEXT NOT NULL, set_code TEXT NOT NULL, collector_number TEXT, rarity TEXT, artist TEXT, image_uri TEXT, hash BLOB NOT NULL, hash_version INTEGER DEFAULT 1, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS sync_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_cards_name ON cards(name); CREATE INDEX IF NOT EXISTS idx_cards_updated ON cards(updated_at); `); console.log("[LocalDB] Database initialized"); } /** * Get the last sync timestamp. */ export async function getLastSyncTimestamp(): Promise { if (!db) await initDatabase(); const result = await db!.getFirstAsync<{ value: string }>( "SELECT value FROM sync_metadata WHERE key = ?", ["last_sync"] ); return result ? parseInt(result.value, 10) : 0; } /** * Set the last sync timestamp. */ export async function setLastSyncTimestamp(timestamp: number): Promise { if (!db) await initDatabase(); await db!.runAsync( "INSERT OR REPLACE INTO sync_metadata (key, value) VALUES (?, ?)", ["last_sync", timestamp.toString()] ); } /** * Get all cached card hashes for recognition. */ export async function getCachedHashes(): Promise { if (!db) await initDatabase(); const rows = await db!.getAllAsync<{ scryfall_id: string; name: string; set_code: string; collector_number: string | null; image_uri: string | null; hash: Uint8Array; }>("SELECT scryfall_id, name, set_code, collector_number, image_uri, hash FROM cards"); return rows.map((row) => ({ id: row.scryfall_id, name: row.name, setCode: row.set_code, collectorNumber: row.collector_number || undefined, imageUri: row.image_uri || undefined, hash: new Uint8Array(row.hash), })); } /** * Get the count of cached cards. */ export async function getCachedCardCount(): Promise { if (!db) await initDatabase(); const result = await db!.getFirstAsync<{ count: number }>( "SELECT COUNT(*) as count FROM cards" ); return result?.count || 0; } /** * Insert or update cards from Convex. */ export async function upsertCards(cards: Array<{ scryfallId: string; oracleId: string; name: string; setCode: string; collectorNumber: string; rarity: string; artist?: string; imageUri?: string; hash: Uint8Array; hashVersion: number; updatedAt: number; }>): Promise { if (!db) await initDatabase(); // Use a transaction for batch insert await db!.withTransactionAsync(async () => { for (const card of cards) { await db!.runAsync( `INSERT OR REPLACE INTO cards (scryfall_id, oracle_id, name, set_code, collector_number, rarity, artist, image_uri, hash, hash_version, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ card.scryfallId, card.oracleId, card.name, card.setCode, card.collectorNumber, card.rarity, card.artist || null, card.imageUri || null, card.hash, card.hashVersion, card.updatedAt, ] ); } }); console.log(`[LocalDB] Upserted ${cards.length} cards`); } /** * Clear all cached data. */ export async function clearCache(): Promise { if (!db) await initDatabase(); await db!.execAsync(` DELETE FROM cards; DELETE FROM sync_metadata; `); console.log("[LocalDB] Cache cleared"); } /** * Close the database connection. */ export async function closeDatabase(): Promise { if (db) { await db.closeAsync(); db = null; } }