diff --git a/database/transaction.go b/database/transaction.go new file mode 100644 index 0000000..45743a1 --- /dev/null +++ b/database/transaction.go @@ -0,0 +1,68 @@ +package database + +import ( + "context" + "fmt" + + "go.ntppool.org/common/logger" +) + +// DB interface for database operations that can begin transactions +type DB[Q any] interface { + Begin(ctx context.Context) (Q, error) +} + +// TX interface for transaction operations +type TX interface { + Commit(ctx context.Context) error + Rollback(ctx context.Context) error +} + +// WithTransaction executes a function within a database transaction +// Handles proper rollback on error and commit on success +func WithTransaction[Q TX](ctx context.Context, db DB[Q], fn func(ctx context.Context, q Q) error) error { + tx, err := db.Begin(ctx) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + + var committed bool + defer func() { + if !committed { + if rbErr := tx.Rollback(ctx); rbErr != nil { + // Log rollback error but don't override original error + log := logger.FromContext(ctx) + log.ErrorContext(ctx, "failed to rollback transaction", "error", rbErr) + } + } + }() + + if err := fn(ctx, tx); err != nil { + return err + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + committed = true + return nil +} + +// WithReadOnlyTransaction executes a read-only function within a transaction +// Always rolls back at the end (for consistent read isolation) +func WithReadOnlyTransaction[Q TX](ctx context.Context, db DB[Q], fn func(ctx context.Context, q Q) error) error { + tx, err := db.Begin(ctx) + if err != nil { + return fmt.Errorf("failed to begin read-only transaction: %w", err) + } + + defer func() { + if rbErr := tx.Rollback(ctx); rbErr != nil { + log := logger.FromContext(ctx) + log.ErrorContext(ctx, "failed to rollback read-only transaction", "error", rbErr) + } + }() + + return fn(ctx, tx) +}