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
cmd/bd/main.go:1435 — LenientOpen: isWorkingSetReconcileCommand(cmd) is
set unconditionally, so the flag does reach server mode. No plumbing is
missing.
cmd/bd/store_factory.go:77 — if cfg.ServerMode { return dolt.New(ctx, cfg) }
returns before the cfg.LenientOpen branch at :103, which is embedded-only
(embeddeddolt.OpenForWorkingSetReconcile).
internal/storage/dolt/store.go:373-379 — the LenientOpen doc comment ends
"Ignored in server mode."
internal/storage/dolt/store.go:1972 — if !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:
- 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.
- Leave
issues dirty:
INSERT INTO issues (id, title, description, design, acceptance_criteria, notes)
VALUES ('repro-1', 'uncommitted issue', '', '', '', '');
- Open the store writable (
dolt.New with a plain server config).
- 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.
Summary
Config.LenientOpenexists specifically to break the #4566 recovery deadlock("a pending migration refuses a dirty table → run
bd dolt commit→ thatcommand 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 commitcannot open aserver-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
bdentirely and hand-issueCALL DOLT_ADD(...)/CALL DOLT_COMMIT(...)through the rawdoltclient.Verified on
main@185b339be.Where it breaks
cmd/bd/main.go:1435—LenientOpen: isWorkingSetReconcileCommand(cmd)isset unconditionally, so the flag does reach server mode. No plumbing is
missing.
cmd/bd/store_factory.go:77—if cfg.ServerMode { return dolt.New(ctx, cfg) }returns before the
cfg.LenientOpenbranch at:103, which is embedded-only(
embeddeddolt.OpenForWorkingSetReconcile).internal/storage/dolt/store.go:373-379— theLenientOpendoc comment ends"Ignored in server mode."
internal/storage/dolt/store.go:1972—if !cfg.ReadOnly && !cfg.Gateway { store.initSchema(...) }.LenientOpenis never consulted, so a writableserver-mode open always runs migrations and hits
schema.DirtyTablesErrorbefore the commit can run.
For contrast, embedded does not skip
initSchema; it runs it and toleratesthe specific refusals —
internal/storage/embeddeddolt/store.go:388-422(
*schema.RemoteMigrateGateError) and:426-453(
*schema.DirtyTablesError) — warning and continuing for theopenWorkingSetReconcileintent instead of failing the open.internal/storage/schema/dirty_tables_error.go:27emits "runbd 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:schema.MigrateUpTo), thenCALL DOLT_COMMIT('-Am', ...)so the baseline is clean. v51 leaves severalpending migrations that alter
issues.issuesdirty:dolt.Newwith a plain server config).LenientOpen: true— the config the root pre-run buildsfor
bd dolt commit/bd vc commit.Observed on
185b339be: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 thisscenario 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
configtable.
configis not indolt_ignore, and0030_migrate_local_metadata_keys.up.sql:27already issuesDELETE FROM config— so a future migration touching
configwould have wedged all of them at oncewith no in-band recovery. The operator's only working escape was the raw
DOLT_ADD/DOLT_COMMITpair.Related, but not duplicates
the same policy.
Adjacent, but a different guard: it returns a deliberately untyped error and
fires mid-pass after the main migrations have applied, so it is not fixed by
honoring
LenientOpen.bd dolt commitprintsCommitted.while skipping the config table, and the pre-pull error names it as the fix #5111 (open) —bd dolt commitreports "Committed." while skipping theconfigtable. Commit-side, not open-side. Note the interaction: for aconfig-only dirty working set, honoringLenientOpenlets the open succeedbut the commit still lands nothing until those are fixed. The
issuescaseabove recovers fully.
issuesonfresh proxied init. Same symptom, different cause.
Notes on scope
schema.CheckForwardDrift) and project-identityverification both run before the
initSchemacall site, so they areunaffected: a lenient open should relax migration, not safety.
Skipping
initSchemaoutright onLenientOpen(the obvious one-line patch)over-skips and diverges from embedded, which runs the pass and only tolerates
the refusals.
cfg.ProxiedServer) errors out early today("proxy server store should be uow provider"), so it is not reachable; the
internal/storage/uowprovider has its owninitSchemawith the same guardand would need separate treatment if that path is revived.
LenientOpen, the advice indirty_tables_error.gobecomes true in both modes rather than needing to be made mode-aware.
Environment
main@185b339bedolt sql-server)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.