dbx

package module
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 6 Imported by: 0

README

dbx

A lightweight, context-aware abstraction layer for Go's database/sql package that simplifies database operations and transaction management.

Go Reference

Table of Contents

Why dbx?

The standard database/sql package is powerful but requires boilerplate code for common patterns. dbx addresses several pain points:

  • Context Management: Eliminates the need to pass both context.Context and database connections separately
  • Transaction Handling: Automatic transaction lifecycle management with transaction reuse for nested operations
  • Unified Interface: Same API for both direct database operations and transactions
  • Testing: Easier to mock and test database operations
  • Clean Architecture: Promotes separation of concerns between business logic and data access

Design Philosophy

dbx follows these core principles:

  1. Context-Driven: Database connections and transactions are embedded within Go contexts
  2. Interface-Based: Uses interfaces for maximum flexibility and testability
  3. Zero Magic: Predictable behavior with no hidden surprises
  4. Minimal Overhead: Thin layer that doesn't compromise performance
  5. Standard Library Compatible: Works seamlessly with existing database/sql code

Installation

go get github.com/ziflex/dbx@latest

Key Concepts

Database Interfaces

The Database interface provides connection management, transaction creation, and query execution. Context creation is exposed separately by DatabaseWithContext:

type Database interface {
    io.Closer
    Beginner // Begins transactions
    Executor // Executes queries directly
}

type DatabaseWithContext interface {
    Database
    ContextCreator // Creates dbx.Context
}
Context Interface

The Context interface extends Go's context.Context with database execution capabilities:

type Context interface {
    context.Context
    Executor() Executor  // Returns sql.DB or sql.Tx depending on transaction state
}
Executor Interface

The Executor interface abstracts both *sql.DB and *sql.Tx operations:

type Executor interface {
    Exec(query string, args ...interface{}) (sql.Result, error)
    Query(query string, args ...interface{}) (*sql.Rows, error)
    QueryRow(query string, args ...interface{}) *sql.Row
    // ... context variants
}

This design allows your functions to work with both direct database connections and transactions without modification.

Quick Start

Here's a complete example showing basic database operations:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
    "github.com/ziflex/dbx"
)

// User represents a user record
type User struct {
    ID   int
    Name string
}

// getUserNames demonstrates querying with dbx.Context
func getUserNames(ctx dbx.Context) ([]User, error) {
    executor := ctx.Executor()
    
    rows, err := executor.QueryContext(ctx, "SELECT id, name FROM users ORDER BY name")
    if err != nil {
        return nil, fmt.Errorf("failed to query users: %w", err)
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name); err != nil {
            return nil, fmt.Errorf("failed to scan user: %w", err)
        }
        users = append(users, user)
    }
    
    return users, rows.Err()
}

func main() {
    // Connect to database
    db, err := sql.Open("postgres", "postgres://user:password@localhost/dbname?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Wrap with dbx
    dbxDB := dbx.New(db)
    
    // Create dbx context and query
    users, err := getUserNames(dbxDB.Context(context.Background()))
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Found %d users\n", len(users))
    for _, user := range users {
        fmt.Printf("- %s (ID: %d)\n", user.Name, user.ID)
    }
}
Key Benefits Demonstrated:
  • Single Parameter: Functions only need dbx.Context instead of separate context and database parameters
  • Consistent Interface: Same API works for both direct DB operations and transactions
  • Better Error Handling: Proper error wrapping and handling patterns

Working with Contexts

dbx provides multiple ways to work with contexts, allowing flexibility in your application architecture.

Direct Context Creation

Create a dbx context directly from a Database:

func directExample() {
    db := dbx.New(sqlDB)
    ctx := db.Context(context.Background())
    
    // Use ctx for database operations
    result, err := getUserCount(ctx)
}

func getUserCount(ctx dbx.Context) (int, error) {
    var count int
    err := ctx.Executor().QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&count)
    return count, err
}
Context Extraction Pattern

Extract dbx context from standard Go context for cleaner service layers:

