Skip to content

feat(tx): introduce Tx interface for generic Open and extensible Client - #58

Open
cnlangzi wants to merge 4 commits into
mainfrom
fix/tx
Open

feat(tx): introduce Tx interface for generic Open and extensible Client#58
cnlangzi wants to merge 4 commits into
mainfrom
fix/tx

Conversation

@cnlangzi

@cnlangzi cnlangzi commented Jul 28, 2026

Copy link
Copy Markdown
Member

Mirrors the Database abstraction for *sql.Tx so custom database implementations can return custom transaction types from Begin/BeginTx.

Changes

  • New Tx interface in database.go (mirrors the Database subset used by sqle: Query/QueryContext/QueryRow/QueryRowContext/Exec/ExecContext/Prepare/PrepareContext/Commit/Rollback). *sql.Tx satisfies it by default.
  • Database.Begin/BeginTx now return the Tx interface instead of *sql.Tx. Custom database implementations can return any type satisfying the interface.
  • Tx struct renamed to TxContext so the new Tx interface can coexist. TxContext wraps a Tx interface and keeps the per-transaction prepared-statement cache.
  • sqlDBWrapper (unexported) adapts *sql.DB to the new Database signature.
  • Open(...any) accepts any mix of Database and *sql.DB (runtime detection auto-wraps *sql.DB).
  • OpenDB(...*sql.DB) added as a type-safe convenience for raw *sql.DB slices.
  • db.Add keeps the Database interface signature; callers wrap *sql.DB via &sqlDBWrapper{DB: db} if needed.

Tests

New file tx_interface_test.go:

  • TestCustomTxCommitHook — custom Tx's Commit fires through db.Begin/tx.Commit.
  • TestCustomTxRollbackHook — custom Tx's Rollback fires when Transaction returns an error.
  • TestCustomTxBeginReturnsInterfaceClient.BeginTx wraps the custom Tx in TxContext.
  • TestOpenDBBackwardCompatOpenDB(...*sql.DB) works.
  • TestOpenMixedArgsOpen accepts both *sql.DB and a Database wrapper in one call.

Backward compatibility

  • Open(*sql.DB) keeps working — Open detects *sql.DB and wraps it via sqlDBWrapper internally.
  • *sqle.Tx is renamed to *sqle.TxContext; this is a breaking change to the struct name. migrate/, dtc.go, and tx_test.go are updated to match.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce a Tx interface abstraction and update transaction handling and DB opening APIs to support custom transaction implementations while preserving *sql.DB compatibility.

New Features:

  • Add a Tx interface mirroring the subset of *sql.Tx used by the library so custom transaction types can be plugged in.
  • Add OpenDB(...*sql.DB) as a convenience constructor for working directly with standard library *sql.DB instances.

Enhancements:

  • Rename the concrete Tx wrapper to TxContext and have it embed the Tx interface while maintaining per-transaction prepared statement caching.
  • Change the Database interface so Begin/BeginTx return the new Tx interface, and adapt *sql.DB via an internal sqlDBWrapper type.
  • Refactor Open to accept a variadic list of mixed Database implementations and *sql.DB values with runtime adaptation, and adjust DB.Add to explicitly require Database implementations.

Documentation:

  • Update AGENTS.md and CHANGELOG entries to describe the new Tx interface, TxContext type, and the revised Open/OpenDB APIs.

Tests:

  • Add tx_interface_test.go to cover custom Tx implementations, OpenDB backward compatibility, and mixed-argument Open usage.
  • Update existing tests to use OpenDB where appropriate and to work with TxContext instead of the old Tx struct.

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce a new Tx interface abstraction for transactions, refactor the existing Tx struct into TxContext, and change DB opening APIs to support both *sql.DB and custom Database/Tx implementations while maintaining backward compatibility paths.

File-Level Changes

