{"record":{"id":"10bab53ffb008540","repo":"gastownhall/beads","slug":"pre-existing-dirty-tables-changed-during-schema-mi","errorCode":null,"errorMessage":"pre-existing dirty tables changed during schema migration: %s","messagePattern":"pre-existing dirty tables changed during schema migration: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/schema/schema.go","lineNumber":727,"sourceCode":"\t}\n\n\tappliedIgnored, ignoredColumnAdded, err := ignoredSource.migrate(ctx, db, 0)\n\tif err != nil {\n\t\treturn applied, fmt.Errorf(\"ignored migrations: %w\", err)\n\t}\n\tif err := unstageIgnoredTables(ctx, db); err != nil {\n\t\treturn applied, fmt.Errorf(\"unstaging ignored migration tables: %w\", err)\n\t}\n\n\tif applied == 0 && !backfilled && appliedIgnored == 0 && !mainColumnAdded && !ignoredColumnAdded {\n\t\treturn applied, nil\n\t}\n\tchangedDirtyTables, err := changedDirtyTableSignatures(ctx, db, dirtyBeforeSignatures)\n\tif err != nil {\n\t\treturn applied, fmt.Errorf(\"checking pre-existing dirty table diffs: %w\", err)\n\t}\n\tif len(changedDirtyTables) > 0 {\n\t\treturn applied, fmt.Errorf(\"pre-existing dirty tables changed during schema migration: %s\", strings.Join(changedDirtyTables, \", \"))\n\t}\n\n\tstaged, err := stageSchemaTables(ctx, db, dirtyBefore)\n\tif err != nil {\n\t\treturn applied, fmt.Errorf(\"staging migrations: %w\", err)\n\t}\n\tif !staged {\n\t\treturn applied, nil\n\t}\n\tif err := DrainCall(ctx, db, \"CALL DOLT_COMMIT('-m', 'schema: apply migrations')\"); err != nil {\n\t\tif !strings.Contains(strings.ToLower(err.Error()), \"nothing to commit\") {\n\t\t\treturn applied, fmt.Errorf(\"committing migrations: %w\", err)\n\t\t}\n\t}\n\n\treturn applied, nil\n}\n","sourceCodeStart":709,"sourceCodeEnd":745,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L709-L745","documentation":"MigrateUp verifies that no pre-existing dirty table changed during the migration pass by comparing post-pass table signatures to pre-pass snapshots. When one or more dirty tables differ, this plain error names them. It means the migration pass touched user data it was supposed to leave alone — a safety invariant violation — so the pass aborts before staging and committing the schema.","triggerScenarios":"Calling MigrateUp/MigrateUpWithLock on a database where dirty (unstaged, user-modified) tables exist and some migration/backfill/rekey step (e.g. rekeyDependencyIDs, rekeyAuxRowIDsAllPasses, ensureBackfilledCustomStatusesCustomTypes) modified rows in one of those tables, so changedDirtyTableSignatures returns a non-empty list.","commonSituations":"Users working directly in the database (uncommitted DML in dirty tables) while an upgrade runs, a bug in a backfill/rekey pass that writes to user tables, or a partially recovered database where dirty tables were left in an unexpected state by a previous crash — the diff then flags them and blocks the commit.","solutions":["Read the table names in the error and inspect what changed in them (dolt diff / dolt status) to determine whether the change was expected user work or migration fallout","If the changes are legitimate user work, stage/commit or otherwise settle the dirty tables first, then re-run MigrateUp on a clean working set","If a migration/backfill step wrongly wrote to user tables, capture the diff output and file/fix the migration bug before retrying","Restore affected dirty tables from backup if migration writes corrupted them","Re-run MigrateUp with no dirty tables present so the invariant check passes"],"exampleFix":"// before: upgrading with uncommitted user edits in dirty tables\n_, err := schema.MigrateUp(ctx, db) // error: dirty tables changed\n// after: commit or stash user work so no dirty tables exist pre-migration\n// (dolt add/commit or dolt reset first)\n_, err := schema.MigrateUp(ctx, db)","handlingStrategy":"validation","validationCode":"// Before migrating, ensure the working set has no dirty (uncommitted) tables\nrows, err := db.QueryContext(ctx, \"SELECT table_name FROM dolt_status WHERE staged=false AND status='modified'\")\nif err != nil {\n    return err\n}\ndefer rows.Close()\nvar dirty []string\nfor rows.Next() {\n    var t string\n    if err := rows.Scan(&t); err != nil {\n        return err\n    }\n    dirty = append(dirty, t)\n}\nif len(dirty) > 0 {\n    return fmt.Errorf(\"commit or reset dirty tables before migrating: %s\", strings.Join(dirty, \", \"))\n}","typeGuard":"// Narrow the typed dirty-tables error that the main-source guard returns\nfunc asDirtyTables(err error) (tables []string, ok bool) {\n    var dte *schema.DirtyTablesError\n    if errors.As(err, &dte) {\n        return dte.Tables, true\n    }\n    return nil, false\n}","tryCatchPattern":"applied, err := schema.MigrateUp(ctx, db)\nif err != nil {\n    if strings.Contains(err.Error(), \"pre-existing dirty tables changed during schema migration:\") {\n        // Invariant violation: user data changed. Capture the diff and\n        // DO NOT commit or reconcile; restore/fix before retry.\n        return fmt.Errorf(\"migration mutated dirty tables; restore from backup: %w\", err)\n    }\n    return err\n}","preventionTips":["Never run upgrades while users have uncommitted DML in the database — commit or stash first","Diff the named tables immediately to determine whether changes were user work or migration fallout","Keep pre-migration backups so dirty tables can be restored if a backfill/rekey writes to them","Report migrations that touch user tables as bugs — this error signals an invariant violation, not routine behavior"],"tags":["database","migration","data-integrity","dolt"],"backgroundTag":"dirty-table-changed-during-migration","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}