dbutil

package
v0.4.3 Latest Latest
Warning

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

Go to latest
Published: Jan 26, 2026 License: MIT Imports: 7 Imported by: 0

README

Database Utilities Package

The dbutil package provides database utility functions and helpers for safe database interactions with connection management, query execution, transaction handling, and context support. It's designed to work with Go's standard database/sql package while adding convenience and safety features.

Features

  • Connection Management: Configuration and connection pooling
  • Query Execution: Safe query execution with struct scanning
  • Transaction Management: Automatic transaction handling with rollback
  • Context Support: Cancellation and timeout support
  • Error Handling: Enhanced error detection and classification
  • Struct Scanning: Automatic scanning into structs
  • Field Mapping: Customizable struct-to-column mapping

Installation

go get github.com/julianstephens/go-utils/dbutil

Usage

Basic Setup
package main

import (
    "context"
    "database/sql"
    "log"
    "time"
    
    "github.com/julianstephens/go-utils/dbutil"
    _ "github.com/lib/pq"
)

func main() {
    db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    opts := dbutil.DefaultConnectionOptions()
    opts.MaxOpenConns = 25
    opts.MaxIdleConns = 5
    opts.ConnMaxLifetime = time.Hour
    
    if err := dbutil.ConfigureDB(db, opts); err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()
    if err := dbutil.PingWithContext(ctx, db, 5*time.Second); err != nil {
        log.Fatal("Database ping failed:", err)
    }
}
Struct Scanning
package main

import (
    "context"
    "database/sql"
    "log"
    "time"
    
    "github.com/julianstephens/go-utils/dbutil"
)

type User struct {
    ID        int64     `db:"id"`
    Name      string    `db:"name"`
    Email     string    `db:"email"`
    CreatedAt time.Time `db:"created_at"`
}

func main() {
    db := setupDatabase()
    ctx := context.Background()

    var user User
    err := dbutil.QueryRowScan(ctx, db, &user, 
        "SELECT id, name, email, created_at FROM users WHERE id = $1", 1)
    if err != nil {
        if dbutil.IsNoRowsError(err) {
            log.Println("User not found")
        } else {
            log.Printf("Query failed: %v", err)
        }
        return
    }

    var users []User
    err = dbutil.QuerySlice(ctx, db, &users,
        "SELECT id, name, email, created_at FROM users WHERE active = $1", true)
    if err != nil {
        log.Fatal(err)
    }
    
    for _, u := range users {
        log.Printf("%s (%s)", u.Name, u.Email)
    }
}
Transaction Management
package main

import (
    "context"
    "database/sql"
    "log"
    
    "github.com/julianstephens/go-utils/dbutil"
)

func main() {
    db := setupDatabase()
    ctx := context.Background()

    err := dbutil.WithTransaction(ctx, db, func(tx *sql.Tx) error {
        result, err := dbutil.ExecTx(ctx, tx, 
            "INSERT INTO users (name, email) VALUES ($1, $2)", 
            "John Doe", "[email protected]")
        if err != nil {
            return err
        }

        userID, err := result.LastInsertId()
        if err != nil {
            return err
        }

        _, err = dbutil.ExecTx(ctx, tx,
            "INSERT INTO audit_log (action, user_id) VALUES ($1, $2)",
            "user_created", userID)
        return err
    })

    if err != nil {
        log.Printf("Transaction failed: %v", err)
    }
}
Advanced Transaction Options
package main

import (
    "context"
    "database/sql"
    "log"
    "time"
    
    "github.com/julianstephens/go-utils/dbutil"
)

func main() {
    db := setupDatabase()
    ctx := context.Background()

    txOpts := dbutil.TransactionOptions{
        Isolation: sql.LevelReadCommitted,
        ReadOnly:  false,
        Timeout:   30 * time.Second,
    }

    err := dbutil.WithTransactionOptions(ctx, db, txOpts, func(tx *sql.Tx) error {
        var count int
        err := dbutil.QueryRowScanTx(ctx, tx, &count,
            "SELECT COUNT(*) FROM users WHERE active = $1", true)
        if err != nil {
            return err
        }

        if count < 100 {
            _, err = dbutil.ExecTx(ctx, tx,
                "INSERT INTO users (name, email) VALUES ($1, $2)",
                "New User", "[email protected]")
        }
        return err
    })

    if err != nil {
        log.Printf("Transaction failed: %v", err)
    }
}
Utility Operations
package main

