diff --git a/package.json b/package.json index 8fd796f..5449deb 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "typescript": "^5" }, "dependencies": { + "@hasna/cloud": "^0.1.41", "@hasna/events": "^0.1.6", "@modelcontextprotocol/sdk": "^1.26.0", "chalk": "^5.3.0", diff --git a/src/db/index.ts b/src/db/index.ts index 895c279..c375c5a 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,11 +1,11 @@ /** * SQLite DB module for hooks — persistent storage at ~/.hasna/hooks/hooks.db * - * Uses bun:sqlite with WAL mode for concurrent reads. + * Uses the @hasna/cloud SQLite adapter with WAL mode for concurrent reads. * Supports HASNA_HOOKS_DATA_DIR / HOOKS_DATA_DIR and HASNA_HOOKS_DB_PATH / HOOKS_DB_PATH env overrides. */ -import { Database } from "bun:sqlite"; +import { SqliteAdapter } from "@hasna/cloud"; import { existsSync, mkdirSync, cpSync } from "fs"; import { join } from "path"; import { homedir } from "os"; @@ -13,7 +13,7 @@ import { runMigrations } from "./migrations"; import { runLegacyImport } from "./legacy-import"; import { runRetention } from "./retention"; -let instance: Database | null = null; +let instance: SqliteAdapter | null = null; function resolveDataDir(): string { const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR; @@ -46,14 +46,14 @@ function ensureDir(dbPath: string): void { } } -export function getDb(): Database { +export function getDb(): SqliteAdapter { if (instance) return instance; const dbPath = getDbPath(); const isNew = dbPath === ":memory:" || !existsSync(dbPath); ensureDir(dbPath); - instance = new Database(dbPath); + instance = new SqliteAdapter(dbPath); instance.exec("PRAGMA journal_mode=WAL"); instance.exec("PRAGMA foreign_keys=ON"); runMigrations(instance); @@ -82,8 +82,8 @@ export function closeDb(): void { } } -export function createTestDb(): Database { - const db = new Database(":memory:"); +export function createTestDb(): SqliteAdapter { + const db = new SqliteAdapter(":memory:"); db.exec("PRAGMA journal_mode=WAL"); db.exec("PRAGMA foreign_keys=ON"); return db; diff --git a/src/db/legacy-import.ts b/src/db/legacy-import.ts index 7d2e394..d906bac 100644 --- a/src/db/legacy-import.ts +++ b/src/db/legacy-import.ts @@ -8,14 +8,14 @@ * Tracks completion via a `_meta` table row keyed "legacy_import_done". */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; import { existsSync, readdirSync, readFileSync } from "fs"; import { join } from "path"; import { homedir } from "os"; const META_KEY = "legacy_import_done"; -function ensureMetaTable(db: Database): void { +function ensureMetaTable(db: DbAdapter): void { db.exec(` CREATE TABLE IF NOT EXISTS _meta ( key TEXT PRIMARY KEY, @@ -24,21 +24,21 @@ function ensureMetaTable(db: Database): void { `); } -function isAlreadyDone(db: Database): boolean { +function isAlreadyDone(db: DbAdapter): boolean { ensureMetaTable(db); - const row = db.query<{ value: string }, [string]>("SELECT value FROM _meta WHERE key = ?").get(META_KEY); + const row = db.get("SELECT value FROM _meta WHERE key = ?", META_KEY) as { value: string } | undefined; return row?.value === "1"; } -function markDone(db: Database): void { - db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]); +function markDone(db: DbAdapter): void { + db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", META_KEY, "1"); } function nanoid(): string { return crypto.randomUUID().replace(/-/g, "").slice(0, 21); } -function importJsonlFile(db: Database, filePath: string): number { +function importJsonlFile(db: DbAdapter, filePath: string): number { let count = 0; try { const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean); @@ -49,16 +49,14 @@ function importJsonlFile(db: Database, filePath: string): number { `INSERT OR IGNORE INTO hook_events (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - nanoid(), - entry.timestamp ?? new Date().toISOString(), - entry.session_id ?? "legacy", - "sessionlog", - "PostToolUse", - entry.tool_name ?? null, - entry.tool_input ? String(entry.tool_input).slice(0, 500) : null, - null, - ] + nanoid(), + entry.timestamp ?? new Date().toISOString(), + entry.session_id ?? "legacy", + "sessionlog", + "PostToolUse", + entry.tool_name ?? null, + entry.tool_input ? String(entry.tool_input).slice(0, 500) : null, + null, ); count++; } catch { @@ -71,7 +69,7 @@ function importJsonlFile(db: Database, filePath: string): number { return count; } -function importErrorsLog(db: Database, filePath: string): number { +function importErrorsLog(db: DbAdapter, filePath: string): number { let count = 0; try { const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean); @@ -87,14 +85,12 @@ function importErrorsLog(db: Database, filePath: string): number { `INSERT OR IGNORE INTO hook_events (id, timestamp, session_id, hook_name, event_type, error) VALUES (?, ?, ?, ?, ?, ?)`, - [ - nanoid(), - timestamp, - sessionPrefix ? `legacy-${sessionPrefix}` : "legacy", - "errornotify", - "PostToolUse", - errorMsg.slice(0, 500), - ] + nanoid(), + timestamp, + sessionPrefix ? `legacy-${sessionPrefix}` : "legacy", + "errornotify", + "PostToolUse", + errorMsg.slice(0, 500), ); count++; } catch { @@ -107,7 +103,7 @@ function importErrorsLog(db: Database, filePath: string): number { return count; } -export function runLegacyImport(db: Database): void { +export function runLegacyImport(db: DbAdapter): void { try { if (isAlreadyDone(db)) return; diff --git a/src/db/migrations/001_initial.ts b/src/db/migrations/001_initial.ts index c6d3178..63f42b0 100644 --- a/src/db/migrations/001_initial.ts +++ b/src/db/migrations/001_initial.ts @@ -3,10 +3,10 @@ * Creates hook_events table and indexes. */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; import { CREATE_HOOK_EVENTS_TABLE, CREATE_INDEXES } from "../schema"; -export function up(db: Database): void { +export function up(db: DbAdapter): void { db.exec(CREATE_HOOK_EVENTS_TABLE); for (const idx of CREATE_INDEXES) { db.exec(idx); diff --git a/src/db/migrations/002_session_events.ts b/src/db/migrations/002_session_events.ts index 1178b33..70ab523 100644 --- a/src/db/migrations/002_session_events.ts +++ b/src/db/migrations/002_session_events.ts @@ -7,15 +7,14 @@ * (fresh databases created from the updated schema.ts). */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; import { CREATE_HOOK_EVENTS_TABLE, CREATE_INDEXES } from "../schema"; -export function up(db: Database): void { - const row = db - .query<{ sql: string | null }, [string]>( - "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?" - ) - .get("hook_events"); +export function up(db: DbAdapter): void { + const row = db.get( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", + "hook_events", + ) as { sql: string | null } | undefined; // Table missing (shouldn't happen — 001 creates it) or already current. if (!row?.sql) return; diff --git a/src/db/migrations/003_user_prompt_submit_event.ts b/src/db/migrations/003_user_prompt_submit_event.ts index 575c6b3..4300120 100644 --- a/src/db/migrations/003_user_prompt_submit_event.ts +++ b/src/db/migrations/003_user_prompt_submit_event.ts @@ -7,15 +7,14 @@ * already current (fresh databases or databases rebuilt by a newer 002). */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; import { CREATE_HOOK_EVENTS_TABLE, CREATE_INDEXES } from "../schema"; -export function up(db: Database): void { - const row = db - .query<{ sql: string | null }, [string]>( - "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?" - ) - .get("hook_events"); +export function up(db: DbAdapter): void { + const row = db.get( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", + "hook_events", + ) as { sql: string | null } | undefined; if (!row?.sql) return; if (row.sql.includes("UserPromptSubmit")) return; diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 5629052..850f70d 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -4,14 +4,14 @@ * Migrations are additive-only, never destructive. */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; import { up as migration001 } from "./001_initial"; import { up as migration002 } from "./002_session_events"; import { up as migration003 } from "./003_user_prompt_submit_event"; interface Migration { version: string; - up: (db: Database) => void; + up: (db: DbAdapter) => void; } const MIGRATIONS: Migration[] = [ @@ -20,7 +20,7 @@ const MIGRATIONS: Migration[] = [ { version: "003_user_prompt_submit_event", up: migration003 }, ]; -function ensureMigrationsTable(db: Database): void { +function ensureMigrationsTable(db: DbAdapter): void { db.exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, @@ -29,12 +29,12 @@ function ensureMigrationsTable(db: Database): void { `); } -function getApplied(db: Database): Set { - const rows = db.query<{ version: string }, []>("SELECT version FROM schema_migrations").all(); +function getApplied(db: DbAdapter): Set { + const rows = db.all("SELECT version FROM schema_migrations") as Array<{ version: string }>; return new Set(rows.map((r) => r.version)); } -export function runMigrations(db: Database): void { +export function runMigrations(db: DbAdapter): void { ensureMigrationsTable(db); const applied = getApplied(db); @@ -42,9 +42,10 @@ export function runMigrations(db: Database): void { if (applied.has(migration.version)) continue; migration.up(db); - db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [ + db.run( + "INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", migration.version, new Date().toISOString(), - ]); + ); } } diff --git a/src/db/migrations/migrations.test.ts b/src/db/migrations/migrations.test.ts index 10f0b10..7d54f4d 100644 --- a/src/db/migrations/migrations.test.ts +++ b/src/db/migrations/migrations.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect } from "bun:test"; -import { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; +import { createTestDb } from "../index"; import { runMigrations } from "./index"; import { up as migration002 } from "./002_session_events"; import { up as migration003 } from "./003_user_prompt_submit_event"; @@ -40,17 +41,21 @@ const PRE_003_HOOK_EVENTS_TABLE = ` ) `; -function insertEvent(db: Database, id: string, eventType: string): void { +function insertEvent(db: DbAdapter, id: string, eventType: string): void { db.run( `INSERT INTO hook_events (id, timestamp, session_id, hook_name, event_type) VALUES (?, ?, ?, ?, ?)`, - [id, new Date().toISOString(), "session-1", "sessionlog", eventType] + id, + new Date().toISOString(), + "session-1", + "sessionlog", + eventType, ); } describe("migrations", () => { test("fresh database accepts session and Codewith prompt events", () => { - const db = new Database(":memory:"); + const db = createTestDb(); runMigrations(db); insertEvent(db, "e1", "SessionStart"); @@ -58,13 +63,13 @@ describe("migrations", () => { insertEvent(db, "e3", "UserPromptSubmit"); insertEvent(db, "e4", "PreToolUse"); - const rows = db.query<{ event_type: string }, []>("SELECT event_type FROM hook_events").all(); + const rows = db.all("SELECT event_type FROM hook_events") as Array<{ event_type: string }>; expect(rows.map((r) => r.event_type).sort()).toEqual(["PreToolUse", "SessionEnd", "SessionStart", "UserPromptSubmit"]); db.close(); }); test("002 rebuilds a legacy table so session events are accepted and rows survive", () => { - const db = new Database(":memory:"); + const db = createTestDb(); db.exec(LEGACY_HOOK_EVENTS_TABLE); insertEvent(db, "legacy-1", "PreToolUse"); @@ -74,17 +79,14 @@ describe("migrations", () => { migration002(db); // Existing rows preserved - const kept = db - .query<{ id: string }, []>("SELECT id FROM hook_events") - .all() - .map((r) => r.id); + const kept = (db.all("SELECT id FROM hook_events") as Array<{ id: string }>).map((r) => r.id); expect(kept).toEqual(["legacy-1"]); // New event types accepted after rebuild insertEvent(db, "post-migration", "SessionStart"); insertEvent(db, "post-migration-2", "SessionEnd"); insertEvent(db, "post-migration-3", "UserPromptSubmit"); - const count = db.query<{ n: number }, []>("SELECT COUNT(*) as n FROM hook_events").get(); + const count = db.get("SELECT COUNT(*) as n FROM hook_events") as { n: number } | undefined; expect(count?.n).toBe(4); // Invalid event types still rejected @@ -93,7 +95,7 @@ describe("migrations", () => { }); test("003 rebuilds a pre-003 table so UserPromptSubmit is accepted and rows survive", () => { - const db = new Database(":memory:"); + const db = createTestDb(); db.exec(PRE_003_HOOK_EVENTS_TABLE); insertEvent(db, "legacy-1", "SessionStart"); @@ -102,16 +104,13 @@ describe("migrations", () => { migration003(db); insertEvent(db, "post-migration", "UserPromptSubmit"); - const rows = db - .query<{ id: string }, []>("SELECT id FROM hook_events ORDER BY id") - .all() - .map((r) => r.id); + const rows = (db.all("SELECT id FROM hook_events ORDER BY id") as Array<{ id: string }>).map((r) => r.id); expect(rows).toEqual(["legacy-1", "post-migration"]); db.close(); }); test("002 and 003 are idempotent on an already-current table", () => { - const db = new Database(":memory:"); + const db = createTestDb(); runMigrations(db); insertEvent(db, "e1", "SessionStart"); insertEvent(db, "e2", "UserPromptSubmit"); @@ -119,43 +118,40 @@ describe("migrations", () => { migration002(db); // second run must be a no-op, not a failure migration003(db); - const kept = db.query<{ id: string }, []>("SELECT id FROM hook_events").all(); + const kept = db.all("SELECT id FROM hook_events") as Array<{ id: string }>; expect(kept).toHaveLength(2); db.close(); }); test("runMigrations records all migrations exactly once", () => { - const db = new Database(":memory:"); + const db = createTestDb(); runMigrations(db); runMigrations(db); // re-running must not double-apply - const versions = db - .query<{ version: string }, []>("SELECT version FROM schema_migrations ORDER BY version") - .all() - .map((r) => r.version); + const versions = (db.all("SELECT version FROM schema_migrations ORDER BY version") as Array<{ version: string }>).map( + (r) => r.version, + ); expect(versions).toEqual(["001_initial", "002_session_events", "003_user_prompt_submit_event"]); db.close(); }); test("migrating a legacy DB via runMigrations upgrades the CHECK constraint", () => { - const db = new Database(":memory:"); + const db = createTestDb(); // Simulate a DB created by 001 only (old schema, 001 recorded) db.exec(`CREATE TABLE schema_migrations (version TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`); db.exec(LEGACY_HOOK_EVENTS_TABLE); - db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [ + db.run( + "INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", "001_initial", new Date().toISOString(), - ]); + ); insertEvent(db, "old-row", "Stop"); runMigrations(db); insertEvent(db, "new-row", "SessionEnd"); insertEvent(db, "prompt-row", "UserPromptSubmit"); - const rows = db - .query<{ id: string }, []>("SELECT id FROM hook_events ORDER BY id") - .all() - .map((r) => r.id); + const rows = (db.all("SELECT id FROM hook_events ORDER BY id") as Array<{ id: string }>).map((r) => r.id); expect(rows).toEqual(["new-row", "old-row", "prompt-row"]); db.close(); }); diff --git a/src/db/retention.ts b/src/db/retention.ts index 9a04a33..cbadcc7 100644 --- a/src/db/retention.ts +++ b/src/db/retention.ts @@ -5,16 +5,17 @@ * Called on DB open after migrations. */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; -export function runRetention(db: Database, days?: number): number { +export function runRetention(db: DbAdapter, days?: number): number { const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30"); const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays); const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString(); try { - db.run("DELETE FROM hook_events WHERE timestamp < ?", [cutoff]); - const changes = db.query<{ changes: number }, []>("SELECT changes() as changes").get()?.changes ?? 0; + db.run("DELETE FROM hook_events WHERE timestamp < ?", cutoff); + const row = db.get("SELECT changes() as changes") as { changes: number } | undefined; + const changes = row?.changes ?? 0; return changes; } catch { return 0; diff --git a/src/db/schema.test.ts b/src/db/schema.test.ts index 9aa885f..32b21a5 100644 --- a/src/db/schema.test.ts +++ b/src/db/schema.test.ts @@ -10,19 +10,31 @@ describe("hook_events schema", () => { db.run( `INSERT INTO hook_events (id, timestamp, session_id, hook_name, event_type) VALUES (?, ?, ?, ?, ?)`, - ["evt1", new Date().toISOString(), "sess", "session-start", "SessionStart"], + "evt1", + new Date().toISOString(), + "sess", + "session-start", + "SessionStart", ); db.run( `INSERT INTO hook_events (id, timestamp, session_id, hook_name, event_type) VALUES (?, ?, ?, ?, ?)`, - ["evt2", new Date().toISOString(), "sess", "prompt-guard", "UserPromptSubmit"], + "evt2", + new Date().toISOString(), + "sess", + "prompt-guard", + "UserPromptSubmit", ); db.run( `INSERT INTO hook_events (id, timestamp, session_id, hook_name, event_type) VALUES (?, ?, ?, ?, ?)`, - ["evt3", new Date().toISOString(), "sess", "fleet-catchup", "SessionEnd"], + "evt3", + new Date().toISOString(), + "sess", + "fleet-catchup", + "SessionEnd", ); - const row = db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM hook_events").get(); + const row = db.get("SELECT COUNT(*) as count FROM hook_events") as { count: number } | undefined; expect(row?.count).toBe(3); } finally { db.close(); diff --git a/src/db/schema.ts b/src/db/schema.ts index ba63010..2a51daf 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,9 +1,9 @@ /** * SQLite schema for @hasna/hooks local data. - * Owned by this package (no shared runtime dependency). + * Owned by this package and applied through the shared cloud adapter. */ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; export const CREATE_HOOK_EVENTS_TABLE = ` CREATE TABLE IF NOT EXISTS hook_events ( @@ -45,7 +45,7 @@ export interface HookEventRow { metadata: string | null; } -export function applySchema(db: Database): void { +export function applySchema(db: DbAdapter): void { db.exec(CREATE_HOOK_EVENTS_TABLE); for (const idx of CREATE_INDEXES) { db.exec(idx); diff --git a/src/db/storage-sync.ts b/src/db/storage-sync.ts index 2182d26..6f47fd2 100644 --- a/src/db/storage-sync.ts +++ b/src/db/storage-sync.ts @@ -1,4 +1,4 @@ -import type { Database } from "bun:sqlite"; +import type { DbAdapter } from "@hasna/cloud"; import { getDb } from "./index.js"; import { PG_MIGRATIONS } from "./pg-migrations.js"; import { PgAdapterAsync } from "./remote-storage.js"; @@ -147,7 +147,7 @@ export async function storageSync(options?: { tables?: string[] }): Promise<{ pu export function getSyncMetaAll(): SyncMeta[] { const db = getDb(); ensureSyncMetaTable(db); - return db.query("SELECT table_name, last_synced_at, direction FROM _hooks_sync_meta ORDER BY table_name, direction").all() as SyncMeta[]; + return db.all("SELECT table_name, last_synced_at, direction FROM _hooks_sync_meta ORDER BY table_name, direction") as SyncMeta[]; } export function getStorageStatus(): StorageStatus { @@ -177,11 +177,11 @@ export function parseStorageTables(value?: string | string[] | null): StorageTab return resolveTables(Array.isArray(value) ? value : value.split(",")); } -async function pushTable(db: Database, remote: PgAdapterAsync, table: StorageTable): Promise { +async function pushTable(db: DbAdapter, remote: PgAdapterAsync, table: StorageTable): Promise { const result: SyncResult = { table, rowsRead: 0, rowsWritten: 0, errors: [] }; try { if (!tableExists(db, table)) return result; - const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all() as Row[]; + const rows = db.all(`SELECT * FROM ${quoteIdent(table)}`) as Row[]; result.rowsRead = rows.length; if (rows.length === 0) return result; const remoteColumns = await getRemoteColumns(remote, table); @@ -193,7 +193,7 @@ async function pushTable(db: Database, remote: PgAdapterAsync, table: StorageTab return result; } -async function pullTable(remote: PgAdapterAsync, db: Database, table: StorageTable): Promise { +async function pullTable(remote: PgAdapterAsync, db: DbAdapter, table: StorageTable): Promise { const result: SyncResult = { table, rowsRead: 0, rowsWritten: 0, errors: [] }; try { if (!tableExists(db, table)) return result; @@ -221,8 +221,8 @@ function filterRemoteColumns(remoteColumns: Map, columns: string return columns.filter((column) => remoteColumns.has(column)); } -function filterLocalColumns(db: Database, table: string, columns: string[]): string[] { - const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all() as Array<{ name: string }>; +function filterLocalColumns(db: DbAdapter, table: string, columns: string[]): string[] { + const rows = db.all(`PRAGMA table_info(${quoteIdent(table)})`) as Array<{ name: string }>; const allowed = new Set(rows.map((row) => row.name)); return columns.filter((column) => allowed.has(column)); } @@ -249,7 +249,7 @@ async function upsertPg(remote: PgAdapterAsync, table: StorageTable, columns: st return rows.length; } -function upsertSqlite(db: Database, table: StorageTable, columns: string[], rows: Row[]): number { +function upsertSqlite(db: DbAdapter, table: StorageTable, columns: string[], rows: Row[]): number { if (columns.length === 0) return 0; const primaryKeys = PRIMARY_KEYS[table]; const columnList = columns.map(quoteIdent).join(", "); @@ -260,21 +260,20 @@ function upsertSqlite(db: Database, table: StorageTable, columns: string[], rows const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`; - const statement = db.query( + const statement = db.prepare( `INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ); - const insert = db.transaction((batch: Row[]) => { - for (const row of batch) statement.run(...columns.map((column) => coerceForSqlite(row[column]))); + db.transaction(() => { + for (const row of rows) statement.run(...columns.map((column) => coerceForSqlite(row[column]))); }); - insert(rows); return rows.length; } -function recordSyncMeta(db: Database, direction: "push" | "pull", results: SyncResult[]): void { +function recordSyncMeta(db: DbAdapter, direction: "push" | "pull", results: SyncResult[]): void { ensureSyncMetaTable(db); const now = new Date().toISOString(); - const statement = db.query(` + const statement = db.prepare(` INSERT INTO _hooks_sync_meta (table_name, last_synced_at, direction) VALUES (?, ?, ?) ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at @@ -285,7 +284,7 @@ function recordSyncMeta(db: Database, direction: "push" | "pull", results: SyncR } } -function ensureSyncMetaTable(db: Database): void { +function ensureSyncMetaTable(db: DbAdapter): void { db.exec(` CREATE TABLE IF NOT EXISTS _hooks_sync_meta ( table_name TEXT NOT NULL, @@ -296,8 +295,8 @@ function ensureSyncMetaTable(db: Database): void { `); } -function tableExists(db: Database, table: string): boolean { - const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table); +function tableExists(db: DbAdapter, table: string): boolean { + const row = db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table); return Boolean(row); }