database

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Dec 30, 2025 License: MIT Imports: 7 Imported by: 0

README

Database Package

Unified interface for SQL database operations across PostgreSQL, MariaDB/MySQL, and future databases.

Quick Start

import (
    "go.uber.org/fx"
    "github.com/Aleph-Alpha/std/v1/database"
    "github.com/Aleph-Alpha/std/v1/postgres"
)

app := fx.New(
    database.FXModule,
    
    // Choose your database
    fx.Provide(func() database.Config {
        return database.PostgresConfig(postgres.Config{
            Connection: postgres.Connection{
                Host: "localhost",
                Port: 5432,
                User: "myuser",
                Password: "mypass",
                DbName: "mydb",
            },
        })
    }),
    
    // Your application receives database.Client
    fx.Invoke(func(db database.Client) {
        // Use db...
    }),
)
Without FX
import (
    "github.com/Aleph-Alpha/std/v1/database"
    "github.com/Aleph-Alpha/std/v1/postgres"
    "github.com/Aleph-Alpha/std/v1/mariadb"
)

// Application code depends on the interface
type UserService struct {
    db database.Client
}

// Select implementation via configuration
func NewDB(config Config) (database.Client, error) {
    switch config.DBType {
    case "postgres":
        return postgres.NewPostgres(config.Postgres)
    case "mariadb":
        return mariadb.NewMariaDB(config.MariaDB)
    default:
        return nil, fmt.Errorf("unsupported database: %s", config.DBType)
    }
}

Switching Databases

To switch from PostgreSQL to MariaDB, just change the config:

// From PostgreSQL
fx.Provide(func() database.Config {
    return database.PostgresConfig(postgres.Config{...})
})

// To MariaDB
fx.Provide(func() database.Config {
    return database.MariaDBConfig(mariadb.Config{...})
})

No application code changes needed!

Benefits

  • True database-agnosticism: Switch databases with configuration, not code changes
  • Interface compatibility: Same methods work across PostgreSQL and MariaDB
  • Easy testing: Mock the interface for unit tests
  • Future-proof: Ready for additional databases (SQLite, CockroachDB, etc.)
  • FX Integration: First-class support for dependency injection

Implementations

Database Package Implementation
PostgreSQL v1/postgres *postgres.Postgres
MariaDB/MySQL v1/mariadb *mariadb.MariaDB

Key Methods

CRUD Operations
  • Find, First, Create, Save, Update, Delete, Count
Query Builder
  • Query(ctx).Where(...).Order(...).Limit(...).Find(&result)
Transactions
  • Transaction(ctx, func(tx Client) error { ... })
Error Handling
  • TranslateError(err) - Normalize to std sentinels
  • IsRetryable(err), IsTemporary(err), IsCritical(err)

Database-Specific Behavior

Row-Level Locking

PostgreSQL: Works everywhere, supports all lock modes
MariaDB: Requires InnoDB + explicit transactions

Always use locking within transactions for compatibility:

db.Transaction(ctx, func(tx database.Client) error {
    return tx.Query(ctx).ForUpdate().First(&user)
})

See doc.go for complete documentation.

Documentation

Overview

Package database provides a unified interface for SQL database operations.

This package defines shared interfaces (Client, QueryBuilder) that work across different SQL databases including PostgreSQL, MariaDB/MySQL, and potentially others.

Philosophy

The database package follows Go's "accept interfaces, return structs" principle:

  • Applications depend on database.Client interface
  • Implementations (postgres, mariadb) return concrete types
  • Concrete types implement the interface

This enables:

  • True database-agnostic application code
  • Easy switching between databases via configuration
  • Simplified testing with mock implementations
  • Future support for additional databases (SQLite, CockroachDB, etc.)

Basic Usage

Import the database package and choose an implementation:

import (
    "github.com/Aleph-Alpha/std/v1/database"
    "github.com/Aleph-Alpha/std/v1/postgres"
    "github.com/Aleph-Alpha/std/v1/mariadb"
)

// Application code depends on the interface
type UserRepository struct {
    db database.Client
}

func NewUserRepository(db database.Client) *UserRepository {
    return &UserRepository{db: db}
}

