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 config

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped driver message; retry the pull with a fresh connection.
  2. Align go-mysql-driver and Dolt server versions if the wire type differs from expectations.
  3. Verify the diff query manually in a Dolt SQL shell to rule out server-side anomalies.
  4. 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

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


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