import (
    "context"
    "database/sql"
    "log"
    
    "github.com/julianstephens/go-utils/dbutil"
)

func main() {
    db := setupDatabase()
    ctx := context.Background()

    exists, err := dbutil.Exists(ctx, db, 
        "SELECT 1 FROM users WHERE email = $1", "[email protected]")
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("User exists: %t", exists)

    count, err := dbutil.Count(ctx, db, 
        "SELECT COUNT(*) FROM users WHERE active = $1", true)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Active users: %d", count)
}
Error Handling
package main

import (
    "context"
    "database/sql"
    "errors"
    "log"
    
    "github.com/julianstephens/go-utils/dbutil"
)

func getUserByID(ctx context.Context, db *sql.DB, userID int64) (*User, error) {
    var user User
    err := dbutil.QueryRowScan(ctx, db, &user,
        "SELECT id, name, email, created_at FROM users WHERE id = $1", userID)
    
    if err != nil {
        if dbutil.IsNoRowsError(err) {
            return nil, errors.New("user not found")
        }
        if dbutil.IsContextError(err) {
            return nil, errors.New("request timeout or cancelled")
        }
        if dbutil.IsConnectionError(err) {
            return nil, errors.New("database connection failed")
        }
        return nil, err
    }
    return &user, nil
}
Field Mapping
package main

import (
    "context"
    "database/sql"
    "log"
    "time"
    
    "github.com/julianstephens/go-utils/dbutil"
)

type UserProfile struct {
    UserID    int64  `db:"user_id"`
    FirstName string `db:"first_name"`
    LastName  string `db:"last_name"`
}

func main() {
    db := setupDatabase()
    ctx := context.Background()

    queryOpts := dbutil.QueryOptions{
        Timeout: 30 * time.Second,
        MaxRows: 100,
    }

    var profiles []UserProfile
    err := dbutil.QuerySliceWithOptions(ctx, db, &profiles, queryOpts,
        "SELECT user_id, first_name, last_name FROM user_profiles")
    if err != nil {
        log.Fatal(err)
    }
}

Configuration Options

ConnectionOptions
  • MaxOpenConns - Maximum open connections
  • MaxIdleConns - Maximum idle connections
  • ConnMaxLifetime - Connection lifetime
  • ConnMaxIdleTime - Connection idle time
  • PingTimeout - Ping timeout
  • RetryAttempts - Retry attempts
  • RetryDelay - Retry delay
QueryOptions
  • Timeout - Query timeout
  • MaxRows - Maximum rows (0 = no limit)
  • FieldMapper - Field name mapper function
TransactionOptions
  • Isolation - Transaction isolation level
  • ReadOnly - Read-only transaction flag
  • Timeout - Transaction timeout

API Reference

Connection Management
  • ConfigureDB(db *sql.DB, opts ConnectionOptions) error - Configure database
  • PingWithContext(ctx context.Context, db *sql.DB, timeout time.Duration) error - Ping with timeout
  • PingWithRetry(ctx context.Context, db *sql.DB, attempts int, delay time.Duration) error - Ping with retry
Query Execution
  • QueryRowScan(ctx, db, dest, query, args...) error - Query single row into struct
  • QuerySlice(ctx, db, dest, query, args...) error - Query multiple rows into slice
  • QuerySliceWithOptions(ctx, db, dest, opts, query, args...) error - Query with options
  • QueryMap(ctx, db, query, args...) (map[string]any, error) - Query row into map
  • QueryMaps(ctx, db, query, args...) ([]map[string]any, error) - Query rows into maps
  • QueryRow(ctx, db, query, args...) *sql.Row - Raw single row
  • QueryRows(ctx, db, query, args...) (*sql.Rows, error) - Raw multiple rows
  • Exec(ctx, db, query, args...) (sql.Result, error) - Execute query
Transaction Management
  • WithTransaction(ctx, db, fn) error - Execute in transaction
  • WithTransactionOptions(ctx, db, opts, fn) error - Execute with options
  • QueryRowScanTx(ctx, tx, dest, query, args...) error - Query single row in tx
  • QuerySliceTx(ctx, tx, dest, query, args...) error - Query slice in tx
  • QueryMapTx(ctx, tx, query, args...) (map[string]any, error) - Query map in tx
  • QueryMapsTx(ctx, tx, query, args...) ([]map[string]any, error) - Query maps in tx
  • QueryRowTx(ctx, tx, query, args...) *sql.Row - Raw row in tx
  • QueryRowsTx(ctx, tx, query, args...) (*sql.Rows, error) - Raw rows in tx
  • ExecTx(ctx, tx, query, args...) (sql.Result, error) - Execute in tx
Utility Functions
  • Exists(ctx, db, query, args...) (bool, error) - Check if record exists
  • ExistsTx(ctx, tx, query, args...) (bool, error) - Check existence in tx
  • Count(ctx, db, query, args...) (int64, error) - Count records
  • CountTx(ctx, tx, query, args...) (int64, error) - Count in tx
Error Detection
  • IsNoRowsError(err) bool - Check for sql.ErrNoRows
  • IsConnectionError(err) bool - Check for connection errors
  • IsContextError(err) bool - Check for context timeout/cancel
Field Mapping
  • DefaultFieldMapper(fieldName) string - CamelCase to snake_case mapper

Supported Struct Tags

Use the db tag to specify database column names:

type User struct {
    ID       int64  `db:"id"`
    UserName string `db:"user_name"`
    Email    string `db:"email_address"`
}

Thread Safety

All functions in the dbutil package are thread-safe and can be called concurrently from multiple goroutines. The package properly handles the underlying database/sql thread safety guarantees.

Best Practices

  1. Always use context for cancellation and timeouts
  2. Use transactions for operations requiring atomicity
  3. Handle specific error types using provided detection functions
  4. Configure connection pooling appropriately for your workload
  5. Use struct tags to map Go fields to database columns
  6. Set appropriate timeouts for different operation types
  7. Consider read-only transactions for complex read operations

Database Driver Compatibility

Works with any database driver implementing Go's database/sql interface:

  • PostgreSQL (github.com/lib/pq)
  • MySQL (github.com/go-sql-driver/mysql)
  • SQLite (github.com/mattn/go-sqlite3)
  • SQL Server (github.com/denisenkom/go-mssqldb)

Integration

Works well with other go-utils packages:

  • logger: Log database operations
  • config: Manage database configuration

Documentation

Overview

Package dbutil provides utility functions and helpers for interacting with databases in Go projects.

This package offers enhanced database/sql functionality with features like: - Connection management with retry logic and context support - Query execution helpers with automatic scanning and error context - Transaction management utilities with rollback handling - Context-aware operations for cancellation and timeout support - Convenience generic helpers for scanning rows into slices/structs

Core types and functions ------------------------

- ConfigureDB(db *sql.DB, opts *ConnectionOptions) error

  • Helper to apply sensible defaults and connection pooling settings.

- PingWithContext(ctx context.Context, db *sql.DB, timeout time.Duration) error

  • Health check that respects context and timeout.

- QueryRow(ctx context.Context, db *sql.DB, query string, args ...interface{}) *sql.Row

  • Thin wrapper providing consistent logging and error context.

- QueryRows[T any](ctx context.Context, db *sql.DB, query string, args ...interface{}) ([]T, error)

  • Generic helper to run queries and scan results into a slice of T.

- WithTransaction(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error

  • Run a function within a transaction and automatically rollback on error.

Examples --------

Basic Query Row scan:

var user User
err := dbutil.QueryRowScan(ctx, db, &user, "SELECT id, name, email FROM users WHERE id = $1", 1)
if err != nil {
		return err
}

Query multiple rows into a slice using the generic helper:

users, err := dbutil.QueryRows[User](ctx, db, "SELECT id, name, email FROM users WHERE active = $1", true)
if err != nil {
		return err
}

Safe transaction usage:

err := dbutil.WithTransaction(ctx, db, func(tx *sql.Tx) error {
		if _, err := tx.ExecContext(ctx, "INSERT INTO users (name) VALUES ($1)", "alice"); err != nil {
				return err
		}
		return nil
})

The package is intentionally small and focused on making common database operations less error-prone and easier to read. See the package tests for additional usage patterns and edge cases.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfigureDB

func ConfigureDB(db *sql.DB, opts *ConnectionOptions) error

ConfigureDB configures a database connection with the provided options.

func Count

func Count(ctx context.Context, db *sql.DB, query string, args ...any) (int64, error)

Count executes a COUNT query and returns the result.

func CountTx

func CountTx(ctx context.Context, tx *sql.Tx, query string, args ...any) (int64, error)

CountTx is like Count but uses a transaction.

func DefaultFieldMapper

func DefaultFieldMapper(fieldName string) string

DefaultFieldMapper converts Go struct field names to database column names. It converts CamelCase to snake_case (e.g., "UserID" -> "user_id").

func Exec

func Exec(ctx context.Context, db *sql.DB, query string, args ...any) (sql.Result, error)

Exec executes a query without returning any rows. It returns the number of rows affected and any error encountered.

func ExecTx

func ExecTx(ctx context.Context, tx *sql.Tx, query string, args ...any) (sql.Result, error)

ExecTx is like Exec but uses a transaction.

func Exists

func Exists(ctx context.Context, db *sql.DB, query string, args ...any) (bool, error)

Exists checks if a query returns any rows.

func ExistsTx

func ExistsTx(ctx context.Context, tx *sql.Tx, query string, args ...any) (bool, error)

ExistsTx is like Exists but uses a transaction.

func IsConnectionError

func IsConnectionError(err error) bool

IsConnectionError checks if an error is a database connection error.

func IsContextError

func IsContextError(err error) bool

IsContextError checks if an error is a context cancellation or timeout error.

func IsNoRowsError

func IsNoRowsError(err error) bool

IsNoRowsError checks if an error is a sql.ErrNoRows error.

func PingWithContext

func PingWithContext(ctx context.Context, db *sql.DB, timeout time.Duration) error

PingWithContext pings the database with a context timeout.

func PingWithRetry

func PingWithRetry(ctx context.Context, db *sql.DB, attempts int, delay time.Duration) error

PingWithRetry pings the database with retry logic.

func QueryMap

func QueryMap(ctx context.Context, db *sql.DB, query string, args ...any) (map[string]any, error)

QueryMap executes a query and returns the first row as a map[string]interface{}.

func QueryMapTx

func QueryMapTx(ctx context.Context, tx *sql.Tx, query string, args ...any) (map[string]any, error)

QueryMapTx is like QueryMap but uses a transaction.

func QueryMaps

func QueryMaps(ctx context.Context, db *sql.DB, query string, args ...any) ([]map[string]any, error)

QueryMaps executes a query and returns all rows as []map[string]interface{}.

func QueryMapsTx

func QueryMapsTx(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]map[string]any, error)

QueryMapsTx is like QueryMaps but uses a transaction.

func QueryRow

func QueryRow(ctx context.Context, db *sql.DB, query string, args ...any) *sql.Row

QueryRow executes a query that is expected to return at most one row. It returns a *sql.Row which can be scanned into destination variables.

func QueryRowScan

func QueryRowScan(ctx context.Context, db *sql.DB, dest any, query string, args ...any) error

QueryRowScan executes a query that returns a single row and scans the result into dest. dest should be a pointer to a struct with appropriate db tags.

func QueryRowScanTx

func QueryRowScanTx(ctx context.Context, tx *sql.Tx, dest any, query string, args ...any) error

QueryRowScanTx is like QueryRowScan but uses a transaction.