// Configuration-driven database selection
func NewDatabase(config Config) (database.Client, error) {
    switch config.DBType {
    case "postgres":
        return postgres.NewPostgres(config.Postgres)
    case "mariadb":
        return mariadb.NewMariaDB(config.MariaDB)
    default:
        return nil, fmt.Errorf("unsupported database: %s", config.DBType)
    }
}

Using with Fx Dependency Injection

For applications using Uber's fx framework:

import (
    "go.uber.org/fx"
    "github.com/Aleph-Alpha/std/v1/database"
    "github.com/Aleph-Alpha/std/v1/postgres"
)

app := fx.New(
    // Provide the implementation
    postgres.FXModule,

    // Annotate to provide as database.Client interface
    fx.Provide(
        fx.Annotate(
            func(pg *postgres.Postgres) database.Client {
                return pg
            },
            fx.As(new(database.Client)),
        ),
    ),

    // Application code receives database.Client
    fx.Invoke(func(db database.Client) {
        // Use db...
    }),
)

Database-Specific Behavior

While the interface is unified, some operations have database-specific behavior:

## Row-Level Locking

PostgreSQL:

  • Row-level locks work in all contexts (even outside explicit transactions)
  • Supports all locking modes: FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHARE
  • Each statement is implicitly wrapped in a transaction

MariaDB/MySQL:

  • Row-level locks require InnoDB storage engine
  • Row-level locks require explicit transactions (or autocommit=0)
  • With autocommit=1 (default), locks have NO EFFECT in InnoDB
  • Non-InnoDB engines (MyISAM, Aria) fall back to table-level locks
  • Only supports FOR UPDATE and FOR SHARE
  • FOR NO KEY UPDATE and FOR KEY SHARE are no-ops

Best practice: Always use locking within transactions:

err := db.Transaction(ctx, func(tx database.Client) error {
    var user User
    // This works on both PostgreSQL and MariaDB
    err := tx.Query(ctx).
        Where("id = ?", userID).
        ForUpdate().
        First(&user)
    if err != nil {
        return err
    }
    // Update user...
    return tx.Save(ctx, &user)
})

## Error Handling

Use TranslateError to normalize database-specific errors to std sentinels:

err := db.First(ctx, &user, "id = ?", id)
err = db.TranslateError(err) // Normalize to std errors

switch {
case errors.Is(err, postgres.ErrRecordNotFound):
    // Handle not found
case errors.Is(err, postgres.ErrDuplicateKey):
    // Handle duplicate
case db.IsRetryable(err):
    // Retry the operation
}

## Query Builder

The QueryBuilder interface provides a fluent API for complex queries:

var users []User
err := db.Query(ctx).
    Select("id, name, email").
    Where("age > ?", 18).
    Where("status = ?", "active").
    Order("created_at DESC").
    Limit(100).
    Find(&users)

## Transactions

Transactions work identically across databases:

err := db.Transaction(ctx, func(tx database.Client) error {
    // All operations use tx, not db
    if err := tx.Create(ctx, &user); err != nil {
        return err // Automatic rollback
    }
    if err := tx.Create(ctx, &profile); err != nil {
        return err // Automatic rollback
    }
    return nil // Automatic commit
})

Migration Between Databases

To migrate from database-specific code to database-agnostic code:

Before:

import "github.com/Aleph-Alpha/std/v1/postgres"

type Service struct {
    db *postgres.Postgres
}

After:

import "github.com/Aleph-Alpha/std/v1/database"

type Service struct {
    db database.Client
}

All methods remain the same - only the type changes.

Testing

Mock the database.Client interface for unit tests:

type MockDB struct {
    database.Client
    FindFunc func(ctx context.Context, dest interface{}, conditions ...interface{}) error
}

func (m *MockDB) Find(ctx context.Context, dest interface{}, conditions ...interface{}) error {
    if m.FindFunc != nil {
        return m.FindFunc(ctx, dest, conditions...)
    }
    return nil
}

Future Databases

