gastownhall/beads · critical

pending ignored schema migrations alter pre-existing dirty t

Error message

pending ignored schema migrations alter pre-existing dirty tables: %s

What it means

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.

Source

Thrown at internal/storage/schema/schema.go:708

	}
	backfilled = backfilled || auxRekeyed

	touchedIgnoredDirtyTables, err := ignoredSource.pendingMigrationDirtyTables(ctx, db, dirtyBeforeAll)
	if err != nil {
		return applied, fmt.Errorf("checking dirty tables against pending ignored migrations: %w", err)
	}
	if len(touchedIgnoredDirtyTables) > 0 {
		// Deliberately a plain, untyped error (unlike the main-source guard
		// above, which returns *DirtyTablesError): this check fires mid-pass,
		// after the main-source migrations have already applied. A lenient
		// caller (embeddeddolt's openReadOnlyCommand / openWorkingSetReconcile
		// intents) skipping this and returning as if the open succeeded would
		// let a reconcile commit checkpoint a half-applied migration pass.
		// The ignored source also tracks bd-internal state (dolt_ignore'd
		// tables like ignored_schema_migrations), not expected user data, so
		// there is no dirty-commit recovery story to support here the way
		// there is for the main-source guard (#4566 scope).
		return applied, fmt.Errorf("pending ignored schema migrations alter pre-existing dirty tables: %s", strings.Join(touchedIgnoredDirtyTables, ", "))
	}

	appliedIgnored, ignoredColumnAdded, err := ignoredSource.migrate(ctx, db, 0)
	if err != nil {
		return applied, fmt.Errorf("ignored migrations: %w", err)
	}
	if err := unstageIgnoredTables(ctx, db); err != nil {
		return applied, fmt.Errorf("unstaging ignored migration tables: %w", err)
	}

	if applied == 0 && !backfilled && appliedIgnored == 0 && !mainColumnAdded && !ignoredColumnAdded {
		return applied, nil
	}
	changedDirtyTables, err := changedDirtyTableSignatures(ctx, db, dirtyBeforeSignatures)
	if err != nil {
		return applied, fmt.Errorf("checking pre-existing dirty table diffs: %w", err)
	}
	if len(changedDirtyTables) > 0 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Commit or revert the listed pre-existing changes in the working set, then re-run MigrateUp
  2. If the dirty rows are stale migration state from a crashed pass, restore the repo to the last clean Dolt commit and retry
  3. Do not swallow this error and continue opening - the pass is half-applied and a reconcile commit would checkpoint it
  4. Avoid hand-editing dolt_ignore'd bd-internal tables (e.g. ignored_schema_migrations)

Example fix

// before: lenient open that checkpoints a half-applied pass
if _, err := schema.MigrateUp(ctx, db); err != nil {
    log.Warn(err) // proceed anyway -> corrupt reconcile commit
}
// after: treat the guard as fatal for reconcile opens
if _, err := schema.MigrateUp(ctx, db); err != nil {
    return fmt.Errorf("refusing to open for reconcile: %w", err)
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Before upgrading, commit any pending changes so the guard cannot fire
_, _ = db.ExecContext(ctx, "CALL dolt_add('.')")
if _, err := db.ExecContext(ctx, "CALL dolt_commit('-m', 'pre-upgrade checkpoint')"); err != nil {
    return fmt.Errorf("clean working set required before upgrade: %w", err)
}

Type guard

// This error is deliberately untyped: detect it by message
func isIgnoredDirtyTablesErr(err error) bool {
    return err != nil && strings.Contains(err.Error(),
        "pending ignored schema migrations alter pre-existing dirty tables")
}

Try / catch

applied, err := schema.MigrateUp(ctx, db)
if isIgnoredDirtyTablesErr(err) {
    // half-applied pass: do NOT proceed/reconcile; commit or revert, then retry
    return fmt.Errorf("upgrade blocked; clean working set and re-run: %w", err)
}

Prevention

When it happens

Trigger: Calling MigrateUp/MigrateUpWithLock while tables such as ignored-migration targets have uncommitted user/local changes AND pending ignored-source migrations touch those same tables.

Common situations: 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.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/55eb414cada97dbb. Report an issue: GitHub.