Change Details Files
Introduce Tx interface and adapt Database and *sql.DB usage to return it.
  • Change Database.Begin/BeginTx to return the Tx interface instead of *sql.Tx.
  • Define the Tx interface mirroring the subset of *sql.Tx methods used by the library (query, exec, prepare, commit, rollback).
  • Add sqlDBWrapper to adapt *sql.DB to Database by wrapping Begin/BeginTx to return Tx.
  • Update wrapperDB in tests to satisfy the new Database contract by implementing Begin/BeginTx returning Tx.
  • Add a compile-time assertion comment for *sql.Tx satisfying Tx (implicit via method set).
database.go
database_test.go
Rename the concrete Tx wrapper to TxContext and keep per-transaction statement caching behavior.
  • Rename Tx struct to TxContext and change its embedded field from *sql.Tx to the Tx interface.
  • Retain and adapt the per-transaction prepared-statement cache and helper methods (prepareStmt, closeStmts).
  • Update all Tx methods (Query/Exec and *Builder variants, Commit/Rollback) to have TxContext receivers.
  • Ensure Commit/Rollback still close cached statements before delegating to the underlying Tx implementation.
tx.go
Update Client and higher-level flows to use TxContext and the Tx interface.
  • Change Client.Begin/BeginTx to return *TxContext instead of *Tx and to wrap the Database.BeginTx result (which now returns Tx).
  • Update Client.Transaction signature to accept a callback with *TxContext and to use the new BeginTx.
  • Replace usages of *Tx with *TxContext in DTC sessions, migrator flows, and transaction tests.
  • Adjust AGENTS documentation to describe TxContext and the Tx interface roles and update type references.
client.go
dtc.go
migrate/migrator.go
tx_test.go
AGENTS.md
Refactor DB construction APIs to support both *sql.DB and Database values with explicit OpenDB convenience.
  • Change Open from a generic function to Open(dbArgs ...any) that accepts either Database or *sql.DB and panics on unsupported types.
  • Introduce openDatabases as an internal constructor shared by Open and OpenDB.
  • Add OpenDB(...*sql.DB) which wraps each *sql.DB in sqlDBWrapper and forwards to openDatabases.
  • Keep DB.Add(dbs ...Database) but document that callers must wrap *sql.DB (e.g., via sqlDBWrapper) before adding.
  • Update tests that previously relied on generic Open(dbs...) to call OpenDB or to wrap raw *sql.DB when using Add.
db.go
db_test.go
database_test.go
queryer_mapr_test.go
Add tests for custom Tx implementations and mixed Open/OpenDB usage, and document the changes in the changelog.
  • Introduce tx_interface_test.go covering custom Tx implementations, ensuring Commit/Rollback are invoked and that Client.BeginTx returns a TxContext wrapping the custom Tx.
  • Add tests ensuring OpenDB works as a backward-compatible helper for []*sql.DB and that Open supports mixed *sql.DB and Database arguments.
  • Update CHANGELOG with the new Tx interface, TxContext rename, and Open/OpenDB refactors.
tx_interface_test.go
db_test.go
database_test.go
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.51%. Comparing base (a9ec3f9) to head (3f4f1fb).

Files with missing lines Patch % Lines
db.go 83.33% 4 Missing ⚠️
database.go 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #58      +/-   ##
==========================================
+ Coverage   78.26%   78.51%   +0.24%     
==========================================
  Files          47       48       +1     
  Lines        1988     2015      +27     
