Skip to content

Server mode ignores Config.LenientOpen: bd dolt commit cannot open past the dirty-table migration guard (#4566 was embedded-only) #5781

Description

@Toady00

Summary

Config.LenientOpen exists specifically to break the #4566 recovery deadlock
("a pending migration refuses a dirty table → run bd dolt commit → that
command opens the store → the open runs the migration → same refusal"). It is
only honored on the embedded path. In server mode the flag reaches the store
and is then ignored, so bd dolt commit / bd vc commit cannot open a
server-backed database whose working set is dirty on a table a pending
migration alters.

#4566 was titled and scoped to embedded mode, and #4567 fixed it there. Server
mode was never covered. The result is that against an external Dolt server the
refusal's own documented recovery is impossible to execute, and — unlike
embedded — there is no local database directory to move aside and re-clone. The
only escape is to bypass bd entirely and hand-issue CALL DOLT_ADD(...) /
CALL DOLT_COMMIT(...) through the raw dolt client.

Verified on main @ 185b339be.

Where it breaks

  1. cmd/bd/main.go:1435LenientOpen: isWorkingSetReconcileCommand(cmd) is
    set unconditionally, so the flag does reach server mode. No plumbing is
    missing.
  2. cmd/bd/store_factory.go:77if cfg.ServerMode { return dolt.New(ctx, cfg) }
    returns before the cfg.LenientOpen branch at :103, which is embedded-only
    (embeddeddolt.OpenForWorkingSetReconcile).
  3. internal/storage/dolt/store.go:373-379 — the LenientOpen doc comment ends
    "Ignored in server mode."
  4. internal/storage/dolt/store.go:1972if !cfg.ReadOnly && !cfg.Gateway { store.initSchema(...) }. LenientOpen is never consulted, so a writable
    server-mode open always runs migrations and hits schema.DirtyTablesError
    before the commit can run.

For contrast, embedded does not skip initSchema; it runs it and tolerates
the specific refusals — internal/storage/embeddeddolt/store.go:388-422
(*schema.RemoteMigrateGateError) and :426-453
(*schema.DirtyTablesError) — warning and continuing for the
openWorkingSetReconcile intent instead of failing the open.

internal/storage/schema/dirty_tables_error.go:27 emits "run bd dolt commit"
unconditionally, including in server mode where that instruction is currently
guaranteed to fail.

Repro

Minimal, beads-only, no orchestrator. Against a dolt sql-server:

  1. Create a database and migrate it to schema v51 (schema.MigrateUpTo), then
    CALL DOLT_COMMIT('-Am', ...) so the baseline is clean. v51 leaves several
    pending migrations that alter issues.
  2. Leave issues dirty:
    INSERT INTO issues (id, title, description, design, acceptance_criteria, notes)
    VALUES ('repro-1', 'uncommitted issue', '', '', '', '');
  3. Open the store writable (dolt.New with a plain server config).
  4. Open the store with LenientOpen: true — the config the root pre-run builds
    for bd dolt commit / bd vc commit.

Observed on 185b339be:

step 3: failed to initialize schema: schema migration: pending schema migrations
        alter pre-existing dirty tables: issues; run 'bd dolt commit' to commit
        the working set at the current schema, then re-run the migration
        (gastownhall/beads#4566)

step 4: identical error

Expected: step 3 refuses (correct, that is the guard doing its job), and step 4
opens on the current schema without migrating, so the commit that clears the
dirty state can actually run.

Embedded mode already behaves this way — see
internal/storage/embeddeddolt/dirty_tables_gate_test.go, which is exactly this
scenario at the embedded seam.

How this was found

Diagnosing repeated external-Dolt init failures against a live server (one
database per project). 15 of 21 databases carried a permanently dirty config
table. config is not in dolt_ignore, and
0030_migrate_local_metadata_keys.up.sql:27 already issues DELETE FROM config
— so a future migration touching config would have wedged all of them at once
with no in-band recovery. The operator's only working escape was the raw
DOLT_ADD / DOLT_COMMIT pair.

Related, but not duplicates

Notes on scope

  • The forward-drift guard (schema.CheckForwardDrift) and project-identity
    verification both run before the initSchema call site, so they are
    unaffected: a lenient open should relax migration, not safety.
  • A lenient open must still apply pending migrations when nothing is dirty.
    Skipping initSchema outright on LenientOpen (the obvious one-line patch)
    over-skips and diverges from embedded, which runs the pass and only tolerates
    the refusals.
  • The proxied-server path (cfg.ProxiedServer) errors out early today
    ("proxy server store should be uow provider"), so it is not reachable; the
    internal/storage/uow provider has its own initSchema with the same guard
    and would need separate treatment if that path is revived.
  • Once server mode honors LenientOpen, the advice in dirty_tables_error.go
    becomes true in both modes rather than needing to be made mode-aware.

Environment

  • main @ 185b339be
  • Dolt 2.2.3, server mode (dolt sql-server)
  • macOS 26.5.2 arm64, go1.26.5

I have a fix and a server-mode regression test (including the negative case: a
lenient open of a clean database must still migrate to latest); PR to follow.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions