package database import ( "context" "database/sql" "fmt" ) // TxFn is a function that runs within a transaction. // If it returns an error, the transaction is rolled back. // If it returns nil, the transaction is committed. type TxFn func(tx *sql.Tx) error // WithTx executes a function within a database transaction. // The transaction is automatically committed on success or rolled back on error. // // Usage: // // err := database.WithTx(ctx, pool, func(tx *sql.Tx) error { // _, err := tx.ExecContext(ctx, "INSERT INTO users (name) VALUES ($1)", name) // if err != nil { // return err // } // _, err = tx.ExecContext(ctx, "INSERT INTO audit_log (action) VALUES ($1)", "user_created") // return err // }) func WithTx(ctx context.Context, pool *Pool, fn TxFn) error { tx, err := pool.DB.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer func() { if p := recover(); p != nil { _ = tx.Rollback() panic(p) // re-throw panic after rollback } }() if err := fn(tx); err != nil { if rbErr := tx.Rollback(); rbErr != nil { return fmt.Errorf("tx failed: %w, rollback failed: %v", err, rbErr) } return err } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // TxOptions configures transaction behavior. type TxOptions struct { // Isolation sets the transaction isolation level. // Default: sql.LevelDefault (database default) Isolation sql.IsolationLevel // ReadOnly marks the transaction as read-only. // Useful for reporting queries that should never modify data. ReadOnly bool } // WithTxOptions is like WithTx but with configurable transaction options. func WithTxOptions(ctx context.Context, pool *Pool, opts TxOptions, fn TxFn) error { tx, err := pool.DB.BeginTx(ctx, &sql.TxOptions{ Isolation: opts.Isolation, ReadOnly: opts.ReadOnly, }) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer func() { if p := recover(); p != nil { _ = tx.Rollback() panic(p) } }() if err := fn(tx); err != nil { if rbErr := tx.Rollback(); rbErr != nil { return fmt.Errorf("tx failed: %w, rollback failed: %v", err, rbErr) } return err } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // ReadOnlyTx executes a function in a read-only transaction. // This is useful for queries that should never accidentally modify data. func ReadOnlyTx(ctx context.Context, pool *Pool, fn TxFn) error { return WithTxOptions(ctx, pool, TxOptions{ReadOnly: true}, fn) } // SerializableTx executes a function in a serializable transaction. // Use this for operations that require the strongest isolation. // Note: May need retry logic for serialization failures. func SerializableTx(ctx context.Context, pool *Pool, fn TxFn) error { return WithTxOptions(ctx, pool, TxOptions{Isolation: sql.LevelSerializable}, fn) }