feat(tx): add WithTxValue for transactional return values

WithTxValue runs a callback inside a transaction and returns its result,
with rollback on error. Includes tests and documentation updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 18:25:08 -07:00
parent fbb9c74aab
commit 08f8387856
5 changed files with 70 additions and 2 deletions
+1
View File
@@ -14,6 +14,7 @@
- `StringKSUID.Equal` / `IsZero`, `BinaryKSUID.Equal` / `IsZero`, and nil sentinel vars for [ApplyInterfacePtr]. - `StringKSUID.Equal` / `IsZero`, `BinaryKSUID.Equal` / `IsZero`, and nil sentinel vars for [ApplyInterfacePtr].
- `Query`, `MustQuery`, and `UpdateOne` helpers (+ Context variants). - `Query`, `MustQuery`, and `UpdateOne` helpers (+ Context variants).
- `WithTxValue` for transactional functions that return a value.
- Context-aware variants for all query and mutation helpers. - Context-aware variants for all query and mutation helpers.
- `Delete`, `DeleteAffected`, `WithTx`, `ContainsCol`, and `CurrentDialect()`. - `Delete`, `DeleteAffected`, `WithTx`, `ContainsCol`, and `CurrentDialect()`.
- Package documentation (`doc.go`), expanded README, and subpackage docs for `dbxm` / `dbxp`. - Package documentation (`doc.go`), expanded README, and subpackage docs for `dbxm` / `dbxp`.
+1 -1
View File
@@ -44,7 +44,7 @@ For Postgres, use `dbx.DialectPostgres` and blank-import `dbxp` instead of `dbxm
|------|-----------| |------|-----------|
| Query | `Fetch`, `MustFetch`, `FetchOne`, `MustFetchOne`, `Query`, `MustQuery` (+ `*Context` variants) | | Query | `Fetch`, `MustFetch`, `FetchOne`, `MustFetchOne`, `Query`, `MustQuery` (+ `*Context` variants) |
| Mutations | `Insert`, `InsertReturning`, `Update`, `UpdateAffected`, `UpdateOne`, `UpdateReturning`, `Delete`, `DeleteAffected` (+ `*Context` variants) | | Mutations | `Insert`, `InsertReturning`, `Update`, `UpdateAffected`, `UpdateOne`, `UpdateReturning`, `Delete`, `DeleteAffected` (+ `*Context` variants) |
| Transactions | `WithTx` | | Transactions | `WithTx`, `WithTxValue` |
| Columns | `NormalCols`, `ContainsCol`, `ExprValues`, `ExprStringers` | | Columns | `NormalCols`, `ContainsCol`, `ExprValues`, `ExprStringers` |
| Partial update | `ApplyPtr`, `ApplyComplexPtr`, `ApplyInterfacePtr`, `ApplyVal` | | Partial update | `ApplyPtr`, `ApplyComplexPtr`, `ApplyInterfacePtr`, `ApplyVal` |
| Pointers | `Ptr`, `Val`, `NowPtr`, `TrimPtr`, `TrimPtrToNil`, `IsZero` | | Pointers | `Ptr`, `Val`, `NowPtr`, `TrimPtr`, `TrimPtrToNil`, `IsZero` |
+1 -1
View File
@@ -11,7 +11,7 @@
// [Fetch], [FetchOne], [Query], [Insert], [Update], [Delete], and their Must* // [Fetch], [FetchOne], [Query], [Insert], [Update], [Delete], and their Must*
// and Context variants wrap Jet [Statement] execution with consistent error // and Context variants wrap Jet [Statement] execution with consistent error
// semantics. [UpdateOne] requires at least one row affected. // semantics. [UpdateOne] requires at least one row affected.
// [WithTx] runs a function inside a SQL transaction. // [WithTx] and [WithTxValue] run functions inside SQL transactions.
// //
// # Jet column utilities // # Jet column utilities
// //
+27
View File
@@ -24,3 +24,30 @@ func WithTx(ctx context.Context, db QueryExecTx, fn func(tx *sql.Tx) error) erro
return tx.Commit() return tx.Commit()
} }
// WithTxValue begins a transaction on db, runs fn, and commits on success.
// Returns the value from fn or zero and an error if begin, fn, or commit fails.
// The transaction is rolled back if fn returns an error.
func WithTxValue[T any](ctx context.Context, db QueryExecTx, fn func(tx QueryExec) (T, error)) (T, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, err
}
tx, err := db.Begin()
if err != nil {
return zero, err
}
result, err := fn(tx)
if err != nil {
_ = tx.Rollback()
return zero, err
}
if err := tx.Commit(); err != nil {
return zero, err
}
return result, nil
}
+40
View File
@@ -0,0 +1,40 @@
package dbx
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWithTxValue_ReturnsResult(t *testing.T) {
db := openTestDB(t)
result, err := WithTxValue(context.Background(), db, func(tx QueryExec) (int, error) {
return 42, nil
})
require.NoError(t, err)
assert.Equal(t, 42, result)
}
func TestWithTxValue_RollbackOnError(t *testing.T) {
db := openTestDB(t)
fail := errors.New("fail")
_, err := WithTxValue(context.Background(), db, func(tx QueryExec) (int, error) {
return 0, fail
})
assert.ErrorIs(t, err, fail)
}
func TestWithTxValue_CancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := WithTxValue(ctx, openTestDB(t), func(tx QueryExec) (int, error) {
return 1, nil
})
assert.ErrorIs(t, err, context.Canceled)
}