nanocoai/nanoclaw · warning

Pre-existing FK violations carried through migration (not in

Error message

Pre-existing FK violations carried through migration (not introduced by it)

What it means

After running a migration with foreign keys disabled, `foreign_key_check` found violations, but all of them existed before the migration ran (matched by identity against a pre-migration snapshot). The migration itself is not at fault; the warning is informational so pre-existing corruption isn't confused with migration-introduced violations (which instead throw).

Source

Thrown at src/db/migrations/index.ts:218

  // no-op inside one); foreign_key_check runs INSIDE so a violating
  // recreate rolls back atomically with nothing committed.
  if (disableForeignKeys) raw!.pragma('foreign_keys = OFF');
  try {
    await db.transaction(async () => {
      // Snapshot violations BEFORE up() runs: live DBs can carry latent
      // FK orphans. A migration must fail only for violations it introduces.
      const preexisting = disableForeignKeys
        ? new Set((raw!.pragma('foreign_key_check') as FkViolation[]).map(fkIdentity))
        : null;
      if (override) await override.up(db);
      else if (migration.sqliteOnly) await migration.up(raw!);
      else await migration.up(db);
      if (disableForeignKeys && preexisting) {
        const violations = raw!.pragma('foreign_key_check') as FkViolation[];
        const introduced = violations.filter((violation) => !preexisting.has(fkIdentity(violation)));
        const carried = violations.length - introduced.length;
        if (carried > 0) {
          log.warn('Pre-existing FK violations carried through migration (not introduced by it)', {
            migration: migration.name,
            count: carried,
          });
        }
        if (introduced.length > 0) {
          throw new Error(`migration ${migration.name} left FK violations: ${JSON.stringify(introduced.slice(0, 5))}`);
        }
      }
      const next = (await db.get<{ v: number }>('SELECT COALESCE(MAX(version), 0) + 1 AS v FROM schema_version'))!.v;
      await db.run(
        'INSERT INTO schema_version (version, name, applied) VALUES (?, ?, ?)',
        next,
        migration.name,
        new Date().toISOString(),
      );
    });
  } finally {
    if (disableForeignKeys) raw!.pragma('foreign_keys = ON');

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Identify orphans: `pnpm exec tsx scripts/q.ts data/v2.db "pragma foreign_key_check"`
  2. Delete or reparent the orphaned rows
  3. Back up data/v2.db before manual fixes
Defensive patterns

Strategy: validation

Validate before calling

const v = db.pragma('foreign_key_check', { simple: true }) as unknown[];
if (v.length > 0) { /* resolve orphans before migrating */ }

Try / catch

try { await migrate(db); } catch (e) { if (/left FK violations/.test(String(e))) { /* migration bug */ } throw e; }

Prevention

When it happens

Trigger: `migrate()` runs `applyMigration` with `disableForeignKeys`; pre-existing orphan rows (e.g. sessions referencing a deleted agent_group) survive the migration and are counted.

Common situations: DBs corrupted by older bugs, manual deletes that bypassed FK enforcement, or restores from partial backups; surfaces during `pnpm run dev` startup or migrations after upgrading.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/ba5e3d4f32c75883. Report an issue: GitHub.