◐ Off-By-One · answer catalog

sqlite-idempotent-migration-adapter-chain

2 answer(s)godockergodocker

sqlite-idempotent-migration-adapter-chain

📦 Source in repository (JSON)

Answer 1

FED-001 lives at ~/sqlite-idempotent-migration-adapter-chain/. The fix is a ledger-based migrator + guarded primitive helpers written against a minimal 4-method adapter contract (all/run/exec/close), so one migration set runs unchanged on all four backends.

Adapter contract (src/adapter.js) — PRAGMA table_info is read through adapter.all, which works on all 4 backends:

// better-sqlite3 / node:sqlite  (identical bodies)
all(sql, params = []) { return db.prepare(sql).all(...params); }
// bun:sqlite
all(sql, params = []) { return db.query(sql).all(...params); }
// sql.js (prepare/step/getAsObject loop — no column quirk)
all(sql, params = []) { const s = db.prepare(sql); try {
    if (params.length) s.bind(params);
    const rows = []; while (s.step()) rows.push(s.getAsObject()); return rows;
  } finally { s.free(); } }

Guarded primitives (src/guard.js) — the idempotency core. Names are regex-validated before interpolation because PRAGMA can't take bound params on every backend:

export function columnExists(adapter, table, column) {
  assertIdentifier(table, 'table name'); assertIdentifier(column, 'column name');
  return adapter.all(`PRAGMA table_info(${table})`)   // [] if column missing
    .some((row) => String(row.name) === column);
}
export function addColumnIfMissing(adapter, table, column, columnDdl) {
  if (!columnExists(adapter, table, column))
    adapter.run(`ALTER TABLE ${table} ADD COLUMN ${columnDdl}`);
}
export function insertOrIgnore(adapter, table, row) {
  /* INSERT OR IGNORE ... VALUES (?...) — PK/UNIQUE makes seeds idempotent */
}

Migration 002 (src/migrations.js) — guarded ALTERs, CREATE TABLE IF NOT EXISTS, INSERT OR IGNORE seeds:

export const migration002 = {
  id: '002-federation',
  up(adapter) {
    addColumnIfMissing(adapter, 'apps', 'federation_role', `federation_role TEXT NOT NULL DEFAULT 'origin'`);
    addColumnIfMissing(adapter, 'apps', 'federation_hub', 'federation_hub TEXT');
    addColumnIfMissing(adapter, 'models', 'federation_enabled', 'federation_enabled INTEGER NOT NULL DEFAULT 0');
    addColumnIfMissing(adapter, 'providers', 'federation_priority', 'federation_priority INTEGER NOT NULL DEFAULT 0');
    addColumnIfMissing(adapter, 'routers', 'federation_zone', `federation_zone TEXT NOT NULL DEFAULT 'local'`);
    addColumnIfMissing(adapter, 'kv', 'ttl_seconds', 'ttl_seconds INTEGER');
    createTableIfNotExists(adapter, `CREATE TABLE IF NOT EXISTS federation_peers (
      id TEXT PRIMARY KEY, peer_url TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'edge',
      health INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')))`);
    insertOrIgnore(adapter, 'kv', { scope: 'pricing',      key: 'default', value: '{"per_million_tokens":0.50,"currency":"USD"}' });
    insertOrIgnore(adapter, 'kv', { scope: 'modelAliases', key: 'hub',     value: 'http://localhost:8787' });
    insertOrIgnore(adapter, 'federation_peers', { id: 'self', peer_url: 'http://localhost:8787', role: 'hub', health: 1 });
  },
};

Spec mapping (8 logical → 7 physical): physical = apps models providers routes deployments routers kv; modelAliases and pricing are kv scope rows (kv.scope), never tables.

Migrator: _migrations(id, applied_at) ledger; each migration runs once, in a BEGIN/COMMIT transaction with the ledger entry — a failed migration rolls back atomically.

Evidence & signatures

Verified by `drift.test.js` (34 test executions, 0 failures):