func serviceLayerExample(ctx context.Context) {
    // Extract dbx context from regular context
    dbxCtx := dbx.FromContext(ctx)
    if dbxCtx == nil {
        log.Fatal("database context not found")
    }
    
    users, err := getUserNames(dbxCtx)
    // ... handle results
}

func main() {
    db := dbx.New(sqlDB)
    ctx := context.Background()
    
    // Embed dbx context into regular context
    ctx = dbx.WithContext(ctx, db.Context(ctx))
    
    serviceLayerExample(ctx)
}
Context Helper Functions
  • dbx.Is(ctx) - Check whether the context directly implements dbx.Context
  • dbx.As(ctx) - Type-assert a direct dbx.Context with an ok flag
  • dbx.FromContext(ctx) - Extract a direct or embedded dbx context (returns nil if not found)
  • dbx.WithContext(ctx, dbxCtx) - Embed dbx context into regular context

Transaction Management

dbx provides transaction management with automatic lifecycle handling and transaction reuse for nested operations.

Basic Transactions
func createUserWithProfile(ctx context.Context, db dbx.Database, userName, email string) error {
    return dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
        // Insert user
        var userID int64
        err := txCtx.Executor().QueryRowContext(
            txCtx,
            "INSERT INTO users (name) VALUES ($1) RETURNING id",
            userName,
        ).Scan(&userID)
        if err != nil {
            return fmt.Errorf("failed to get user ID: %w", err)
        }
        
        // Insert profile
        _, err = txCtx.Executor().ExecContext(txCtx,
            "INSERT INTO profiles (user_id, email) VALUES ($1, $2)", userID, email)
        if err != nil {
            return fmt.Errorf("failed to insert profile: %w", err)
        }
        
        return nil
    })
}
Transaction Reuse (Default Behavior)

By default, dbx.Transaction reuses existing transactions. This prevents unnecessary nesting:

func processOrder(ctx dbx.Context, orderID int) error {
    // This function works both in and outside transactions
    return dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
        if err := updateInventory(txCtx, orderID); err != nil {
            return err
        }
        
        return updateOrderStatus(txCtx, orderID, "processed")
    })
}

func updateInventory(ctx dbx.Context, orderID int) error {
    // This also uses Transaction, but will reuse the existing one
    return dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
        // Inventory updates here
        return nil
    })
}
Transactions with Return Values

Use TransactionWithResult when you need to return values from transactions:

func createUserAndGetID(ctx context.Context, db dbx.Database, name string) (int64, error) {
    return dbx.TransactionWithResult(ctx, db, func(txCtx dbx.Context) (int64, error) {
        var userID int64
        err := txCtx.Executor().QueryRowContext(
            txCtx,
            "INSERT INTO users (name) VALUES ($1) RETURNING id",
            name,
        ).Scan(&userID)
        return userID, err
    })
}

Advanced Usage

Transaction Options

Control transaction behavior with options. Isolation and read-only options apply only when dbx creates a transaction; a reused transaction retains the options selected by its owner:

// Read-only transaction
err := dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
    // Only SELECT operations allowed
    return generateReport(txCtx)
}, dbx.WithReadOnly(true))

// Custom isolation level
err := dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
    return performCriticalOperation(txCtx)
}, dbx.WithIsolationLevel(sql.LevelSerializable))

// Force an independent transaction (disable reuse; this is not a savepoint)
err := dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
    return independentOperation(txCtx)
}, dbx.WithNewTransaction())

An independent transaction may use another pooled connection and commits separately from the outer transaction. WithNewTransaction does not create a database savepoint.

Error Handling Patterns

dbx automatically handles transaction rollback on errors and panics. An operation error is returned unchanged when rollback succeeds; if rollback also fails, the returned error contains both failures:

func transferFunds(ctx context.Context, db dbx.Database, fromID, toID int, amount decimal.Decimal) error {
    return dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
        // Debit source account
        result, err := txCtx.Executor().ExecContext(txCtx,
            "UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1", 
            amount, fromID)
        if err != nil {
            return fmt.Errorf("failed to debit account %d: %w", fromID, err)
        }
        
        rowsAffected, err := result.RowsAffected()
        if err != nil {
            return fmt.Errorf("failed to check debit result: %w", err)
        }
        if rowsAffected == 0 {
            return fmt.Errorf("insufficient funds in account %d", fromID)
        }
        
        // Credit destination account
        _, err = txCtx.Executor().ExecContext(txCtx,
            "UPDATE accounts SET balance = balance + $1 WHERE id = $2", 
            amount, toID)
        if err != nil {
            return fmt.Errorf("failed to credit account %d: %w", toID, err)
        }
        
        // Any error here will automatically rollback the entire transaction
        return nil
    })
}
Working with Prepared Statements

Executor intentionally exposes only the query methods common to sql.DB and sql.Tx. Both standard implementations also support prepared statements, which can be accessed through a narrow local capability interface without expanding dbx.Executor:

type statementPreparer interface {
    PrepareContext(context.Context, string) (*sql.Stmt, error)
}

func batchInsertUsers(ctx dbx.Context, users []User) error {
    preparer, ok := ctx.Executor().(statementPreparer)
    if !ok {
        return fmt.Errorf("executor does not support prepared statements")
    }

    stmt, err := preparer.PrepareContext(
        ctx,
        "INSERT INTO users (name, email) VALUES ($1, $2)",
    )
    if err != nil {
        return err
    }
    defer stmt.Close()
    
    for _, user := range users {
        if _, err := stmt.ExecContext(ctx, user.Name, user.Email); err != nil {
            return fmt.Errorf("failed to insert user %s: %w", user.Name, err)
        }
    }
    
    return nil
}

Testing

dbx works seamlessly with testing frameworks and mocking libraries:

Using go-sqlmock
func TestGetUserNames(t *testing.T) {
    // Create mock database
    mockDB, mock, err := sqlmock.New()
    require.NoError(t, err)
    defer mockDB.Close()
    
    // Setup expectations
    rows := sqlmock.NewRows([]string{"id", "name"}).
        AddRow(1, "Alice").
        AddRow(2, "Bob")
    mock.ExpectQuery("SELECT id, name FROM users").WillReturnRows(rows)
    
    // Test with dbx
    db := dbx.New(mockDB)
    users, err := getUserNames(db.Context(context.Background()))
    
    require.NoError(t, err)
    assert.Len(t, users, 2)
    assert.Equal(t, "Alice", users[0].Name)
    assert.Equal(t, "Bob", users[1].Name)
    
    // Verify all expectations met
    assert.NoError(t, mock.ExpectationsWereMet())
}
Testing Transactions
func TestTransferFunds(t *testing.T) {
    mockDB, mock, err := sqlmock.New()
    require.NoError(t, err)
    defer mockDB.Close()
    
    // Setup transaction expectations
    mock.ExpectBegin()
    mock.ExpectExec("UPDATE accounts SET balance").
        WithArgs(sqlmock.AnyArg(), 1).
        WillReturnResult(sqlmock.NewResult(0, 1))
    mock.ExpectExec("UPDATE accounts SET balance").
        WithArgs(sqlmock.AnyArg(), 2).
        WillReturnResult(sqlmock.NewResult(0, 1))
    mock.ExpectCommit()
    
    db := dbx.New(mockDB)
    err = transferFunds(context.Background(), db, 1, 2, decimal.NewFromInt(100))
    
    require.NoError(t, err)
    assert.NoError(t, mock.ExpectationsWereMet())
}

API Reference

Core Functions
  • dbx.New(db *sql.DB) DatabaseWithContext - Creates a new dbx database wrapper with context creation
  • dbx.Transaction(ctx context.Context, beginner Beginner, op Operation, opts ...Option) error - Executes an operation in a transaction
  • dbx.TransactionWithResult[T](ctx context.Context, beginner Beginner, op OperationWithResult[T], opts ...Option) (T, error) - Executes a transaction and returns a typed result
Context Functions
  • dbx.FromContext(ctx context.Context) Context - Extract dbx context from context
  • dbx.WithContext(ctx context.Context, dbxCtx Context) context.Context - Embed dbx context
  • dbx.Is(ctx context.Context) bool - Check whether the context directly implements dbx.Context
  • dbx.As(ctx context.Context) (Context, bool) - Type-assert a direct dbx.Context
Transaction Options
  • dbx.WithIsolationLevel(level sql.IsolationLevel) - Set transaction isolation level
  • dbx.WithReadOnly(readOnly bool) - Set read-only flag
  • dbx.WithNewTransaction() - Force creation of an independent transaction (disable reuse)

For complete API documentation, see Go Reference.

Documentation

Overview

Package dbx provides a context-driven database abstraction layer for Go.

It wraps the standard database/sql package to provide better context management, automatic transaction handling, and a unified interface for database operations. The package follows Go best practices for context propagation and makes it easier to work with transactions while maintaining compatibility with existing database/sql code.

Key Features:

  • Context-driven design that embeds database connections within Go contexts
  • Automatic transaction lifecycle management with reuse for nested operations
  • Unified interface that works the same for both direct DB operations and transactions
  • Interface-based design for maximum flexibility and testability
  • Zero magic - predictable behavior with no hidden surprises
  • Minimal overhead - thin layer that doesn't compromise performance

Basic Usage:

db, err := sql.Open("postgres", connectionString)
if err != nil {
    return err
}
// Create a dbx database instance
dbxDB := dbx.New(db)
defer dbxDB.Close()

// Use context for database operations
ctx := context.Background()
dbCtx := dbxDB.Context(ctx)

// Execute queries
rows, err := dbCtx.Executor().QueryContext(dbCtx, "SELECT * FROM users")

Transaction Example:

err := dbx.Transaction(ctx, dbxDB, func(txCtx dbx.Context) error {
    _, err := txCtx.Executor().ExecContext(txCtx, "INSERT INTO users (name) VALUES (?)", "John")
    return err
})

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Is

func Is(ctx context.Context) bool

Is returns true if the provided context is a dbx Context. This function performs a type assertion to check if the context implements the dbx Context interface.

Parameters:

  • ctx: The context to check

Returns:

  • bool: true if ctx is a dbx Context, false otherwise

Example:

if dbx.Is(ctx) {
    // Safe to use dbx-specific operations
    dbCtx, _ := dbx.As(ctx)
    executor := dbCtx.Executor()
}

func Transaction added in v1.1.0

func Transaction(ctx context.Context, beginner Beginner, op Operation, opts ...Option) error

Transaction begins or reuses a transaction, executes the provided operation, and handles commit or rollback automatically. If the context already contains a transaction, it will be reused unless the WithNewTransaction option is specified.

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.
  • If an operation and its rollback both fail, the returned error contains both failures.
  • Transaction options apply only when a new transaction is created.

Parameters:

  • 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.

Example:

err := dbx.Transaction(ctx, db, func(txCtx dbx.Context) error {
    _, err := txCtx.Executor().ExecContext(txCtx, "INSERT INTO users (name) VALUES (?)", "John")
    if err != nil { return err } // triggers automatic rollback
    _, err = txCtx.Executor().ExecContext(txCtx, "INSERT INTO profiles (user_id) VALUES (?)", userID)
    return err
})

func TransactionWithResult added in v1.5.0

func TransactionWithResult[T any](ctx context.Context, beginner Beginner, op OperationWithResult[T], setters ...Option) (T, error)

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: 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: 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) {
    var userID int64
    err := txCtx.Executor().QueryRowContext(
        txCtx,
        "INSERT INTO users (name) VALUES (?) RETURNING id",
        "John",
    ).Scan(&userID)
    return userID, err
})

func WithContext added in v1.4.0

func WithContext(ctx context.Context, dbCtx Context) context.Context