This interface is designed to be implemented by additional databases:

  • SQLite (for embedded use cases)
  • CockroachDB (for distributed SQL)
  • TiDB (for MySQL-compatible distributed SQL)
  • Any other SQL database using GORM

New implementations should:

  1. Implement database.Client and database.QueryBuilder
  2. Document database-specific behavior
  3. Add compile-time checks: var _ database.Client = (*YourDB)(nil)

For more details, see the individual database package documentation:

  • docs/v1/postgres.md
  • docs/v1/mariadb.md

Package database provides a unified interface for SQL database operations.

This package defines shared interfaces (Client, QueryBuilder) that work across different SQL databases including PostgreSQL, MariaDB/MySQL, and potentially others.

Implementations

The Client interface is implemented by:

  • postgres.Postgres (*Postgres)
  • mariadb.MariaDB (*MariaDB)

Usage

Applications can depend on the database.Client interface for true database-agnostic code:

type UserRepository struct {
    db database.Client
}

func NewUserRepository(db database.Client) *UserRepository {
    return &UserRepository{db: db}
}

Then select the implementation via configuration:

var client database.Client
switch config.DBType {
case "postgres":
    client, err = postgres.NewClient(config.Postgres)
case "mariadb":
    client, err = mariadb.NewClient(config.MariaDB)
}

Database-Specific Behavior

While the interface is unified, some methods have database-specific behavior:

Row-Level Locking:

  • PostgreSQL: Works in all contexts (even outside explicit transactions)
  • MariaDB: Requires InnoDB storage engine AND explicit transactions (or autocommit=0)

Lock Modes:

  • ForNoKeyUpdate(), ForKeyShare(): PostgreSQL-only (no-op in MariaDB)
  • ForUpdate(), ForShare(): Supported by both

See individual database package documentation for details.

Index

Examples

Constants

This section is empty.

Variables

FXModule provides database.Client via dependency injection. It selects the implementation (postgres or mariadb) based on the provided Config.

Usage:

app := fx.New(
    database.FXModule,
    fx.Provide(func() database.Config {
        return database.PostgresConfig(postgres.Config{...})
    }),
    fx.Invoke(func(db *postgres.Postgres) {
        // Receives *postgres.Postgres (concrete type)
        // Has all database.Client methods
    }),
)

Or inject as specific type for database-agnostic code:

fx.Invoke(func(db any) {
    // Use type assertion or interface duck typing
    type clientInterface interface {
        Find(ctx context.Context, dest interface{}, conditions ...interface{}) error
        // ... other methods
    }
    client := db.(clientInterface)
})

Functions

func NewClientWithDI

func NewClientWithDI(params DatabaseParams) (any, error)

NewClientWithDI creates a database client using dependency injection. The concrete implementation (postgres or mariadb) is selected based on Config.Type.

Returns database.Client interface.

Note: Due to Go's type system limitations with covariant return types, this returns any. The concrete types (*postgres.Postgres and *mariadb.MariaDB) have all the required methods and will satisfy database.Client at runtime.

func RegisterDatabaseLifecycle

func RegisterDatabaseLifecycle(params DatabaseLifecycleParams)

RegisterDatabaseLifecycle registers the database client with the fx lifecycle system. This ensures proper initialization and graceful shutdown.

Types

type Client