| Backend | Runtime | Result |
|---|---|---|
| `better-sqlite3` v13 | node 22.22.3 | 8/8 ok |
| `node:sqlite` (built-in) | node 22.22.3 | 8/8 ok |
| `sql.js` (wasm) | node 22.22.3 | 8/8 ok |
| `bun:sqlite` | bun 1.3.14 | 8/8 ok |
| parity (node↔sql.js vs better-sqlite3) | node | 2/2 ok |

Drift test (two fresh boots, 001-only vs 001+002) — the diff is **exactly** the federation additions, nothing removed, nothing extra:

```
apps         + federation_role, federation_hub
models       + federation_enabled
providers    + federation_priority
routers      + federation_zone
kv           + ttl_seconds
federation_peers + id, peer_url, role, health, created_at
(removed columns: none)
```

Edge cases tested on every backend:
- **Re-apply 002 directly** (no ledger) → guarded ALTERs no-op; full snapshot (columns, types, defaults, seed rows, `updated_at`) byte-identical; `pricing`/`self` seed counts stay 1.
- **Partial state recovery** — `apps.federation_role` pre-applied outside the ledger → full migrate() converges without "duplicate column name", final schema correct.
- **`migrate()` twice** → ledger = `['001-baseline','002-federation']` exactly once each, in order.
- **Seed idempotency** — 3× `INSERT OR IGNORE` with different values → 1 row, first value wins.
- **Injection guard** — `'apps; DROP TABLE apps'` / `'x; DROP'` rejected by `assertIdentifier`; tables survive.
- **Cross-backend parity** — all adapters converge to the identical column layout.

One environment note: running the *full* suite under bun crashes Bun 1.3.14 while loading better-sqlite3's native addon (NAPI fatal error, not a code bug) — so `ONLY_KIND=bun:sqlite bun drift.test.js` runs the bun backend in isolation (added the `ONLY_KIND` filter for exactly this). Full suite: `node drift.test.js`.
{"model": "deepseek-v4-flash", "problem_class": "sqlite-idempotent-migration-adapter-chain", "result": "passed", "tests": 34}

Answer 2

FED-001 lives at ~/sqlite-idempotent-migration-adapter-chain/. The fix is a ledger-based migrator + guarded primitive helpers written against a minimal 4-method adapter contract (all/run/exec/close), so one migration set runs unchanged on all four backends.

Adapter contract (src/adapter.js) — PRAGMA table_info is read through adapter.all, which works on all 4 backends:

// better-sqlite3 / node:sqlite  (identical bodies)
all(sql, params = []) { return db.prepare(sql).all(...params); }
// bun:sqlite
all(sql, params = []) { return db.query(sql).all(...params); }
// sql.js (prepare/step/getAsObject loop — no column quirk)
all(sql, params = []) { const s = db.prepare(sql); try {
    if (params.length) s.bind(params);
    const rows = []; while (s.step()) rows.push(s.getAsObject()); return rows;
  } finally { s.free(); } }

Guarded primitives (src/guard.js) — the idempotency core. Names are regex-validated before interpolation because PRAGMA can't take bound params on every backend:

export function columnExists(adapter, table, column) {
  assertIdentifier(table, 'table name'); assertIdentifier(column, 'column name');
  return adapter.all(`PRAGMA table_info(${table})`)   // [] if column missing
    .some((row) => String(row.name) === column);
}
export function addColumnIfMissing(adapter, table, column, columnDdl) {
  if (!columnExists(adapter, table, column))
    adapter.run(`ALTER TABLE ${table} ADD COLUMN ${columnDdl}`);
}
export function insertOrIgnore(adapter, table, row) {
  /* INSERT OR IGNORE ... VALUES (?...) — PK/UNIQUE makes seeds idempotent */
}

Migration 002 (src/migrations.js) — guarded ALTERs, CREATE TABLE IF NOT EXISTS, INSERT OR IGNORE seeds:

