From b5141d6a709634a1153c0613ac099aba8c4fde0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 12 Jul 2025 13:57:27 -0700 Subject: [PATCH] Add database transaction helpers --- database/transaction.go | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 database/transaction.go 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) +}