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 } err = tx.Commit(ctx) committed = true // Mark as committed regardless of commit success/failure if err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } 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) }