WithContext embeds a dbx Context into a Go context as a value. This allows the dbx Context to be retrieved later using FromContext. This is useful when you need to pass a dbx Context through code that expects a standard Go context.

Parameters:

  • ctx: The parent Go context
  • dbCtx: The dbx Context to embed

Returns:

  • context.Context: A new context containing the embedded dbx Context

Example:

dbCtx := database.Context(ctx)
embeddedCtx := dbx.WithContext(context.Background(), dbCtx)
// Later: extractedCtx := dbx.FromContext(embeddedCtx)

Types

type Beginner added in v1.0.1

type Beginner interface {
	// Begin starts a transaction with default options. The default isolation level
	// is dependent on the driver.
	Begin() (*sql.Tx, error)

	// BeginTx starts a transaction with the provided context and options.
	// The provided context is used until the transaction is committed or rolled back.
	// If the context is canceled, the sql package will roll back the transaction.
	BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error)
}

Beginner provides an abstraction for sql.DB's transaction creation methods. This interface allows starting new transactions with different options and isolation levels.

type Context

type Context interface {
	// Embed standard Go context for deadline, cancellation, and value propagation
	context.Context

	// Executor returns the appropriate sql executor for this context.
	// Returns sql.Tx if this context was created within a transaction,
	// otherwise returns sql.DB for direct database operations.
	Executor() Executor
}

Context provides a context-aware abstraction for database operations. It extends the standard Go context.Context while embedding a database executor, allowing seamless propagation of both context information and database connection/transaction state.

Context instances automatically handle the appropriate executor (sql.DB or sql.Tx) based on whether they were created from a direct database connection or within a transaction scope.

func As

func As(ctx context.Context) (Context, bool)

As attempts to extract a dbx Context from the provided context. This function performs a type assertion and returns both the Context and a boolean indicating success.

Parameters:

  • ctx: The context to extract dbx Context from

Returns:

  • Context: The extracted dbx Context (nil if extraction failed)
  • bool: true if extraction was successful, false otherwise

Example:

if dbCtx, ok := dbx.As(ctx); ok {
    executor := dbCtx.Executor()
    // Use executor for database operations
}

func FromContext added in v1.4.0

func FromContext(ctx context.Context) Context

FromContext extracts a dbx Context from the provided Go context. It first checks if the context itself is a dbx Context, then checks if a dbx Context is stored as a value within the context using the internal context key.

Parameters:

  • ctx: The context to extract dbx Context from

Returns:

  • Context: The extracted dbx Context, or nil if none found

Example:

dbCtx := dbx.FromContext(ctx)
if dbCtx != nil {
    executor := dbCtx.Executor()
    // Use executor for database operations
}

func NewContext added in v1.4.0

func NewContext(parent context.Context, exec Executor) Context

NewContext creates a new dbx Context by combining a parent Go context with a database executor. The resulting Context can be used for database operations while preserving the parent context's deadline, cancellation, and value propagation behavior.

Parameters:

  • parent: The parent Go context to wrap
  • exec: The database executor (sql.DB or sql.Tx) to embed

Returns:

  • Context: A new dbx Context combining the parent context and executor

Example:

sqlDB, _ := sql.Open("postgres", connectionString)
dbCtx := dbx.NewContext(context.Background(), sqlDB)
result, err := dbCtx.Executor().ExecContext(dbCtx, "INSERT INTO users (name) VALUES (?)", "John")

func NewContextFrom added in v1.5.0

func NewContextFrom(ctx context.Context, input any) Context

NewContextFrom attempts to find an existing dbx Context in the provided context, or creates a new one using the provided creator if none is found. This function is useful for ensuring that a dbx Context is available while avoiding unnecessary Context creation when one already exists.

The creator parameter can be either a ContextCreator, a Database, or any type that has a Context(context.Context) Context method.

Parameters:

  • ctx: The context to search for an existing dbx Context
  • creator: Either a ContextCreator, Database, or Transactor

Returns:

  • Context: Either the existing dbx Context or a newly created one

