Skip to content

feat(storage): rebind transaction and pinned-connection SQL exactly once - #950

Merged
Kiran01bm merged 10 commits into
mainfrom
kiran01bm/sqlstore-tx-conn-rebind
Aug 7, 2026
Merged

feat(storage): rebind transaction and pinned-connection SQL exactly once#950
Kiran01bm merged 10 commits into
mainfrom
kiran01bm/sqlstore-tx-conn-rebind

Conversation

@Kiran01bm

Copy link
Copy Markdown
Collaborator

Summary

Extends the placeholder-rebind boundary from direct pool execution to transactions and pinned connections. Every store statement — pool, transaction, or pinned-connection transaction — now passes through the dialect's binder exactly once, on the final assembled SQL, at the moment it executes.

What

  • rebindDB.BeginTx returns a *rebindTx and rebindDB.Conn returns a *rebindConn, so stores can no longer obtain a raw handle that bypasses the binder.
  • rebindTx rebinds ExecContext / QueryContext / QueryRowContext and passes Commit / Rollback through; rebindConn begins rebind-aware transactions on the pinned session.
  • Store internals (applies.go, apply_operations.go, control_requests.go, sql_helpers.go) now carry the wrapper types through their transaction and lock-connection plumbing.
  • The advisory-lock flow keeps one sanctioned escape: rebindConn.raw() hands namedlock.Locker the pinned *sql.Conn, since locker implementations emit their engine's native placeholders and must run on the session that holds the lock.
  • Unit tests prove exactly-once rebinding on every path and that the raw() escape never invokes the binder.

Why

The store builds all SQL with MySQL-style ? placeholders; engines with different wire syntax (Postgres $n) rebind at the execution boundary. Until now only direct pool execution was wrapped — statements running inside transactions or on pinned connections reached the driver unrebound, which would break the first non-identity binder. Closing those paths makes the rebind boundary complete before a Postgres dialect lands.

                    ┌──────────────────┐
   store SQL (?)───▶│     rebindDB     │
                    │  ExecContext ──┐ │
                    │  BeginTx ─▶ rebindTx ── Rebind(q) ──▶ driver
                    │  Conn ────▶ rebindConn               ($n / ?)
                    │              │  BeginTx ─▶ rebindTx  │
                    │              └─ raw() ───────────────▶ namedlock
                    └──────────────────┘        (native SQL, no rebind)

Kiran01bm and others added 8 commits August 6, 2026 14:55
…torage

Callers only use the interface method set; widening the server field,
buildGRPCTernClient, and test-helper signatures removes the concrete-type
coupling so a second storage backend can be wired without touching callers.
…coupling

Rename locals that shadowed the storage package import and add a
compile-time storage.Storage conformance assertion in mysqlstore.
…ore core

Mechanical move (rename-detected) of the store implementation and its
white-box tests; mysqlstore becomes a thin public constructor over the
shared core so a second dialect backend can assemble the same store
logic with its own dependencies. No SQL or behavior changes.
…apper

