{"record":{"id":"1e83f387add1f7eb","repo":"gastownhall/beads","slug":"borrowed-conn-is-on-branch-q-want-q-refusing-t","errorCode":null,"errorMessage":"borrowed conn is on branch %q, want %q: refusing to switch a pooled session's branch","messagePattern":"borrowed conn is on branch %q, want %q: refusing to switch a pooled session's branch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/storage/dolt/transaction.go","lineNumber":393,"sourceCode":"// would leak a foreign branch into the pool for an unrelated later caller.\n// Every other production checkout site (federation staging, compact, flatten)\n// restores the branch before releasing the connection; the borrow path\n// preserves the same invariant by refusing instead of switching. Today no\n// shipped flow diverges a pool session's branch from the regular tx's branch\n// (DoltStore.Checkout has no non-test callers), so this is defense in depth\n// for future Checkout callers and for multi-connection tests.\n//\n// Instead of an unconditional checkout it verifies the session is already on\n// the requested branch — the overwhelmingly common case — and sends the\n// caller to the fresh-dial fallback otherwise. Same round-trip count as the\n// checkout it replaces (one statement), so the borrow fast path stays free.\nfunc beginBorrowedTx(ctx context.Context, conn *sql.Conn, branch string) (*sql.Tx, error) {\n\tvar active string\n\tif err := conn.QueryRowContext(ctx, \"SELECT active_branch()\").Scan(&active); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read borrowed conn's active branch: %w\", err)\n\t}\n\tif active != branch {\n\t\treturn nil, fmt.Errorf(\"borrowed conn is on branch %q, want %q: refusing to switch a pooled session's branch\", active, branch)\n\t}\n\ttx, err := conn.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to begin ignored tx: %w\", err)\n\t}\n\treturn tx, nil\n}\n\n// beginTxOnConn checks a connection out to branch and begins a transaction on\n// it. Only the fallback path uses it: the fallback owns a dedicated\n// single-connection pool, so checking its session out is safe. Every Dolt SQL\n// session has its own active branch, so the explicit checkout is required on\n// a fresh dial.\nfunc beginTxOnConn(ctx context.Context, conn *sql.Conn, branch string) (*sql.Tx, error) {\n\tif _, err := conn.ExecContext(ctx, \"CALL DOLT_CHECKOUT(?)\", branch); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to checkout ignored tx branch %s: %w\", branch, err)\n\t}\n\ttx, err := conn.BeginTx(ctx, nil)","sourceCodeStart":375,"sourceCodeEnd":411,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dolt/transaction.go#L375-L411","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconn, _ := db.Conn(ctx)\nconn.ExecContext(ctx, \"CALL DOLT_CHECKOUT(?)\", \"feature-x\")\nconn.Close() // branch leaks into the pool\n// after\nconn, _ := db.Conn(ctx)\nconn.ExecContext(ctx, \"CALL DOLT_CHECKOUT(?)\", \"feature-x\")\ndefer func() { conn.ExecContext(ctx, \"CALL DOLT_CHECKOUT(?)\", originalBranch); conn.Close() }()","handlingStrategy":"fallback","validationCode":"var active string\nif err := conn.QueryRowContext(ctx, \"SELECT active_branch()\").Scan(&active); err == nil && active != expectedBranch {\n    conn.ExecContext(ctx, \"CALL DOLT_CHECKOUT(?)\", expectedBranch) // restore before release\n}","typeGuard":"func sessionOnBranch(ctx context.Context, conn *sql.Conn, want string) bool {\n    var active string\n    return conn.QueryRowContext(ctx, \"SELECT active_branch()\").Scan(&active) == nil && active == want\n}","tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"refusing to switch a pooled session's branch\") {\n    // expected on branch-divergent pools: fall back to a dedicated connection\n    return useDedicatedConn(ctx)\n}","preventionTips":["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"],"tags":["go","dolt","branch","connection-pool","session-state"],"backgroundTag":"pooled-session-branch-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}