{"record":{"id":"55eb414cada97dbb","repo":"gastownhall/beads","slug":"pending-ignored-schema-migrations-alter-pre-existi","errorCode":null,"errorMessage":"pending ignored schema migrations alter pre-existing dirty tables: %s","messagePattern":"pending ignored schema migrations alter pre-existing dirty tables: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/schema/schema.go","lineNumber":708,"sourceCode":"\t}\n\tbackfilled = backfilled || auxRekeyed\n\n\ttouchedIgnoredDirtyTables, err := ignoredSource.pendingMigrationDirtyTables(ctx, db, dirtyBeforeAll)\n\tif err != nil {\n\t\treturn applied, fmt.Errorf(\"checking dirty tables against pending ignored migrations: %w\", err)\n\t}\n\tif len(touchedIgnoredDirtyTables) > 0 {\n\t\t// Deliberately a plain, untyped error (unlike the main-source guard\n\t\t// above, which returns *DirtyTablesError): this check fires mid-pass,\n\t\t// after the main-source migrations have already applied. A lenient\n\t\t// caller (embeddeddolt's openReadOnlyCommand / openWorkingSetReconcile\n\t\t// intents) skipping this and returning as if the open succeeded would\n\t\t// let a reconcile commit checkpoint a half-applied migration pass.\n\t\t// The ignored source also tracks bd-internal state (dolt_ignore'd\n\t\t// tables like ignored_schema_migrations), not expected user data, so\n\t\t// there is no dirty-commit recovery story to support here the way\n\t\t// there is for the main-source guard (#4566 scope).\n\t\treturn applied, fmt.Errorf(\"pending ignored schema migrations alter pre-existing dirty tables: %s\", strings.Join(touchedIgnoredDirtyTables, \", \"))\n\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 {","sourceCodeStart":690,"sourceCodeEnd":726,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L690-L726","documentation":"This is a deliberate, untyped mid-pass guard: pending ignored-source migrations would ALTER tables that were already dirty before MigrateUp started. Unlike the main-source guard (which returns typed *DirtyTablesError before any work), this fires after main migrations applied, so a lenient caller treating it as success would let a reconcile checkpoint a half-applied pass. The message lists the offending tables.","triggerScenarios":"Calling MigrateUp/MigrateUpWithLock while tables such as ignored-migration targets have uncommitted user/local changes AND pending ignored-source migrations touch those same tables.","commonSituations":"A user or process wrote to bd-internal dolt_ignore'd tables before upgrading; an interrupted earlier pass left those tables dirty; opening with reconcile intent on a working set that was never cleaned.","solutions":["Commit or revert the listed pre-existing changes in the working set, then re-run MigrateUp","If the dirty rows are stale migration state from a crashed pass, restore the repo to the last clean Dolt commit and retry","Do not swallow this error and continue opening - the pass is half-applied and a reconcile commit would checkpoint it","Avoid hand-editing dolt_ignore'd bd-internal tables (e.g. ignored_schema_migrations)"],"exampleFix":"// before: lenient open that checkpoints a half-applied pass\nif _, err := schema.MigrateUp(ctx, db); err != nil {\n    log.Warn(err) // proceed anyway -> corrupt reconcile commit\n}\n// after: treat the guard as fatal for reconcile opens\nif _, err := schema.MigrateUp(ctx, db); err != nil {\n    return fmt.Errorf(\"refusing to open for reconcile: %w\", err)\n}","handlingStrategy":"type-guard","validationCode":"// Before upgrading, commit any pending changes so the guard cannot fire\n_, _ = db.ExecContext(ctx, \"CALL dolt_add('.')\")\nif _, err := db.ExecContext(ctx, \"CALL dolt_commit('-m', 'pre-upgrade checkpoint')\"); err != nil {\n    return fmt.Errorf(\"clean working set required before upgrade: %w\", err)\n}","typeGuard":"// This error is deliberately untyped: detect it by message\nfunc isIgnoredDirtyTablesErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(),\n        \"pending ignored schema migrations alter pre-existing dirty tables\")\n}","tryCatchPattern":"applied, err := schema.MigrateUp(ctx, db)\nif isIgnoredDirtyTablesErr(err) {\n    // half-applied pass: do NOT proceed/reconcile; commit or revert, then retry\n    return fmt.Errorf(\"upgrade blocked; clean working set and re-run: %w\", err)\n}","preventionTips":["Commit or stash all working-set changes before running bd upgrades","Never write directly to bd-internal (dolt_ignore'd) tables","Always retry MigrateUp after a failed pass instead of proceeding with the open","For reconcile-style opens, fail closed on any MigrateUp error"],"tags":["database","migration","dolt","dirty-tables","data-integrity"],"backgroundTag":"migration-dirty-table-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}