4.0 KiB
4.0 KiB
adapters/sqlite — SQLite migration adapter
(no submodules)
Purpose
Implements the MigrationAdapter contract for SQLite-family engines. The
factory createSqliteAdapter(client) accepts any object exposing three
methods (runScript, selectAll, runStatement); a small structural
wrapper makes @sqlite.org/sqlite-wasm, bun:sqlite, and better-sqlite3
all conform. This decouples the adapter from the WASM build cost when a
consumer wants to use a native driver, while still letting universal
callers bring sqlite-wasm if they need browser-tier portability.
The DDL is plain ANSI SQLite (TEXT PRIMARY KEY, INTEGER, TEXT NOT NULL),
so the same adapter works against the upstream sqlite3 CLI and against
any wrapper that supports parameterised statements.
Features
- Idempotent
CREATE TABLE IF NOT EXISTSfor the tracking table. - Parameterised inserts and deletes use
?placeholders — no caller data is interpolated into SQL. - Stores
applied_atas ISO 8601 text (SQLite has no native datetime); the adapter rehydrates to a JavaScriptDateon read. - Stores
duration_msasINTEGER. - Wraps
ensureTablefailures asMigrationError(MIGRATION.TABLE_BOOTSTRAP_FAILED). - Sync clients (
better-sqlite3,bun:sqlite) work without modification — the adapter awaits methods that may returnvoidsynchronously.
Usage: Basic Example
import { Database } from "bun:sqlite";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
const db = new Database(":memory:");
const client = {
runScript(sql: string) { db.run(sql); },
selectAll<T>(sql: string, params: unknown[] = []) {
return db.query<T, unknown[]>(sql).all(...params) as T[];
},
runStatement(sql: string, params: unknown[] = []) {
db.query(sql).run(...params);
},
};
const runner = new MigrationRunner(createSqliteAdapter(client));
await runner.runFromDirectory("./db/migrations");
Usage: Advanced Example
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";
const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB(":memory:");
const client = {
runScript(sql: string) {
db.executeBatch(sql);
},
selectAll<T>(sql: string, params: unknown[] = []) {
const out: T[] = [];
db.executeBatch({
sql,
bind: params,
rowMode: "object",
callback(row: T) { out.push(row); return undefined; },
});
return out;
},
runStatement(sql: string, params: unknown[] = []) {
db.executeBatch({ sql, bind: params });
},
};
const adapter = createSqliteAdapter(client);
API Reference
interface SqliteClient { runScript(sql); selectAll<T>(sql, params?); runStatement(sql, params?) }— structural client surface.createSqliteAdapter(client): MigrationAdapter— returns an adapter withdialect: "sqlite".
Implementation Notes
- The three-method surface is the smallest common denominator across the three SQLite drivers the suaveplan tree is likely to encounter. Every shipped adapter normalises to async by awaiting the result, so sync drivers work without modification.
applied_atis stored as ISO 8601 text because SQLite has no native datetime type. Round-trip preserves wall-clock ordering — sorting on the rawapplied_atstring yields the same order as sorting on parsedDate.- The adapter does not currently forward
AbortSignalintorunScriptbecause none of the supported SQLite drivers honor mid-query cancellation. The runner's per-migration abort gate still applies between apply attempts. - The DDL is intentionally minimal — no indices beyond the
nameuniqueness implicit inUNIQUE NOT NULL, no foreign keys. The runner reads applied rows in unspecified order and sorts bynameitself.