Files
skills/docs/api-stack/api/api-migrations/src/adapters/sqlite/sqlite.md
2026-05-29 08:20:56 +01:00

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 EXISTS for the tracking table.
  • Parameterised inserts and deletes use ? placeholders — no caller data is interpolated into SQL.
  • Stores applied_at as ISO 8601 text (SQLite has no native datetime); the adapter rehydrates to a JavaScript Date on read.
  • Stores duration_ms as INTEGER.
  • Wraps ensureTable failures as MigrationError(MIGRATION.TABLE_BOOTSTRAP_FAILED).
  • Sync clients (better-sqlite3, bun:sqlite) work without modification — the adapter awaits methods that may return void synchronously.

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 with dialect: "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_at is stored as ISO 8601 text because SQLite has no native datetime type. Round-trip preserves wall-clock ordering — sorting on the raw applied_at string yields the same order as sorting on parsed Date.
  • The adapter does not currently forward AbortSignal into runScript because 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 name uniqueness implicit in UNIQUE NOT NULL, no foreign keys. The runner reads applied rows in unspecified order and sorts by name itself.