==========================================
+ Hits         1556     1582      +26     
- Misses        310      312       +2     
+ Partials      122      121       -1     
Flag Coverage Δ
Unit-Tests 78.51% <87.50%> (+0.24%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 issues, and left some high level feedback:

  • Consider adding an explicit compile-time assertion like var _ Tx = (*sql.Tx)(nil) next to the Tx interface and sqlDBWrapper to ensure future changes to *sql.Tx keep satisfying the new abstraction.
  • Since Add now requires callers to provide Database implementations and cannot auto-wrap *sql.DB like Open, you might want to add a public helper (e.g., AddDB(...*sql.DB) or WrapSQLDB(*sql.DB) Database) to avoid pushing every consumer to write their own trivial Database adapter.
  • In tx_interface_test.go, the customTx type tracks commitCalls and rollbackCalls but the tests never assert on those counters, so either add assertions to ensure the hooks are exercised or remove the unused tracking fields to keep the test intent clear.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider adding an explicit compile-time assertion like `var _ Tx = (*sql.Tx)(nil)` next to the `Tx` interface and `sqlDBWrapper` to ensure future changes to `*sql.Tx` keep satisfying the new abstraction.
- Since `Add` now requires callers to provide `Database` implementations and cannot auto-wrap `*sql.DB` like `Open`, you might want to add a public helper (e.g., `AddDB(...*sql.DB)` or `WrapSQLDB(*sql.DB) Database`) to avoid pushing every consumer to write their own trivial `Database` adapter.
- In `tx_interface_test.go`, the `customTx` type tracks `commitCalls` and `rollbackCalls` but the tests never assert on those counters, so either add assertions to ensure the hooks are exercised or remove the unused tracking fields to keep the test intent clear.

## Individual Comments

### Comment 1
<location path="tx.go" line_range="8-13" />
<code_context>
-	*sql.Tx
-	noCopy //nolint
-	stmts  map[string]*sql.Stmt
+// TxContext wraps a Tx interface with a per-transaction prepared
+// statement cache. It is the concrete type returned by Client.BeginTx
+// and Client.Transaction so callers can use the cached prepared
+// statements via the Query/Exec/QueryBuilder/ExecBuilder methods.
+//
+// TxContext implements the Tx interface implicitly — its own Query/Exec
+// methods return sqle's *Rows/*Row (which add binding) and take
+// precedence over the embedded Tx interface methods that return the
</code_context>
<issue_to_address>
**issue:** TxContext does not implement Tx due to method signature differences on Query/Exec.

Because TxContext’s methods return *Rows instead of *sql.Rows, its method set does not match the Tx interface and it does not actually satisfy Tx, despite embedding it. You may want to either reword the comment to avoid claiming interface conformance, or change the method signatures (and possibly expose the sqle-specific methods under different names) if TxContext is intended to implement Tx directly.
</issue_to_address>

### Comment 2
<location path="tx_interface_test.go" line_range="52-61" />
<code_context>
+	return &customTx{Tx: tx}, nil
+}
+
+// TestCustomTxCommitHook proves that a custom Tx implementation
+// (here, customTx) reaches Commit/Rollback when used through
+// *sqle.TxContext.
+func TestCustomTxCommitHook(t *testing.T) {
+	raw := createSQLite3()
+	_, err := raw.Exec("CREATE TABLE `t` (`v` int)")
+	require.NoError(t, err)
+
+	wrapped := &customDB{DB: raw}
+	db := Open(wrapped)
+
+	tx, err := db.Begin(nil)
+	require.NoError(t, err)
+
+	_, err = tx.Exec("INSERT INTO `t` (`v`) VALUES (1)")
+	require.NoError(t, err)
+
+	require.NoError(t, tx.Commit())
+
+	// Pull the row back to verify the commit went through.
+	var v int
+	row := db.QueryRow("SELECT `v` FROM `t` WHERE `v` = 1")
+	require.NoError(t, row.Scan(&v))
+	require.Equal(t, 1, v)
+}
+
</code_context>
<issue_to_address>
**issue (testing):** The test does not assert that the customTx.Commit hook was actually invoked.

Right now this only verifies that the transaction commits and data is visible; it never checks `customTx.commitCalls`. As written, you could remove the `Commit` override and this test would still pass.

To verify the hook is actually invoked, assert on the underlying `customTx`, e.g.:

```go
ctx, err := db.Begin(nil)
require.NoError(t, err)

custom, ok := ctx.Tx.(*customTx)
require.True(t, ok)
require.Equal(t, int32(0), custom.commitCalls.Load())

_, err = ctx.Exec("INSERT INTO `t` (`v`) VALUES (1)")
require.NoError(t, err)

require.NoError(t, ctx.Commit())
require.Equal(t, int32(1), custom.commitCalls.Load())
```

This ensures the test fails if the custom `Commit` hook stops being exercised through `TxContext`.
</issue_to_address>

