gastownhall/beads · error
scan dirty config key: %w
Error message
scan dirty config key: %w
What it means
While iterating the dolt_diff rows for the config table, each key is scanned into a sql.NullString (COALESCE(to_key, from_key) can be NULL in theory). This error wraps a rows.Scan failure on that scan, aborting the safety check and therefore the merge/pull before staging. The scan into NullString rarely fails, so this usually indicates a driver/protocol problem rather than data shape.
Source
Thrown at internal/storage/dolt/store.go:3193
// namespace (not just kv.memory.*) un-wedges generic `bd kv set` writes too: a
// kv.* row is this clone's own data, exactly as safe to auto-commit as a memory,
// and a genuine kv.* merge conflict is still left for the operator because
// auto-resolution stays kv.memory.*-only (configConflictsAreMemoryConvergent).
// config's primary key is `key`, so dolt_diff exposes to_key/from_key; an add or
// delete leaves one side NULL, so COALESCE picks whichever key the change carries.
func (s *DoltStore) assertDirtyConfigUserKVOnly(ctx context.Context, conn *sql.Conn) error {
rows, err := conn.QueryContext(ctx,
"SELECT COALESCE(to_key, from_key) FROM dolt_diff('HEAD', 'WORKING', 'config')")
if err != nil {
return fmt.Errorf("inspect dirty config before pull: %w", err)
}
defer rows.Close()
var unsafe []string
for rows.Next() {
var key sql.NullString
if err := rows.Scan(&key); err != nil {
return fmt.Errorf("scan dirty config key: %w", err)
}
if key.Valid && !strings.HasPrefix(key.String, kvkeys.Prefix) {
unsafe = append(unsafe, key.String)
}
}
if err := rows.Err(); err != nil {
return fmt.Errorf("iterate dirty config diff: %w", err)
}
if len(unsafe) > 0 {
return fmt.Errorf("refusing to auto-commit %d dirty internal config key(s) before pull: %s; "+
"only user %s* keys auto-commit before a pull (GH#2455) — commit or revert "+
"these explicitly with `bd dolt commit` first", len(unsafe), strings.Join(unsafe, ", "), kvkeys.Prefix)
}
return nil
}
// CommitWithConfig creates a Dolt commit that includes the config table.
// Use this instead of Commit when the caller intentionally modified configView on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped driver message; retry the pull with a fresh connection.
- Align go-mysql-driver and Dolt server versions if the wire type differs from expectations.
- Verify the diff query manually in a Dolt SQL shell to rule out server-side anomalies.
- Re-run bd sync/pull after fixing; the abort is pre-staging so no partial merge state remains.
Defensive patterns
Strategy: retry
Validate before calling
// sanity-run the same diff manually before pull:
// SELECT COALESCE(to_key, from_key) FROM dolt_diff('HEAD','WORKING','config'); Try / catch
if err != nil && strings.Contains(err.Error(), "scan dirty config key") {
// driver/protocol issue — retry on fresh connection, then check versions
} Prevention
- Match go-mysql-driver and Dolt server versions.
- Avoid network interruptions to remote Dolt during sync.
- Re-run pull after the failure — abort is pre-staging, so no partial state.
- Inspect the diff output manually if the failure repeats.
When it happens
Trigger: The scan fails despite sql.NullString: driver protocol error mid-row, corrupted result stream, or a Dolt version returning the diff columns with an unexpected wire type.
Common situations: Remote Dolt connection degraded mid-result; version skew between the Go driver and Dolt server; interrupted embedded engine during the pull path.
Related errors
- failed to scan dolt_status: %w
- ErrScan
- failed to begin transaction: %w
- failed to recompute is_blocked: %w
- failed to commit is_blocked repairs: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5589b8a52f662a34.
Report an issue: GitHub.