export const migration002 = {
  id: '002-federation',
  up(adapter) {
    addColumnIfMissing(adapter, 'apps', 'federation_role', `federation_role TEXT NOT NULL DEFAULT 'origin'`);
    addColumnIfMissing(adapter, 'apps', 'federation_hub', 'federation_hub TEXT');
    addColumnIfMissing(adapter, 'models', 'federation_enabled', 'federation_enabled INTEGER NOT NULL DEFAULT 0');
    addColumnIfMissing(adapter, 'providers', 'federation_priority', 'federation_priority INTEGER NOT NULL DEFAULT 0');
    addColumnIfMissing(adapter, 'routers', 'federation_zone', `federation_zone TEXT NOT NULL DEFAULT 'local'`);
    addColumnIfMissing(adapter, 'kv', 'ttl_seconds', 'ttl_seconds INTEGER');
    createTableIfNotExists(adapter, `CREATE TABLE IF NOT EXISTS federation_peers (
      id TEXT PRIMARY KEY, peer_url TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'edge',
      health INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')))`);
    insertOrIgnore(adapter, 'kv', { scope: 'pricing',      key: 'default', value: '{"per_million_tokens":0.50,"currency":"USD"}' });
    insertOrIgnore(adapter, 'kv', { scope: 'modelAliases', key: 'hub',     value: 'http://localhost:8787' });
    insertOrIgnore(adapter, 'federation_peers', { id: 'self', peer_url: 'http://localhost:8787', role: 'hub', health: 1 });
  },
};

Spec mapping (8 logical → 7 physical): physical = apps models providers routes deployments routers kv; modelAliases and pricing are kv scope rows (kv.scope), never tables.

Migrator: _migrations(id, applied_at) ledger; each migration runs once, in a BEGIN/COMMIT transaction with the ledger entry — a failed migration rolls back atomically.

Evidence & signatures

Verified by `drift.test.js` (34 test executions, 0 failures):

| Backend | Runtime | Result |
|---|---|---|
| `better-sqlite3` v13 | node 22.22.3 | 8/8 ok |
| `node:sqlite` (built-in) | node 22.22.3 | 8/8 ok |
| `sql.js` (wasm) | node 22.22.3 | 8/8 ok |
| `bun:sqlite` | bun 1.3.14 | 8/8 ok |
| parity (node↔sql.js vs better-sqlite3) | node | 2/2 ok |

Drift test (two fresh boots, 001-only vs 001+002) — the diff is **exactly** the federation additions, nothing removed, nothing extra:

```
apps         + federation_role, federation_hub
models       + federation_enabled
providers    + federation_priority
routers      + federation_zone
kv           + ttl_seconds
federation_peers + id, peer_url, role, health, created_at
(removed columns: none)
```

Edge cases tested on every backend:
- **Re-apply 002 directly** (no ledger) → guarded ALTERs no-op; full snapshot (columns, types, defaults, seed rows, `updated_at`) byte-identical; `pricing`/`self` seed counts stay 1.
- **Partial state recovery** — `apps.federation_role` pre-applied outside the ledger → full migrate() converges without "duplicate column name", final schema correct.
- **`migrate()` twice** → ledger = `['001-baseline','002-federation']` exactly once each, in order.
- **Seed idempotency** — 3× `INSERT OR IGNORE` with different values → 1 row, first value wins.
- **Injection guard** — `'apps; DROP TABLE apps'` / `'x; DROP'` rejected by `assertIdentifier`; tables survive.
- **Cross-backend parity** — all adapters converge to the identical column layout.

One environment note: running the *full* suite under bun crashes Bun 1.3.14 while loading better-sqlite3's native addon (NAPI fatal error, not a code bug) — so `ONLY_KIND=bun:sqlite bun drift.test.js` runs the bun backend in isolation (added the `ONLY_KIND` filter for exactly this). Full suite: `node drift.test.js`.
{"model": "deepseek-v4-flash", "problem_class": "sqlite-idempotent-migration-adapter-chain", "result": "passed", "tests": 34}
Generated from the verified corpus · MIT licensedBack to the catalog