type Client interface {
	// Basic CRUD operations
	Find(ctx context.Context, dest interface{}, conditions ...interface{}) error
	First(ctx context.Context, dest interface{}, conditions ...interface{}) error
	Create(ctx context.Context, value interface{}) error
	Save(ctx context.Context, value interface{}) error
	Update(ctx context.Context, model interface{}, attrs interface{}) (int64, error)
	UpdateColumn(ctx context.Context, model interface{}, columnName string, value interface{}) (int64, error)
	UpdateColumns(ctx context.Context, model interface{}, columnValues map[string]interface{}) (int64, error)
	Delete(ctx context.Context, value interface{}, conditions ...interface{}) (int64, error)
	Count(ctx context.Context, model interface{}, count *int64, conditions ...interface{}) error
	UpdateWhere(ctx context.Context, model interface{}, attrs interface{}, condition string, args ...interface{}) (int64, error)
	Exec(ctx context.Context, sql string, values ...interface{}) (int64, error)

	// Query builder for complex queries
	// Returns the QueryBuilder interface for method chaining
	Query(ctx context.Context) QueryBuilder

	// Transaction support
	// The callback function receives a Client interface (not a concrete type)
	// This allows the same transaction code to work with any database implementation
	Transaction(ctx context.Context, fn func(tx Client) error) error

	// Raw GORM access for advanced use cases
	// Use this when you need direct access to GORM's functionality
	DB() *gorm.DB

	// Error translation / classification.
	//
	// std deliberately returns raw GORM/driver errors from CRUD/query methods.
	// Use TranslateError to normalize errors to std's exported sentinels (ErrRecordNotFound,
	// ErrDuplicateKey, ...), especially when working with the Client interface (e.g. inside
	// Transaction callbacks).
	//
	// Note: GetErrorCategory returns implementation-specific ErrorCategory type
	// (postgres.ErrorCategory or mariadb.ErrorCategory). Cast as needed.
	TranslateError(err error) error
	IsRetryable(err error) bool
	IsTemporary(err error) bool
	IsCritical(err error) bool

	// Lifecycle management
	GracefulShutdown() error
}

Client is the main database client interface that provides CRUD operations, query building, and transaction management.

This interface allows applications to:

  • Switch between PostgreSQL and MariaDB without code changes
  • Write database-agnostic business logic
  • Mock database operations easily for testing
  • Depend on abstractions rather than concrete implementations

Implementations:

  • postgres.Postgres implements this interface
  • mariadb.MariaDB implements this interface

type Config

type Config struct {
	// Type is the database type ("postgres" or "mariadb")
	Type string

	// Postgres configuration (used when Type = "postgres")
	Postgres *postgres.Config

	// MariaDB configuration (used when Type = "mariadb")
	MariaDB *mariadb.Config
}

Config contains configuration for database client creation. Use one of the helper functions (PostgresConfig, MariaDBConfig) to create it.

Example

Example showing how to create a database-agnostic service

package main

import (
	"github.com/Aleph-Alpha/std/v1/database"
	"github.com/Aleph-Alpha/std/v1/postgres"
)

func main() {
	// This function would be called by your application
	// to select the database based on configuration
	createDatabase := func(dbType string) database.Config {
		switch dbType {
		case "postgres":
			return database.PostgresConfig(postgres.Config{
				Connection: postgres.Connection{
					Host: "localhost",
					Port: "5432",
				},
			})
		default:
			return database.Config{}
		}
	}

	cfg := createDatabase("postgres")
	_ = cfg // Pass to database.FXModule or NewClientWithDI
}

func MariaDBConfig

func MariaDBConfig(cfg mariadb.Config) Config

MariaDBConfig creates a database.Config for MariaDB/MySQL. Use this in your fx.Provide function.

Example:

fx.Provide(func() database.Config {
    return database.MariaDBConfig(mariadb.Config{
        Connection: mariadb.Connection{
            Host: "localhost",
            Port: 3306,
            // ...
        },
    })
})

func PostgresConfig

func PostgresConfig(cfg postgres.Config) Config

PostgresConfig creates a database.Config for PostgreSQL. Use this in your fx.Provide function.

Example:

fx.Provide(func() database.Config {
    return database.PostgresConfig(postgres.Config{
        Connection: postgres.Connection{
            Host: "localhost",
            Port: 5432,
            // ...
        },
    })
})
Example

Example showing how to create a PostgreSQL config

package main

import (
	"github.com/Aleph-Alpha/std/v1/database"
	"github.com/Aleph-Alpha/std/v1/postgres"
)

func main() {
	cfg := database.PostgresConfig(postgres.Config{
		Connection: postgres.Connection{
			Host:   "localhost",
			Port:   "5432",
			User:   "myuser",
			DbName: "mydb",
		},
	})

	_ = cfg // Use the config with database.FXModule
}

type DatabaseLifecycleParams

type DatabaseLifecycleParams struct {
	fx.In

	Lifecycle fx.Lifecycle
	Client    Client
}