Stores hold a rebindDB instead of a raw *sql.DB, so every statement
executed directly on the pool passes through the dialect's placeholder
binder exactly once (identity for MySQL). This is the execution seam a
Postgres backend needs to rewrite "?" placeholders to "$n" without
touching store SQL. Transaction and pinned-connection handles remain
raw passthroughs for a follow-up.
Extend the rebind boundary from direct pool execution to transactions
and pinned connections: BeginTx/Conn now return rebind-aware wrappers,
so every store statement rebinds its placeholders exactly once at
execution time. The advisory-lock flow keeps a sanctioned raw()
escape because namedlock.Locker emits engine-native SQL on *sql.Conn.
…ore core (#948)

Mechanical move (rename-detected) of the store implementation and its
white-box tests; mysqlstore becomes a thin public constructor over the
shared core so a second dialect backend can assemble the same store
logic with its own dependencies. No SQL or behavior changes.
…apper (#949)

* refactor(storage): move mysqlstore internals to shared internal/sqlstore core

Mechanical move (rename-detected) of the store implementation and its
white-box tests; mysqlstore becomes a thin public constructor over the
shared core so a second dialect backend can assemble the same store
logic with its own dependencies. No SQL or behavior changes.

* feat(storage): route direct pool execution through rebind-aware DB wrapper

Stores hold a rebindDB instead of a raw *sql.DB, so every statement
executed directly on the pool passes through the dialect's placeholder
binder exactly once (identity for MySQL). This is the execution seam a
Postgres backend needs to rewrite "?" placeholders to "$n" without
touching store SQL. Transaction and pinned-connection handles remain
raw passthroughs for a follow-up.
Base automatically changed from kiran01bm/sqlstore-rebind-wrapper to kiran01bm/storage-iface-decouple August 6, 2026 23:33
@Kiran01bm
Kiran01bm marked this pull request as ready for review August 6, 2026 23:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Base automatically changed from kiran01bm/storage-iface-decouple to main August 6, 2026 23:42
@aparajon

aparajon commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

🤖 Adversarial correctness review, requested by Armand and performed by his agent. Reviewed at head 9da44a5.

Verdict: correct and complete — the rebind boundary is now genuinely closed, and I couldn't find a path around it. The only thing standing between this and merge is mechanical: the branch conflicts with main because it still carries its own pre-squash copies of the #948/#949 commits. Note the review scope: the true delta over what's already merged is just the 4a76fb18 commit (8 files, +210/−57) — the rendered +533 double-counts already-landed stack content, and will collapse once the branch reconciles with main.

Findings

1. (mechanical, gates merge) The branch is unmergeable until it reconciles with main. #948 and #949 squash-merged while this branch still had its own copies of those commits, so GitHub reports a conflict and inflates the diff. Nothing in the conflicting content is a semantic divergence — I diffed the branch's copies against the squashed versions and the head merge commit resolved cleanly with no hand-edits.

Action items

  1. (Finding 1) Merge main into the branch (or rebase dropping the duplicated commits) so the PR is mergeable and the rendered diff shrinks to the real delta.

Verified (tried to break, couldn't)

The bypass surface is closed by construction — after this change the only *sql.Tx/*sql.Conn/*sql.DB mentions in the package are the wrapper internals, the New(db *sql.DB) entry point, and the unexported raw() escape, so no store code can reach an unrebound handle without it showing up in review; every binder.Rebind call sits at an execution point (never during assembly), no prepared-statement path exists to smuggle SQL past it, and the identity inserters execute through the rebind-aware queryExecer; the raw() escape exactly matches namedlock.Locker's contract (locker SQL is engine-native by design, and the Postgres locker already emits its own syntax) and the new test proves the binder is never invoked on that path; the replaced passthrough test's invariant is covered more strongly by the new suite (the counting binder now detects double-rebinds and the assertions check the SQL the driver actually received, on all three paths — pool, pool-tx, pinned-conn-tx); the head merge commit contains no conflict-resolution edits; go build ./..., go test -race ./pkg/storage/..., and the sqlstore integration suite (testcontainers) all pass locally at head, and CI is fully green.

This review was generated by Claude Code (claude-fable-5).

@aparajon aparajon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Approving on Armand's behalf after the adversarial correctness review above (no blocking correctness findings; merge after reconciling the branch with main). This stamp was left by Claude Code (claude-fable-5).

…contracts

Make the two binder bypasses grep-distinct and self-documenting: the
advisory-lock escape is lockerConn(), and Raw is lifecycle-only with no
SQL allowed through it. Also let the recording driver stub accept
transaction options so the isolation-level-bearing BeginTx paths are
unit-covered.
Copilot AI lite review requested due to automatic review settings August 7, 2026 04:15
@Kiran01bm

Copy link
Copy Markdown
Collaborator Author

Review response from Kiran's (@Kiran01bm) AI code review assessment agent

Summary: the single (mechanical) finding is fixed — the branch has reconciled with main and the PR is now mergeable, with the rendered diff collapsed to the true delta.

# Finding Status Explanation
1 Branch unmergeable: carries pre-squash copies of the #948/#949 commits, inflating the diff and conflicting with main fixed Base retargeted to main after the stack below merged (#947#949); main merged into the branch at head 7834297 with no semantic hand-edits. GitHub now reports the PR mergeable and the diff shows only this PR's delta.
"Verified (tried to break, couldn't)" confirmations no action Noted with thanks. One heads-up: the head merge also carries a small post-review refinement from a separate internal review — the unexported raw() escape referenced in the verified section is now lockerConn() (grep-distinct from the lifecycle-only Raw), with the escape contracts documented and isolation-level BeginTx paths added to the unit suite. No behavior change.

@Kiran01bm
Kiran01bm enabled auto-merge (squash) August 7, 2026 04:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the SQL placeholder-rebinding boundary in pkg/storage/internal/sqlstore by ensuring that statements executed via transactions and pinned connections also pass through the dialect binder exactly once at the execution edge (closing gaps that would break non-identity binders like Postgres $n).

Changes:

  • Wrap BeginTx and Conn to return rebind-aware transaction/connection handles (*rebindTx, *rebindConn) so transactional and pinned-connection execution paths rebind placeholders exactly once.
  • Thread the wrapper types through store internals (apply/control-request flows) and preserve a single advisory-lock escape hatch via rebindConn.raw() (to provide the pinned *sql.Conn to namedlock).
  • Add unit tests asserting “exactly-once” rebinding across pool, transaction, pinned-connection transaction, and that the raw() escape bypasses the binder.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/storage/internal/sqlstore/db.go Introduces rebindTx / rebindConn and wraps BeginTx/Conn to enforce exactly-once rebinding at execution.
pkg/storage/internal/sqlstore/sql_helpers.go Updates rollback helper to operate on rebind-aware transactions.
pkg/storage/internal/sqlstore/identity.go Aligns identity insert execution interfaces with rebind-aware pool/tx handles.
pkg/storage/internal/sqlstore/applies.go Threads rebind-aware tx/conn through apply write + advisory-lock plumbing and uses raw() for namedlock boundary.
pkg/storage/internal/sqlstore/control_requests.go Updates control request “FOR UPDATE” helpers to use *rebindTx.
pkg/storage/internal/sqlstore/apply_operations.go Uses pinned raw conn specifically at the namedlock boundary for stranded reaper election.
pkg/storage/internal/sqlstore/db_test.go Adds tests proving rebinding happens exactly once for pool/tx/pinned-tx and is bypassed via raw().
pkg/storage/internal/sqlstore/apply_operations_test.go Updates namedlock test usage to go through the raw() escape.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +144 to +148
// Raw runs f against the pinned driver connection, for lifecycle control such
// as discarding a session whose advisory-lock state is uncertain.
func (c *rebindConn) Raw(f func(driverConn any) error) error {
return c.conn.Raw(f)
}
@Kiran01bm
Kiran01bm merged commit ed6dbce into main Aug 7, 2026
33 checks passed
@Kiran01bm
Kiran01bm deleted the kiran01bm/sqlstore-tx-conn-rebind branch August 7, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants