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:
- Implement database.Client and database.QueryBuilder
- Document database-specific behavior
- 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 ¶
var FXModule = fx.Module("database", fx.Provide(NewClientWithDI), fx.Invoke(RegisterDatabaseLifecycle), )
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 ¶
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 ¶
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 ¶
DatabaseLifecycleParams groups the dependencies needed for database lifecycle management
type DatabaseParams ¶
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
ForUpdateSkipLocked() QueryBuilder
ForUpdateNoWait() QueryBuilder
ForNoKeyUpdate() 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