Repository navigation
Expand file tree
/
Copy pathsqltransaction.go
More file actions
116 lines (90 loc) · 3.19 KB
/
Copy pathsqltransaction.go
File metadata and controls
116 lines (90 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
Package sqltransaction executes business logic inside a transaction with
begin/commit/rollback control flow and consistent error handling.
# How It Works
[Exec] and [ExecWithOptions] execute a caller-provided [ExecFunc] inside a
transaction:
1. Start transaction via [DB.BeginTx].
2. Run the provided function with `*sql.Tx`.
3. Commit on success.
4. Roll back automatically if the function fails or commit is not reached.
Rollback behavior is guarded to avoid noisy false failures:
- rollback is skipped after successful commit,
- `sql.ErrTxDone` during rollback is ignored,
- rollback failures are joined with the current error for full diagnostics.
# Usage
err := sqltransaction.Exec(ctx, db, func(ctx context.Context, tx *sql.Tx) error {
// Execute all related SQL operations using tx.
// Return an error to trigger rollback.
return nil
})
if err != nil {
return err
}
For a similar helper using github.com/jmoiron/sqlx instead of database/sql,
see:
github.com/tecnickcom/nurago/pkg/sqlxtransaction
# When To Use
- Several statements must succeed or fail together.
- You want rollback guaranteed on error and on panic, without repeating the
boilerplate.
*/
package sqltransaction
import (
"context"
"database/sql"
"errors"
"fmt"
)
var (
// ErrNilDB is returned when the provided DB is nil.
ErrNilDB = errors.New("db must not be nil")
// ErrNilExecFunc is returned when the provided ExecFunc is nil.
ErrNilExecFunc = errors.New("exec function must not be nil")
)
// ExecFunc is the type of the function to be executed inside a SQL Transaction.
type ExecFunc func(ctx context.Context, tx *sql.Tx) error
// DB defines the transaction entry point required by [Exec] and [ExecWithOptions].
type DB interface {
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
}
// Exec executes function inside SQL transaction with automatic rollback on error and guarded cleanup.
func Exec(ctx context.Context, db DB, run ExecFunc) error {
return ExecWithOptions(ctx, db, run, nil)
}
// ExecWithOptions executes function in SQL transaction with custom isolation level or read-only option; returns joined errors if commit/rollback both fail.
func ExecWithOptions(ctx context.Context, db DB, run ExecFunc, opts *sql.TxOptions) (err error) {
if db == nil {
return ErrNilDB
}
if run == nil {
return ErrNilExecFunc
}
// committed gates the deferred rollback. It is a flag rather than an
// `err != nil` check so the transaction is still rolled back when run panics:
// during a panic the named return is nil, so an error check would leak the tx.
var committed bool
tx, berr := db.BeginTx(ctx, opts)
if berr != nil {
return fmt.Errorf("unable to start SQL transaction: %w", berr)
}
defer func() {
if committed {
return
}
kerr := tx.Rollback()
if kerr != nil && !errors.Is(kerr, sql.ErrTxDone) {
err = errors.Join(err, fmt.Errorf("failed rolling back SQL transaction: %w", kerr))
}
}()
rerr := run(ctx, tx)
if rerr != nil {
return fmt.Errorf("failed executing a function inside SQL transaction: %w", rerr)
}
cerr := tx.Commit()
if cerr != nil {
return fmt.Errorf("unable to commit SQL transaction: %w", cerr)
}
committed = true
return nil
}