decolua/9router · error · MigrationAborted
[DB][migrate] ${tableName} row-count mismatch: expected ${ro
Error message
[DB][migrate] ${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}. Dropped: What it means
Integrity assertion in importWithAssertion() during legacy db.json → SQLite migration. Rows are inserted one-by-one, failures are collected into `dropped`, and afterwards the table's row count is compared to the source array length; any mismatch means rows were dropped (constraint violations, type errors), so the function logs the dropped rows with reasons, throws MigrationAborted, and the migration aborts rather than silently losing data.
Source
Thrown at src/lib/db/migrate.js:36
// legacy db.json kept intact, marker not written → next boot retries.
export class MigrationAborted extends Error {
constructor(message, droppedRows) {
super(message);
this.name = "MigrationAborted";
this.droppedRows = droppedRows;
}
}
// Insert rows one-by-one, collect failures, then assert COUNT(*) matches input length.
function importWithAssertion(adapter, tableName, rows, insertFn, rowMeta) {
const dropped = [];
for (const row of rows) {
try { insertFn(row); }
catch (err) { dropped.push({ ...rowMeta(row), reason: err.message }); }
}
const inserted = adapter.get(`SELECT COUNT(*) as c FROM ${tableName}`)?.c ?? 0;
if (inserted !== rows.length) {
console.warn(`[DB][migrate] ${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}. Dropped:`, dropped);
throw new MigrationAborted(`${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}`, dropped);
}
}
function readJsonSafe(file) {
if (!fs.existsSync(file)) return null;
try { return JSON.parse(fs.readFileSync(file, "utf-8")); } catch { return null; }
}
function isFreshDb(adapter) {
// Table _meta may not exist yet on truly fresh DB
try {
const row = adapter.get(`SELECT COUNT(*) as c FROM _meta`);
return !row || row.c === 0;
} catch {
return true;
}
}View on GitHub (pinned to 90b52e06ff)
Solutions
- Read the `dropped` array in the warning — each entry has rowMeta and `reason` naming the exact constraint failure.
- Fix the offending rows in the legacy db.json (e.g. fill missing NOT NULL fields, dedupe ids) and re-run the migration.
- Delete the new SQLite DB file and the migration marker so the migration re-runs cleanly from db.json.
- Back up both db.json and the SQLite file before retrying.
Example fix
// before (db.json rows)
{"accounts":[{"id":"a1"},{"id":"a1"}]}
// after (dedupe)
{"accounts":[{"id":"a1"}]}
// then delete the partially-migrated SQLite file and restart to re-run migration Defensive patterns
Strategy: validation
Validate before calling
// Validate legacy db.json before migration
const db = JSON.parse(fs.readFileSync("db.json", "utf8"));
for (const t of Object.keys(db)) {
const ids = new Set();
for (const row of db[t] ?? []) {
if (row.id != null) {
if (ids.has(row.id)) throw new Error(`${t}: duplicate id ${row.id}`);
ids.add(row.id);
}
}
} Try / catch
try { await getAdapter(); /* triggers runMigrationOnce */ }
catch (e) {
if (e.message.includes("row-count mismatch")) {
console.error("Migration aborted — fix the dropped rows listed in the warning, then delete the partial SQLite DB and retry.");
}
} Prevention
- Back up db.json before upgrading versions that change the schema.
- Never hand-edit db.json; fix data through the app or a script that respects constraints.
- After a failed migration, always delete the partially-migrated SQLite file before retrying.
- Read the `dropped` entries in the warning — each names the exact row and constraint reason.
When it happens
Trigger: Running runMigrationOnce/importLegacyMain when a legacy table's data violates the new SQLite schema: NOT NULL / UNIQUE / CHECK / FK constraint violations, malformed values (e.g. non-JSON in a JSON column), duplicate primary keys, or rows corrupted in the legacy db.json.
Common situations: Upgrading from an old 9router version whose db.json predates current constraints; hand-edited db.json; partially corrupted legacy file; legacy rows referencing accounts that no longer exist.
Related errors
- ${tableName} row-count mismatch: expected ${rows.length}, go
- [DB][migrate] pre-schema backup failed (continuing): ${e.mes
- [DB] No SQLite driver available (bun/better/node/sql.js all
- [DB] bun:sqlite unavailable: ${e.message}
- [DB] better-sqlite3 unavailable: ${e.message}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/9c17ba9831a4ebbb.
Report an issue: GitHub.