DatabaseLifecycleParams groups the dependencies needed for database lifecycle management

type DatabaseParams

type DatabaseParams struct {
	fx.In

	Config Config
}

DatabaseParams groups the dependencies needed to create a database client

type QueryBuilder

type QueryBuilder interface {
	// Query modifiers - these return QueryBuilder for chaining
	Select(query interface{}, args ...interface{}) QueryBuilder
	Where(query interface{}, args ...interface{}) QueryBuilder
	Or(query interface{}, args ...interface{}) QueryBuilder
	Not(query interface{}, args ...interface{}) QueryBuilder
	Joins(query string, args ...interface{}) QueryBuilder
	LeftJoin(query string, args ...interface{}) QueryBuilder
	RightJoin(query string, args ...interface{}) QueryBuilder
	Preload(query string, args ...interface{}) QueryBuilder
	Group(query string) QueryBuilder
	Having(query interface{}, args ...interface{}) QueryBuilder
	Order(value interface{}) QueryBuilder
	Limit(limit int) QueryBuilder
	Offset(offset int) QueryBuilder
	Raw(sql string, values ...interface{}) QueryBuilder
	Model(value interface{}) QueryBuilder
	Distinct(args ...interface{}) QueryBuilder
	Table(name string) QueryBuilder
	Unscoped() QueryBuilder
	Scopes(funcs ...func(*gorm.DB) *gorm.DB) QueryBuilder

	// Locking methods
	//
	// Note on row-level locking:
	//   - PostgreSQL: All locking methods work in any context
	//   - MariaDB: Row-level locks require:
	//     * InnoDB storage engine (MyISAM/Aria use table-level locks)
	//     * Explicit transaction (or autocommit=0)
	//     * With autocommit=1, locks have NO EFFECT in InnoDB
	//
	// Always use locking within Transaction() for MariaDB:
	//   db.Transaction(ctx, func(tx Client) error {
	//       return tx.Query(ctx).ForUpdate().First(&user)
	//   })
	ForUpdate() QueryBuilder
	ForShare() QueryBuilder
	ForUpdateSkipLocked() QueryBuilder
	ForShareSkipLocked() QueryBuilder
	ForUpdateNoWait() QueryBuilder
	ForNoKeyUpdate() QueryBuilder // PostgreSQL-only (no-op in MariaDB)
	ForKeyShare() QueryBuilder    // PostgreSQL-only (no-op in MariaDB)

	// Conflict handling and returning
	OnConflict(onConflict interface{}) QueryBuilder
	Returning(columns ...string) QueryBuilder

	// Custom clauses
	Clauses(conds ...interface{}) QueryBuilder

	// Terminal operations - these execute the query
	Scan(dest interface{}) error
	Find(dest interface{}) error
	First(dest interface{}) error
	Last(dest interface{}) error
	Count(count *int64) error
	Updates(values interface{}) (int64, error)
	Delete(value interface{}) (int64, error)
	Pluck(column string, dest interface{}) (int64, error)
	Create(value interface{}) (int64, error)
	CreateInBatches(value interface{}, batchSize int) (int64, error)
	FirstOrInit(dest interface{}, conds ...interface{}) error
	FirstOrCreate(dest interface{}, conds ...interface{}) error

	// Utility methods
	Done()                // Finalize builder (currently a no-op)
	ToSubquery() *gorm.DB // Convert to GORM subquery
}

QueryBuilder provides a fluent interface for building complex database queries. All chainable methods return the QueryBuilder interface, allowing method chaining. Terminal operations (like Find, First, Create) execute the query and return results.

Example:

var users []User
err := db.Query(ctx).
    Where("age > ?", 18).
    Order("created_at DESC").
    Limit(10).
    Find(&users)

Database-Specific Behavior

Some methods have database-specific behavior or limitations:

  • ForNoKeyUpdate(), ForKeyShare(): PostgreSQL-only (no-op in MariaDB)
  • Row-level locking (ForUpdate, ForShare, etc.):
  • PostgreSQL: Works in all contexts
  • MariaDB: Requires InnoDB storage engine AND explicit transactions

Jump to

Keyboard shortcuts

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