### Comment 3
<location path="tx_interface_test.go" line_range="78-87" />
<code_context>
+	require.Equal(t, 1, v)
+}
+
+// TestCustomTxRollbackHook proves that Rollback on the underlying
+// customTx is invoked when Transaction falls back to rollback.
+func TestCustomTxRollbackHook(t *testing.T) {
+	raw := createSQLite3()
+	_, err := raw.Exec("CREATE TABLE `t` (`v` int)")
+	require.NoError(t, err)
+
+	wrapped := &customDB{DB: raw}
+	db := Open(wrapped)
+
+	sentinel := errors.New("nope")
+	err = db.Transaction(context.Background(), nil, func(ctx context.Context, tx *TxContext) error {
+		_, err := tx.Exec("INSERT INTO `t` (`v`) VALUES (1)")
+		require.NoError(t, err)
+		return sentinel
+	})
+	require.ErrorIs(t, err, sentinel)
+
+	// The row should not be visible (transaction rolled back).
+	var count int
+	row := db.QueryRow("SELECT COUNT(*) FROM `t`")
+	require.NoError(t, row.Scan(&count))
+	require.Equal(t, 0, count)
+}
+
</code_context>
<issue_to_address>
**issue (testing):** Similarly, the rollback test does not verify that the customTx.Rollback override was used.

This verifies rollback behavior via the absence of rows, but not that the `customTx` wrapper’s `Rollback` was invoked. To ensure the hook is exercised, capture the underlying `customTx` in the transaction callback and assert `rollbackCalls` after the transaction:

```go
wrapped := &customDB{DB: raw}
db := Open(wrapped)

var underlying *customTx
err = db.Transaction(context.Background(), nil, func(ctx context.Context, tx *TxContext) error {
    custom, ok := tx.Tx.(*customTx)
    require.True(t, ok)
    underlying = custom

    _, err := tx.Exec("INSERT INTO `t` (`v`) VALUES (1)")
    require.NoError(t, err)
    return sentinel
})
require.ErrorIs(t, err, sentinel)
require.NotNil(t, underlying)
require.Equal(t, int32(1), underlying.rollbackCalls.Load())
```