Example:

// This will reuse existing dbx Context or create new one
dbCtx := dbx.NewContextFrom(ctx, database)
executor := dbCtx.Executor()

func NewDatabaseContext added in v1.9.0

func NewDatabaseContext(parent context.Context, db Database) Context

NewDatabaseContext creates a new dbx Context from a Database instance. This is a convenience function that allows creating a context from any Database, regardless of whether it implements ContextCreator or not.

Parameters:

  • parent: The parent Go context to wrap
  • db: The Database instance to use as the executor

Returns:

  • Context: A new dbx Context with the database as executor

Example:

db := dbx.New(sqlDB)
dbCtx := dbx.NewDatabaseContext(context.Background(), db)
result, err := dbCtx.Executor().ExecContext(dbCtx, "INSERT INTO users (name) VALUES (?)", "John")

type ContextCreator added in v1.0.1

type ContextCreator interface {
	// Context creates a new dbx Context from a standard Go context.
	// The returned Context embeds the database executor and can be used
	// for database operations within the provided context's lifecycle.
	Context(ctx context.Context) Context
}

ContextCreator provides the ability to create dbx Context instances from standard Go contexts. This is typically implemented by Database to bootstrap the context-driven database operations.

type Database

type Database interface {
	// Embed io.Closer to allow proper database connection cleanup
	io.Closer

	// Embed Beginner to support transaction creation
	Beginner

	// Embed Executor to support direct database operations
	Executor
}

Database interface represents the main entry point for dbx operations. It combines database connection management, transaction initiation, and direct query execution capabilities.

Database implementations should wrap sql.DB and provide context-aware database operations while maintaining compatibility with the standard database/sql interface.

type DatabaseWithContext added in v1.9.0

type DatabaseWithContext interface {
	Database
	ContextCreator
}

DatabaseWithContext is an optional interface that databases can implement to provide context creation capabilities. This is separate from the main Database interface to maintain compatibility with external packages.

func New

func New(db *sql.DB) DatabaseWithContext

New creates a new Database instance that wraps the provided sql.DB. The returned Database provides context-driven database operations and automatic transaction management while preserving all the functionality of the underlying sql.DB.

The returned database also implements ContextCreator, allowing direct context creation via the Context method.

Parameters:

  • db: A properly initialized sql.DB instance. The caller remains responsible for its configuration and driver setup. Closing the returned wrapper closes db.

Returns:

  • DatabaseWithContext: A dbx Database that can create contexts and manage transactions.

Example:

sqlDB, err := sql.Open("postgres", connectionString)
if err != nil {
    return err
}
dbxDB := dbx.New(sqlDB)
defer dbxDB.Close()

ctx := dbxDB.Context(context.Background())
rows, err := ctx.Executor().QueryContext(ctx, "SELECT * FROM users")

type Executor

type Executor interface {
	// Exec executes a query without returning any rows.
	// The args are for any placeholder parameters in the query.
	Exec(query string, args ...interface{}) (sql.Result, error)

	// Query executes a query that returns rows, typically a SELECT.
	// The args are for any placeholder parameters in the query.
	Query(query string, args ...interface{}) (*sql.Rows, error)

	// QueryRow executes a query that is expected to return at most one row.
	// QueryRow always returns a non-nil value. Errors are deferred until
	// Row's Scan method is called.
	QueryRow(query string, args ...interface{}) *sql.Row

	// ExecContext executes a query without returning any rows.
	// The args are for any placeholder parameters in the query.
	ExecContext(dbContext context.Context, query string, args ...interface{}) (sql.Result, error)

	// QueryContext executes a query that returns rows, typically a SELECT.
	// The args are for any placeholder parameters in the query.
	QueryContext(dbContext context.Context, query string, args ...interface{}) (*sql.Rows, error)

	// QueryRowContext executes a query that is expected to return at most one row.
	// QueryRowContext always returns a non-nil value. Errors are deferred until
	// Row's Scan method is called.
	QueryRowContext(dbContext context.Context, query string, args ...interface{}) *sql.Row
}

