Skip to content

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

Merged
Kiran01bm merged 3 commits into
kiran01bm/storage-iface-decouplefrom
kiran01bm/sqlstore-rebind-wrapper
Aug 6, 2026
Merged

feat(storage): route direct pool execution through rebind-aware DB wrapper#949
Kiran01bm merged 3 commits into
kiran01bm/storage-iface-decouplefrom
kiran01bm/sqlstore-rebind-wrapper

Conversation

@Kiran01bm

Copy link
Copy Markdown
Collaborator

Summary

Routes all direct connection-pool execution in the shared sqlstore core through a rebind-aware DB wrapper. Every statement executed on the pool now passes through the dialect's placeholder binder exactly once before reaching the SQL driver — the identity transform for MySQL, and the seam a Postgres backend needs to rewrite ? placeholders to $n without touching store SQL.

What

  • New rebindDB wrapper around *sql.DB: ExecContext / QueryContext / QueryRowContext rebind the query via a binder interface; BeginTx / Conn / PingContext / Close are raw passthroughs.
  • MySQLDialect gains an identity Rebind.
  • All store structs (and Storage) hold *rebindDB instead of *sql.DB, so no store can bypass the rebind boundary. New(db *sql.DB) is unchanged; no SQL or behavior edits.
  • Unit tests back the wrapper with a recording driver.Connector stub, proving each direct execution method rebinds exactly once and forwards the rebound SQL and args, and that Tx/pinned-conn handles bypass the binder.

Why

The store core emits MySQL-style ? placeholders throughout. Postgres requires $n placeholders at the wire, and rewriting ~120 hand-written statements would be invasive and error-prone. Centralizing the rewrite at the execution boundary keeps store SQL dialect-neutral: a Postgres constructor only needs to supply a ?-to-$n binder. Transaction and pinned-connection statement rebinding is intentionally deferred to a follow-up to keep this change narrow.

Before / after

Before:
  store ──── query with "?" ────────────────────▶ *sql.DB ──▶ driver

After:
  store ──── query with "?" ──▶ rebindDB ──▶ binder.Rebind (MySQL: identity)
                                    │
                                    └───── rebound query ──▶ *sql.DB ──▶ driver

  BeginTx / Conn: raw passthrough handles (no rebinding yet)

…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.
@aparajon

aparajon commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

🤖 Adversarial correctness review, requested by Armand and performed by his agent. Reviewed at head 79dc872 (the PR's own diff on top of #948's package move).

Verdict: clean — no fix-before-merge findings. The seam is in the right place, the boundary is actually sealed (I went looking for bypasses and found none), and the identity transform provably changes nothing for MySQL.

Verified solid:

  • The "no store can bypass the rebind boundary" claim holds under inspection, not just by convention. After this change the only raw *sql.DB left in the package is the wrapper's own field/constructor and the New(db *sql.DB) entry point; every store struct holds *rebindDB, and nothing outside db.go touches .pool. Since rebindDB deliberately exposes no accessor for the underlying pool, a future store literally cannot execute pool SQL around the binder without changing db.go itself — the boundary is structural.
  • The identity.InsertID path routes through the seam too. queryExecer is satisfied by both *rebindDB (pool path — rebound) and *sql.Tx (tx path — raw, exactly matching the documented BeginTx/Conn passthrough), so the guarded-insert helpers get the same one-rebind guarantee as direct executions.
  • The raw-handle carve-out is coherent and honest. BeginTx/Conn return raw handles, the godoc says so, and the follow-up deferral is explicit in the PR body. The handles that matter today are MySQL-only anyway (the GET_LOCK pinned-conn path and the tx-based claim machinery), so nothing currently reachable executes un-rebindable SQL on a non-MySQL engine.
  • The driver-stub tests prove the right things at the right layer. The recording driver.Connector observes the exact SQL text handed to the driver, so the exactly-once rebind, the args forwarding, and — the one that usually goes untested — the negative case (tx and pinned-conn statements reach the driver with native placeholders and zero binder calls) are all pinned below database/sql, not by mocking the wrapper itself.
  • No behavior change for MySQL, proven end to end: the identity Rebind plus the full sqlstore integration suite running every store's real SQL through the wrapper against MySQL — green locally with -race, and CI is green 32/32.

One forward-looking note for the follow-up (nothing to change here): the binder contract says "pure string transformation", which is right, but the eventual ?$n Postgres binder must also skip ? inside string literals and comments — the store's plain-statement SQL style makes that tractable, and the parity suite from #935 is the natural place to pin it when it lands.

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 per the adversarial review above — clean, no blocking findings. Stamped by Armand's agent (claude-fable-5).

Base automatically changed from kiran01bm/sqlstore-core-move to kiran01bm/storage-iface-decouple August 6, 2026 20:33
@Kiran01bm
Kiran01bm marked this pull request as ready for review August 6, 2026 23:28
@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.

@Kiran01bm
Kiran01bm merged commit 54515e5 into kiran01bm/storage-iface-decouple Aug 6, 2026
1 check passed
@Kiran01bm
Kiran01bm deleted the kiran01bm/sqlstore-rebind-wrapper branch August 6, 2026 23:33
Kiran01bm added a commit that referenced this pull request Aug 7, 2026
…nce (#950)

* refactor(storage): depend on storage.Storage instead of *mysqlstore.Storage

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.

* refactor(storage): address review suggestions on storage interface decoupling

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

* 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.

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

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.

* refactor(storage): move mysqlstore internals to shared internal/sqlstore 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.

* feat(storage): route direct pool execution through rebind-aware DB wrapper (#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.
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.

2 participants