{"record":{"id":"9d64934b4f084480","repo":"decolua/9router","slug":"tablename-row-count-mismatch-expected-rows-l","errorCode":null,"errorMessage":"${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}","messagePattern":"(.+?) row-count mismatch: expected (.+?), got (.+?)","errorType":"exception","errorClass":"MigrationAborted","httpStatus":null,"severity":"critical","filePath":"src/lib/db/migrate.js","lineNumber":37,"sourceCode":"export 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}\n","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/db/migrate.js#L19-L55","documentation":"During legacy db.json → SQLite migration, importWithAssertion copies every row from the legacy JSON into the new table via insertFn, collecting insert failures in `dropped` instead of throwing. It then compares the table's actual COUNT(*) against the number of source rows; a mismatch means one or more rows failed to insert, so it aborts the whole migration with MigrationAborted rather than silently losing data.","triggerScenarios":"Any insertFn(row) throwing inside the loop (constraint violations, NOT NULL columns, invalid JSON for a JSON column, duplicate primary key) so that COUNT(*) < rows.length. Also if rows.length is miscounted or the table had pre-existing rows making COUNT(*) > rows.length.","commonSituations":"Users with hand-edited or corrupted legacy db.json files (nulls where non-null is required, ids that clash, fields of the wrong type); schema drift after an app upgrade where the new SQLite schema is stricter than what the legacy JSON contained; a partially-completed previous migration leaving rows in the table.","solutions":["Read the console.warn output '[DB][migrate] ... Dropped:' — each dropped row lists rowMeta and the reason; fix or remove the offending rows in the legacy db.json (default ~/.9router/db.json) and retry.","Check for duplicate `id` values or NOT NULL violations in the legacy JSON and correct them before re-running migration.","If the target table already contains rows from a failed prior migration, delete/reset the SQLite database (or the specific table) so the import starts clean.","Update the app: newer versions may have relaxed the schema or added row sanitization during import."],"exampleFix":"// before: legacy row with duplicate id causes silent drop then abort\n{ \"apiKeys\": [ { \"id\": \"k1\", \"name\": \"a\" }, { \"id\": \"k1\", \"name\": \"b\" } ] }\n// after: give every row a unique id\n{ \"apiKeys\": [ { \"id\": \"k1\", \"name\": \"a\" }, { \"id\": \"k2\", \"name\": \"b\" } ] }","handlingStrategy":"try-catch","validationCode":"// sanity-check legacy rows before migration\nfunction assertRowsMigratable(tableName, rows) {\n  const ids = new Set();\n  for (const r of rows) {\n    if (r.id == null) throw new Error(`${tableName}: row without id`);\n    if (ids.has(r.id)) throw new Error(`${tableName}: duplicate id ${r.id}`);\n    ids.add(r.id);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await migrateLegacyDb();\n} catch (err) {\n  if (err instanceof MigrationAborted) {\n    console.error(\"Migration aborted; dropped rows:\", err.dropped ?? err.details);\n    // keep legacy db.json untouched and surface `dropped` to the user\n  } else throw err;\n}","preventionTips":["Never hand-edit ~/.9router/db.json; back it up before upgrades.","Ensure every legacy row has a unique, non-null id matching the new schema.","Watch the '[DB][migrate] ... Dropped:' warn lines — they name each failing row and reason.","If a migration fails, don't retry blindly; fix the listed rows or start from a clean SQLite db."],"tags":["database","migration","data-integrity"],"backgroundTag":"migration-row-count-mismatch","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}