gastownhall/beads · warning
borrowed conn is on branch %q, want %q: refusing to switch a
Error message
borrowed conn is on branch %q, want %q: refusing to switch a pooled session's branch
What it means
The borrow fast path never switches a pooled session's branch: DOLT_CHECKOUT is session-level and the connection returns to the pool as-is, so changing its branch would leak a foreign branch into the pool for unrelated later callers. If `SELECT active_branch()` returns a branch different from the regular transaction's branch, the library deliberately refuses and the caller falls back to a fresh dial. Today no shipped flow diverges a pool session's branch (Checkout has no non-test callers), so this is defense-in-depth — seeing it means some code checked out a branch on a pooled session without restoring it.
Source
Thrown at internal/storage/dolt/transaction.go:393
// would leak a foreign branch into the pool for an unrelated later caller.
// Every other production checkout site (federation staging, compact, flatten)
// restores the branch before releasing the connection; the borrow path
// preserves the same invariant by refusing instead of switching. Today no
// shipped flow diverges a pool session's branch from the regular tx's branch
// (DoltStore.Checkout has no non-test callers), so this is defense in depth
// for future Checkout callers and for multi-connection tests.
//
// Instead of an unconditional checkout it verifies the session is already on
// the requested branch — the overwhelmingly common case — and sends the
// caller to the fresh-dial fallback otherwise. Same round-trip count as the
// checkout it replaces (one statement), so the borrow fast path stays free.
func beginBorrowedTx(ctx context.Context, conn *sql.Conn, branch string) (*sql.Tx, error) {
var active string
if err := conn.QueryRowContext(ctx, "SELECT active_branch()").Scan(&active); err != nil {
return nil, fmt.Errorf("failed to read borrowed conn's active branch: %w", err)
}
if active != branch {
return nil, fmt.Errorf("borrowed conn is on branch %q, want %q: refusing to switch a pooled session's branch", active, branch)
}
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to begin ignored tx: %w", err)
}
return tx, nil
}
// beginTxOnConn checks a connection out to branch and begins a transaction on
// it. Only the fallback path uses it: the fallback owns a dedicated
// single-connection pool, so checking its session out is safe. Every Dolt SQL
// session has its own active branch, so the explicit checkout is required on
// a fresh dial.
func beginTxOnConn(ctx context.Context, conn *sql.Conn, branch string) (*sql.Tx, error) {
if _, err := conn.ExecContext(ctx, "CALL DOLT_CHECKOUT(?)", branch); err != nil {
return nil, fmt.Errorf("failed to checkout ignored tx branch %s: %w", branch, err)
}
tx, err := conn.BeginTx(ctx, nil)View on GitHub (pinned to 71377f2769)
Solutions
- Find the code path that left a pooled session on another branch and restore the branch (CALL DOLT_CHECKOUT back) before releasing the connection
- Route branch-switching work through a dedicated connection/pool (like the fresh-dial fallback) instead of shared pooled sessions
- Restart/reset the pool (or server) to clear stranded sessions after fixing the cause
- Leave the refusal in place — it is intentional; do not 'fix' it by switching branches on borrowed sessions
Example fix
// before
conn, _ := db.Conn(ctx)
conn.ExecContext(ctx, "CALL DOLT_CHECKOUT(?)", "feature-x")
conn.Close() // branch leaks into the pool
// after
conn, _ := db.Conn(ctx)
conn.ExecContext(ctx, "CALL DOLT_CHECKOUT(?)", "feature-x")
defer func() { conn.ExecContext(ctx, "CALL DOLT_CHECKOUT(?)", originalBranch); conn.Close() }() Defensive patterns
Strategy: fallback
Validate before calling
var active string
if err := conn.QueryRowContext(ctx, "SELECT active_branch()").Scan(&active); err == nil && active != expectedBranch {
conn.ExecContext(ctx, "CALL DOLT_CHECKOUT(?)", expectedBranch) // restore before release
} Type guard
func sessionOnBranch(ctx context.Context, conn *sql.Conn, want string) bool {
var active string
return conn.QueryRowContext(ctx, "SELECT active_branch()").Scan(&active) == nil && active == want
} Try / catch
if err != nil && strings.Contains(err.Error(), "refusing to switch a pooled session's branch") {
// expected on branch-divergent pools: fall back to a dedicated connection
return useDedicatedConn(ctx)
} Prevention
- Always restore the original branch before returning any pooled connection
- Use DoltStore.Checkout only on dedicated connections, never shared pool sessions
- Audit custom code paths that call DOLT_CHECKOUT for missing restore-on-error (defer)
- After multi-branch tests, reset the pool so no session stays on a side branch
When it happens
Trigger: A future or custom caller of DoltStore.Checkout ran DOLT_CHECKOUT on a pooled connection and released it without switching back; multi-connection tests leaving sessions on other branches; any production code path (federation staging, compact, flatten) that failed to restore the branch before releasing.
Common situations: Writing new code that uses Checkout against the shared pool; tests with multiple connections checking out branches; a crash between checkout and restore leaving pool sessions stranded on a side branch.
Related errors
- acquire connection for gc: %w
- acquire connection for remote-ref prune: %w
- acquire connection for flatten: %w
- acquire connection for compact: %w
- acquire connection for branch: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/1e83f387add1f7eb.
Report an issue: GitHub.