Repository navigation
Expand file tree
/
Copy pathsqlxtransaction.go
More file actions
118 lines (91 loc) · 3.26 KB
/
Copy pathsqlxtransaction.go
File metadata and controls
118 lines (91 loc) · 3.26 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
117
118
/*
Package sqlxtransaction handles begin/commit/rollback control flow around
business logic executed inside a sqlx transaction.
# How It Works
[Exec] and [ExecWithOptions] accept a caller-provided [ExecFunc] and run it
inside a sqlx transaction:
1. Start a transaction with [DB.BeginTxx].
2. Execute the provided function with the transaction object.
3. Commit if execution succeeds.
4. Roll back automatically if execution fails or commit is not reached.
Rollback behavior is deferred and guarded:
- rollback is skipped after a successful commit,
- `sql.ErrTxDone` is ignored during deferred rollback,
- rollback failures are joined with the current error so diagnostics are not
lost.
# Usage
err := sqlxtransaction.Exec(ctx, db, func(ctx context.Context, tx *sqlx.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 based on the standard database/sql package (instead of
github.com/jmoiron/sqlx), see:
github.com/tecnickcom/nurago/pkg/sqltransaction
# When To Use
- You use sqlx and want struct scanning inside a transaction.
- Rollback must be guaranteed on error and on panic.
*/
package sqlxtransaction
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/jmoiron/sqlx"
)
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 *sqlx.Tx) error
// DB defines the transaction entry point required by [Exec] and [ExecWithOptions].
type DB interface {
BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error)
}
// Exec executes function inside sqlx 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 sqlx 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.BeginTxx(ctx, opts)
if berr != nil {
return fmt.Errorf("unable to start SQLX 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 SQLX transaction: %w", kerr))
}
}()
rerr := run(ctx, tx)
if rerr != nil {
return fmt.Errorf("failed executing a function inside SQLX transaction: %w", rerr)
}
cerr := tx.Commit()
if cerr != nil {
return fmt.Errorf("unable to commit SQLX transaction: %w", cerr)
}
committed = true
return nil
}