func QueryRowTx

func QueryRowTx(ctx context.Context, tx *sql.Tx, query string, args ...any) *sql.Row

QueryRowTx is like QueryRow but uses a transaction.

func QueryRows

func QueryRows(ctx context.Context, db *sql.DB, query string, args ...any) (*sql.Rows, error)

QueryRows executes a query and returns multiple rows. It's the caller's responsibility to close the returned *sql.Rows.

func QueryRowsTx

func QueryRowsTx(ctx context.Context, tx *sql.Tx, query string, args ...any) (*sql.Rows, error)

QueryRowsTx is like QueryRows but uses a transaction.

func QuerySlice

func QuerySlice(ctx context.Context, db *sql.DB, dest any, query string, args ...any) error

QuerySlice executes a query and scans all rows into a slice of structs. dest should be a pointer to a slice of structs with appropriate db tags.

func QuerySliceTx

func QuerySliceTx(ctx context.Context, tx *sql.Tx, dest any, query string, args ...any) error

QuerySliceTx is like QuerySlice but uses a transaction.

func QuerySliceWithOptions

func QuerySliceWithOptions(
	ctx context.Context,
	db *sql.DB,
	dest any,
	query string,
	opts *QueryOptions,
	args ...any,
) error

QuerySliceWithOptions executes a query and scans all rows into a slice of structs with options.

func QuerySliceWithOptionsTx

func QuerySliceWithOptionsTx(
	ctx context.Context,
	tx *sql.Tx,
	dest any,
	query string,
	opts *QueryOptions,
	args ...any,
) error

QuerySliceWithOptionsTx is like QuerySliceWithOptions but uses a transaction.

func WithTransaction

func WithTransaction(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error

WithTransaction executes a function within a database transaction. If the function returns an error, the transaction is rolled back. Otherwise, the transaction is committed.

func WithTransactionOptions

func WithTransactionOptions(ctx context.Context, db *sql.DB, opts *TransactionOptions, fn func(*sql.Tx) error) error

WithTransactionOptions is like WithTransaction but allows specifying transaction options.

Types

type ConnectionOptions

type ConnectionOptions struct {
	// MaxOpenConns sets the maximum number of open connections to the database.
	MaxOpenConns int
	// MaxIdleConns sets the maximum number of connections in the idle connection pool.
	MaxIdleConns int
	// ConnMaxLifetime sets the maximum amount of time a connection may be reused.
	ConnMaxLifetime time.Duration
	// ConnMaxIdleTime sets the maximum amount of time a connection may be idle.
	ConnMaxIdleTime time.Duration
	// PingTimeout sets the timeout for ping operations.
	PingTimeout time.Duration
	// RetryAttempts sets the number of retry attempts for failed operations.
	RetryAttempts int
	// RetryDelay sets the delay between retry attempts.
	RetryDelay time.Duration
}

ConnectionOptions holds configuration options for database connections.

func DefaultConnectionOptions

func DefaultConnectionOptions() *ConnectionOptions

DefaultConnectionOptions returns sensible default connection options.

type QueryOptions

type QueryOptions struct {
	// Timeout sets the timeout for query execution.
	Timeout time.Duration
	// MaxRows limits the number of rows returned (0 means no limit).
	MaxRows int
	// FieldMapper is a function to map struct field names to database column names.
	FieldMapper func(string) string
}

QueryOptions holds configuration options for query execution.

func DefaultQueryOptions

func DefaultQueryOptions() *QueryOptions

DefaultQueryOptions returns sensible default query options.

type TransactionOptions

type TransactionOptions struct {
	// Isolation sets the transaction isolation level.
	Isolation sql.IsolationLevel
	// ReadOnly sets whether the transaction is read-only.
	ReadOnly bool
	// Timeout sets the timeout for the entire transaction.
	Timeout time.Duration
}

TransactionOptions holds configuration options for transactions.

func DefaultTransactionOptions

func DefaultTransactionOptions() *TransactionOptions

DefaultTransactionOptions returns sensible default transaction options.

Jump to

Keyboard shortcuts

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