Executor provides an abstraction for both sql.DB and sql.Tx, allowing uniform database operations regardless of whether you're working with a direct database connection or within a transaction.

This interface mirrors the core methods available in both sql.DB and sql.Tx, providing both context-aware and legacy methods for maximum compatibility.

type Operation

type Operation func(ctx Context) error

Operation represents a user-defined database operation that needs to be performed within a transaction. Operations receive a dbx Context and should return an error if the operation fails.

Example:

op := func(ctx dbx.Context) error {
	_, err := ctx.Executor().ExecContext(ctx, "INSERT INTO users (name) VALUES (?)", "John")
	return err
}

type OperationWithResult added in v1.2.0

type OperationWithResult[T any] func(ctx Context) (T, error)

OperationWithResult represents a user-defined database operation that needs to be performed within a transaction and returns a typed result. This is useful for operations that need to return data, such as inserted IDs or query results.

Example:

op := func(ctx dbx.Context) (int64, error) {
	result, err := ctx.Executor().ExecContext(ctx, "INSERT INTO users (name) VALUES (?)", "John")
	if err != nil {
		return 0, err
	}
	return result.LastInsertId()
}

type Option added in v1.2.0

type Option func(opts *options)

Option is a functional option type for configuring transaction behavior. Options are applied when creating transactions through Transaction or TransactionWithResult functions.

Example:

err := dbx.Transaction(ctx, db, operation,
    dbx.WithIsolationLevel(sql.LevelSerializable),
    dbx.WithReadOnly(true),
)

func WithIsolationLevel added in v1.2.0

func WithIsolationLevel(level sql.IsolationLevel) Option

WithIsolationLevel sets the isolation level for the transaction. This option configures how the transaction isolates its operations from other concurrent transactions. The option is applied only when a new transaction is created; it cannot change an existing reused transaction.

Parameters:

  • level: The SQL isolation level (e.g., sql.LevelReadCommitted, sql.LevelSerializable)

Returns:

  • Option: A functional option that can be passed to Transaction functions

Example:

err := dbx.Transaction(ctx, db, operation,
    dbx.WithIsolationLevel(sql.LevelSerializable),
)

func WithNewTransaction added in v1.4.0

func WithNewTransaction() Option

WithNewTransaction forces the creation of an independent transaction even if there is an existing transaction in the context. The new transaction is not a nested transaction or savepoint: it can be committed or rolled back independently and may use a different connection from the database pool.

By default, dbx reuses existing transactions found in the context to avoid nested transaction issues. Use this option when you explicitly need an independent transaction boundary.

Returns:

  • Option: A functional option that can be passed to Transaction functions

Example:

// Force a new transaction even if we're already in a transaction
err := dbx.Transaction(ctx, db, independentOperation,
    dbx.WithNewTransaction(),
)

func WithReadOnly added in v1.2.0

func WithReadOnly(readOnly bool) Option

WithReadOnly sets the read-only flag for the transaction. Read-only transactions can provide performance benefits and prevent accidental data modifications. The option is applied only when a new transaction is created; it cannot change an existing reused transaction.

Parameters:

  • readOnly: true to make the transaction read-only, false for read-write

Returns:

  • Option: A functional option that can be passed to Transaction functions

Example:

// Create a read-only transaction for safe data reading
users, err := dbx.TransactionWithResult(ctx, db, getUsersOperation,
    dbx.WithReadOnly(true),
)

type Transactor added in v1.0.1

type Transactor interface {
	// Commit commits the transaction. If the transaction has already been
	// committed or rolled back, Commit returns an error.
	Commit() error

	// Rollback aborts the transaction. If the transaction has already been
	// committed or rolled back, Rollback returns an error.
	Rollback() error

	// Embed Executor to provide all database operation methods
	Executor
}

Transactor provides an abstraction for sql.Tx, extending Executor with transaction control methods. This interface allows managing transaction lifecycle through explicit commit and rollback operations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL