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

  1. Find the code path that left a pooled session on another branch and restore the branch (CALL DOLT_CHECKOUT back) before releasing the connection
  2. Route branch-switching work through a dedicated connection/pool (like the fresh-dial fallback) instead of shared pooled sessions
  3. Restart/reset the pool (or server) to clear stranded sessions after fixing the cause
  4. 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

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


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