{"record":{"id":"9c17ba9831a4ebbb","repo":"decolua/9router","slug":"db-migrate-tablename-row-count-mismatch-exp","errorCode":null,"errorMessage":"[DB][migrate] ${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}. Dropped:","messagePattern":"\\[DB\\]\\[migrate\\] (.+?) row-count mismatch: expected (.+?), got (.+?)\\. Dropped:","errorType":"console","errorClass":"MigrationAborted","httpStatus":null,"severity":"error","filePath":"src/lib/db/migrate.js","lineNumber":36,"sourceCode":"// legacy db.json kept intact, marker not written → next boot retries.\nexport class MigrationAborted extends Error {\n  constructor(message, droppedRows) {\n    super(message);\n    this.name = \"MigrationAborted\";\n    this.droppedRows = droppedRows;\n  }\n}\n\n// Insert rows one-by-one, collect failures, then assert COUNT(*) matches input length.\nfunction importWithAssertion(adapter, tableName, rows, insertFn, rowMeta) {\n  const dropped = [];\n  for (const row of rows) {\n    try { insertFn(row); }\n    catch (err) { dropped.push({ ...rowMeta(row), reason: err.message }); }\n  }\n  const inserted = adapter.get(`SELECT COUNT(*) as c FROM ${tableName}`)?.c ?? 0;\n  if (inserted !== rows.length) {\n    console.warn(`[DB][migrate] ${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}. Dropped:`, dropped);\n    throw new MigrationAborted(`${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}`, dropped);\n  }\n}\n\nfunction readJsonSafe(file) {\n  if (!fs.existsSync(file)) return null;\n  try { return JSON.parse(fs.readFileSync(file, \"utf-8\")); } catch { return null; }\n}\n\nfunction isFreshDb(adapter) {\n  // Table _meta may not exist yet on truly fresh DB\n  try {\n    const row = adapter.get(`SELECT COUNT(*) as c FROM _meta`);\n    return !row || row.c === 0;\n  } catch {\n    return true;\n  }\n}","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/db/migrate.js#L18-L54","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (db.json rows)\n{\"accounts\":[{\"id\":\"a1\"},{\"id\":\"a1\"}]}\n// after (dedupe)\n{\"accounts\":[{\"id\":\"a1\"}]}\n// then delete the partially-migrated SQLite file and restart to re-run migration","handlingStrategy":"validation","validationCode":"// Validate legacy db.json before migration\nconst db = JSON.parse(fs.readFileSync(\"db.json\", \"utf8\"));\nfor (const t of Object.keys(db)) {\n  const ids = new Set();\n  for (const row of db[t] ?? []) {\n    if (row.id != null) {\n      if (ids.has(row.id)) throw new Error(`${t}: duplicate id ${row.id}`);\n      ids.add(row.id);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try { await getAdapter(); /* triggers runMigrationOnce */ }\ncatch (e) {\n  if (e.message.includes(\"row-count mismatch\")) {\n    console.error(\"Migration aborted — fix the dropped rows listed in the warning, then delete the partial SQLite DB and retry.\");\n  }\n}","preventionTips":["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."],"tags":["migration","sqlite","data-integrity","constraint-violation"],"backgroundTag":"migration-row-count-mismatch","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}