This confirms `TxContext.Rollback` delegates to `customTx.Rollback`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tx.go Outdated
Comment thread tx_interface_test.go
Comment on lines +52 to +61
// TestCustomTxCommitHook proves that a custom Tx implementation
// (here, customTx) reaches Commit/Rollback when used through
// *sqle.TxContext.
func TestCustomTxCommitHook(t *testing.T) {
raw := createSQLite3()
_, err := raw.Exec("CREATE TABLE `t` (`v` int)")
require.NoError(t, err)

wrapped := &customDB{DB: raw}
db := Open(wrapped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (testing): The test does not assert that the customTx.Commit hook was actually invoked.

Right now this only verifies that the transaction commits and data is visible; it never checks customTx.commitCalls. As written, you could remove the Commit override and this test would still pass.

To verify the hook is actually invoked, assert on the underlying customTx, e.g.:

ctx, err := db.Begin(nil)
require.NoError(t, err)

custom, ok := ctx.Tx.(*customTx)
require.True(t, ok)
require.Equal(t, int32(0), custom.commitCalls.Load())

_, err = ctx.Exec("INSERT INTO `t` (`v`) VALUES (1)")
require.NoError(t, err)

require.NoError(t, ctx.Commit())
require.Equal(t, int32(1), custom.commitCalls.Load())

This ensures the test fails if the custom Commit hook stops being exercised through TxContext.

Comment thread tx_interface_test.go
Comment on lines +78 to +87
// TestCustomTxRollbackHook proves that Rollback on the underlying
// customTx is invoked when Transaction falls back to rollback.
func TestCustomTxRollbackHook(t *testing.T) {
raw := createSQLite3()
_, err := raw.Exec("CREATE TABLE `t` (`v` int)")
require.NoError(t, err)

wrapped := &customDB{DB: raw}
db := Open(wrapped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (testing): Similarly, the rollback test does not verify that the customTx.Rollback override was used.

This verifies rollback behavior via the absence of rows, but not that the customTx wrapper’s Rollback was invoked. To ensure the hook is exercised, capture the underlying customTx in the transaction callback and assert rollbackCalls after the transaction:

wrapped := &customDB{DB: raw}
db := Open(wrapped)

var underlying *customTx
err = db.Transaction(context.Background(), nil, func(ctx context.Context, tx *TxContext) error {
    custom, ok := tx.Tx.(*customTx)
    require.True(t, ok)
    underlying = custom

    _, err := tx.Exec("INSERT INTO `t` (`v`) VALUES (1)")
    require.NoError(t, err)
    return sentinel
})
require.ErrorIs(t, err, sentinel)
require.NotNil(t, underlying)
require.Equal(t, int32(1), underlying.rollbackCalls.Load())

This confirms TxContext.Rollback delegates to customTx.Rollback.

Mirror the Database abstraction for *sql.Tx so custom database
implementations can return custom transaction types from Begin/BeginTx.

- Add Tx interface in database.go (mirrors Database subset: Query/Exec/Prepare/Commit/Rollback).
- Database.Begin/BeginTx now return the Tx interface; *sql.Tx satisfies it by default.
- Rename the existing Tx struct to TxContext so the new Tx interface can coexist.
- TxContext wraps a Tx interface (was *sql.Tx) and keeps the per-transaction prepared-statement cache.
- Add sqlDBWrapper (unexported) to adapt *sql.DB to the new Database signature.
- Open(...any) accepts Database or *sql.DB; add OpenDB(...*sql.DB) as a type-safe convenience.
- Tests: TestCustomTxCommitHook, TestCustomTxRollbackHook, TestCustomTxBeginReturnsInterface, TestOpenDBBackwardCompat, TestOpenMixedArgs.

Co-Authored-By: Claude <noreply@anthropic.com>
cnlangzi and others added 3 commits July 28, 2026 22:23
- Reword TxContext doc: clarify it does NOT implement the Tx
  interface (Query/Exec return *Rows instead of *sql.Rows).
- TestCustomTxCommitHook now asserts the customTx.commitCalls
  counter so removing the Commit override would break the test.
- TestCustomTxRollbackHook captures the underlying *customTx in
  the transaction callback and asserts rollbackCalls after.
- Add public AddDB(*sql.DB) helper so callers don't have to wrap
  each *sql.DB with sqlDBWrapper manually when calling Add.
- Add public WrapSQLDB(*sql.DB) Database for ad-hoc conversion.
- Add compile-time assertion that *sqlDBWrapper satisfies Database
  alongside the existing *sql.Tx satisfies Tx assertion.

Co-Authored-By: Claude <noreply@anthropic.com>
The struct is sqle's wrapper that adds a per-transaction prepared
statement cache on top of a Tx interface. Renaming to RawTx better
conveys the role (a thin wrapper over the underlying transaction)
and matches the user's preferred mental model where:
- Tx (interface) is the public abstraction
- RawTx (struct) is the internal-ish wrapper returned from
  Client.BeginTx / Client.Transaction

Also updates AGENTS.md, CHANGELOG.md, dtc.go, client.go,
migrate/migrator.go, tx_test.go, and tx_interface_test.go to match.

Co-Authored-By: Claude <noreply@anthropic.com>
The struct is sqle's wrapper that adds a per-transaction prepared
statement cache on top of a Tx interface. Renaming to Transaction
better conveys the role (a concrete transactional object exposed to
callers) and matches the user's preferred mental model where:
- Tx (interface) is the public abstraction
- Transaction (struct) is the wrapper returned from
  Client.BeginTx / Client.Transaction

This also makes the public API self-documenting: `*sqle.Transaction`
reads naturally in user code, while still leaving the `Tx` interface
free for the abstraction layer.

Also updates AGENTS.md, CHANGELOG.md, dtc.go, client.go,
migrate/migrator.go, tx_test.go, and tx_interface_test.go to match.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant