Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func NewDatabaseContext(parent context.Context, db Database) Context {
//
// Parameters:
// - ctx: The context to search for an existing dbx Context
// - creator: Either a ContextCreator, Database, or any type with Context method to use if no existing Context is found
// - creator: Either a ContextCreator, Database, or Transactor
//
// Returns:
// - Context: Either the existing dbx Context or a newly created one
Expand All @@ -133,30 +133,24 @@ func NewDatabaseContext(parent context.Context, db Database) Context {
// // This will reuse existing dbx Context or create new one
// dbCtx := dbx.NewContextFrom(ctx, database)
// executor := dbCtx.Executor()
func NewContextFrom(ctx context.Context, creator interface{}) Context {
func NewContextFrom(ctx context.Context, input any) Context {
found := FromContext(ctx)

if found != nil {
return found
}

// Try ContextCreator interface first
if cc, ok := creator.(ContextCreator); ok {
return cc.Context(ctx)
switch val := input.(type) {
case ContextCreator:
return val.Context(ctx)
case Database:
return NewDatabaseContext(ctx, val)
case Transactor:
return NewContext(ctx, val)
default:
// If none work, panic with helpful message
panic("input must implement ContextCreator, Database, or Transactor")
}

// Try Database interface
if db, ok := creator.(Database); ok {
return NewDatabaseContext(ctx, db)
}

// Try any type with Context method (for backward compatibility)
if contextProvider, ok := creator.(interface{ Context(context.Context) Context }); ok {
return contextProvider.Context(ctx)
}

// If none work, panic with helpful message
panic("creator must implement ContextCreator, Database, or have Context(context.Context) Context method")
}

// FromContext extracts a dbx Context from the provided Go context.
Expand Down
92 changes: 40 additions & 52 deletions transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,101 +8,89 @@ import (
// and handles commit or rollback automatically. If the context already contains
// a transaction, it will be reused unless the WithNewTransaction option is specified.
//
// The transaction lifecycle is managed automatically:
// - If a new transaction is created, it will be committed on success or rolled back on error
// - If an existing transaction is reused, commit/rollback is left to the outer transaction
// - Any panic during operation execution will trigger a rollback if a new transaction was created
// Transaction lifecycle management:
// - If a new transaction is created, it's committed on successful operation or rolled back on error.
// - If an existing transaction is reused, commit/rollback is left to the outer transaction.
// - Any panic during operation execution triggers rollback if a new transaction was created.
//
// Parameters:
// - ctx: The parent Go context
// - db: Database instance to create transaction from (if needed)
// - op: Operation to execute within the transaction
// - opts: Optional transaction configuration (isolation level, read-only, etc.)
// - ctx: Parent Go context.
// - beginner: A Beginner capable of creating transactions (typically a Database).
// - op: Operation to execute within the transaction, taking a dbx.Context.
// - opts: Optional configuration (e.g., isolation, read-only, always create new transaction).
//
// Returns:
// - error: Any error from transaction creation, operation execution, or commit/rollback
// - error: Any error from transaction creation, operation execution, or commit/rollback.
//
// Example:
//
// err := dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
// _, err := txCtx.Executor().Exec("INSERT INTO users (name) VALUES (?)", "John")
// if err != nil {
// return err // This will trigger automatic rollback
// }
// if err != nil { return err } // triggers automatic rollback
// _, err = txCtx.Executor().Exec("INSERT INTO profiles (user_id) VALUES (?)", userID)
// return err
// })
func Transaction(ctx context.Context, db Database, op Operation, opts ...Option) error {
_, err := transactionWithInternal(ctx, db, func(ctx Context) (interface{}, error) {
func Transaction(ctx context.Context, beginner Beginner, op Operation, opts ...Option) error {
_, err := transactionWithInternal(ctx, beginner, func(ctx Context) (interface{}, error) {
return nil, op(ctx)
}, opts)

return err
}

// TransactionWithResult begins a transaction and executes an operation that returns a typed result.
// Like Transaction, it handles automatic commit/rollback and transaction reuse, but allows
// the operation to return a value along with any error.
//
// The transaction lifecycle follows the same rules as Transaction:
// - New transactions are committed on success or rolled back on error
// - Existing transactions are reused and their lifecycle managed by the outer scope
// TransactionWithResult begins a transaction and executes an operation returning a typed result.
// Handles automatic commit/rollback and transaction reuse (see Transaction for rules).
//
// Parameters:
// - ctx: The parent Go context
// - db: Database instance to create transaction from (if needed)
// - op: Operation to execute that returns a typed result
// - setters: Optional transaction configuration options
// - ctx: Parent Go context.
// - beginner: A Beginner capable of creating transactions (typically a Database).
// - op: Operation to execute within the transaction that returns (T, error).
// - setters: Optional configuration (transaction isolation, read-only, always create, etc.).
//
// Returns:
// - T: The result returned by the operation (zero value if error occurred)
// - error: Any error from transaction creation, operation execution, or commit/rollback
// - T: Result returned by the operation (zero value if error).
// - error: Any error from transaction creation, operation execution, or commit/rollback.
//
// Example:
//
// userID, err := dbx.TransactionWithResult(ctx, db, func(txCtx dbx.Context) (int64, error) {
// result, err := txCtx.Executor().Exec("INSERT INTO users (name) VALUES (?)", "John")
// if err != nil {
// return 0, err
// }
// if err != nil { return 0, err }
// return result.LastInsertId()
// })
func TransactionWithResult[T any](ctx context.Context, db Database, op OperationWithResult[T], setters ...Option) (T, error) {
return transactionWithInternal(ctx, db, op, setters)
func TransactionWithResult[T any](ctx context.Context, beginner Beginner, op OperationWithResult[T], setters ...Option) (T, error) {
return transactionWithInternal(ctx, beginner, op, setters)
}

// transactionWithInternal implements the core transaction logic used by both
// Transaction and TransactionWithResult functions. It handles transaction creation,
// reuse detection, operation execution, and automatic commit/rollback.
// transactionWithInternal contains core transaction logic for Transaction and TransactionWithResult.
//
// Transaction Reuse Logic:
// - If opts.AlwaysCreate is false (default), checks for existing transaction in context
// - If existing transaction found, reuses it and delegates lifecycle management to outer scope
// - If no existing transaction or AlwaysCreate is true, creates a new transaction
// Transaction reuse/creation:
// - By default, attempts to detect and reuse an existing transaction in context.
// - If WithNewTransaction is specified or no transaction exists, creates a new one.
//
// Error Handling:
// - If operation returns an error and a new transaction was created, automatically rolls back
// - If operation succeeds and a new transaction was created, automatically commits
// - If reusing existing transaction, no automatic commit/rollback occurs
// Error handling and lifecycle:
// - Rolls back on error or panic if a new transaction was created.
// - Commits on success if a new transaction was created.
// - Existing transactions are reused with lifecycle managed by caller.
//
// Parameters:
// - ctx: Parent Go context
// - db: Database instance for creating new transactions
// - op: Operation to execute within transaction scope
// - setters: Transaction configuration options
// - ctx: Parent Go context.
// - beginner: Capable of creating a new transaction.
// - op: Operation to execute, returns (T, error).
// - setters: List of functional options for transaction configuration.
//
// Returns:
// - T: Result from the operation (zero value if error occurred)
// - error: Any error from transaction management or operation execution
func transactionWithInternal[T any](ctx context.Context, db Database, op OperationWithResult[T], setters []Option) (T, error) {
// - T: Operation result (zero value if error).
// - error: Any error from transaction handling or op execution.
func transactionWithInternal[T any](ctx context.Context, beginner Beginner, op OperationWithResult[T], setters []Option) (T, error) {
var tx Transactor
var createdTx bool
var dbCtx Context
opts := newOptions(setters)

if !opts.AlwaysCreate {
// retrieve existing or create a new context
dbCtx = NewContextFrom(ctx, db)
dbCtx = NewContextFrom(ctx, beginner)
executor := dbCtx.Executor()

// check if the executor is a transaction
Expand All @@ -119,7 +107,7 @@ func transactionWithInternal[T any](ctx context.Context, db Database, op Operati
createdTx = true

// create a new transaction
tx, err = db.BeginTx(ctx, opts.TxOptions)
tx, err = beginner.BeginTx(ctx, opts.TxOptions)

if err != nil {
return *new(T), err
Expand Down
Loading