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 ¶
- func Is(ctx context.Context) bool
- func Transaction(ctx context.Context, beginner Beginner, op Operation, opts ...Option) error
- func TransactionWithResult[T any](ctx context.Context, beginner Beginner, op OperationWithResult[T], ...) (T, error)
- func WithContext(ctx context.Context, dbCtx Context) context.Context
- type Beginner
- type Context
- type ContextCreator
- type Database
- type DatabaseWithContext
- type Executor
- type Operation
- type OperationWithResult
- type Option
- type Transactor
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Is ¶
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
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
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 ¶
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
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
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
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
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
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
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.