Documentation
¶
Overview ¶
Package db provides the data access layer for the appointments service. It wraps a PostgreSQL database using database/sql and exposes type-safe query methods generated by sqlc.
Architecture ¶
The package is built around three layers:
- Connection management (New, Config, [database]) — opens and configures the connection pool, returns a Database handle.
- Replica (Replica) — a thin wrapper around *sql.DB that implements the DBTX interface and exposes Replica.Begin for transactions.
- Query layer ([Query], [Querier]) — a stateless [Queries] value whose methods accept a DBTX so they work transparently against a plain connection or an active transaction.
Basic Usage ¶
Initialize the database once at application startup:
database, err := db.New(db.Config{
PrimaryDSN: os.Getenv("DATABASE_URL"),
Namespace: "appointments",
})
if err != nil {
log.Fatal(err)
}
defer database.Close()
Run a query against the primary replica:
tenant, err := db.Query.FindTenantByID(ctx, database.RO(), tenantID)
Run multiple queries inside a transaction:
tx, err := database.RW().Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback()
if err := db.Query.InsertTenant(ctx, tx, params); err != nil {
return err
}
if err := db.Query.InsertStatusHistory(ctx, tx, historyParams); err != nil {
return err
}
return tx.Commit()
Key Features ¶
Type-safe queries: all SQL operations are generated by sqlc v1.30.0 from files in the queries/ directory. Never edit *_generated.go files by hand; run go generate ./pkg/db/... to regenerate them.
Connection pooling: New configures the pool with sensible defaults (25 max open connections, 10 idle, 5-minute lifetime) to avoid cold-start latency.
Unified DBTX interface: DBTX is satisfied by both *Replica and *sql.Tx, so every query method accepts either without branching.
Transaction support: Replica.Begin starts a DBTx that adds DBTx.Commit and DBTx.Rollback on top of DBTX.
Package db provides database transaction utilities for the platform. It offers transaction lifecycle management with automatic rollback on errors and proper error wrapping for consistent fault handling across services.
The package is shared across all services.
Index ¶
- func Tx(ctx context.Context, db *Replica, fn func(context.Context, DBTX) error) error
- func TxWithResult[T any](ctx context.Context, db *Replica, fn func(context.Context, DBTX) (T, error)) (T, error)
- func UnmarshalNullableJSONTo[T any](data any) (T, error)
- type Config
- type DBTX
- type DBTx
- type Database
- type Replica
- func (r *Replica) Begin(ctx context.Context) (DBTx, error)
- func (r *Replica) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
- func (r *Replica) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
- func (r *Replica) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
- func (r *Replica) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
- type ReplicaOption
- type TracedTx
- func (t *TracedTx) Commit() error
- func (t *TracedTx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
- func (t *TracedTx) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
- func (t *TracedTx) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
- func (t *TracedTx) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
- func (t *TracedTx) Rollback() error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Tx ¶
Tx executes fn within a database transaction without returning a result. It is a convenience wrapper around TxWithResult for operations that only need error handling.
Tx begins a transaction on db, executes fn with the transaction context, and commits on success or rolls back on failure. All database errors are wrapped with ServiceUnavailable fault codes.
The ctx parameter provides cancellation and timeout control. The db parameter must be a valid Replica instance. The fn parameter receives the transaction context and a DBTX interface for database operations.
Tx returns nil on successful commit, or an error if any step fails. Error handling follows the same patterns as TxWithResult.
Use Tx for operations that don't need to return values, such as:
- Deleting records with audit logging
- Updating configuration settings
- Batch cleanup operations
- State changes that only need success/failure indication
See TxWithResult for detailed transaction behavior and DBTX for available database operations.
func TxWithResult ¶
func TxWithResult[T any](ctx context.Context, db *Replica, fn func(context.Context, DBTX) (T, error)) (T, error)
TxWithResult executes fn within a database transaction and returns the result. It begins a transaction on db, executes fn with the transaction context, and commits on success or rolls back on failure.
The function automatically handles the complete transaction lifecycle: begin, execute, and commit/rollback.
TxWithResult is generic and preserves type safety for return values. The ctx parameter provides cancellation and timeout control for the entire transaction. The db parameter must be a valid Replica instance, typically from [Database.Primary] for write operations.
The fn parameter receives the transaction context and a DBTX interface for database operations. It should perform all required operations and return the result with any error.
TxWithResult returns the function result on successful commit, or an error if any step fails. Transaction begin errors return ServiceUnavailable. Rollback errors during error handling also return ServiceUnavailable, except for sql.ErrTxDone which indicates the transaction was already completed. Commit errors return ServiceUnavailable.
Context cancellation triggers automatic rollback. The function is safe for concurrent use but callers must avoid operations that could deadlock with other concurrent transactions.
Common usage scenarios include:
- Creating tenants with associated information atomically
- Batch operations that must succeed or fail as a unit
- Complex queries requiring consistency guarantees
Edge cases and limitations:
- If fn returns an error, rollback is attempted even if the transaction is already in a failed state, which may produce additional errors
- Database connection issues during commit may leave the transaction in an undefined state on the server side
- Context cancellation after fn execution causes rollback instead of commit
Anti-patterns to avoid:
- Long-running operations within fn that could timeout
- Nesting calls to TxWithResult (creates nested transactions)
- Ignoring the returned error from fn
- Accessing the DBTX parameter outside of the fn callback
Use context.WithTimeout to prevent indefinite blocking. For operations that may conflict, implement retry logic with exponential backoff at the caller level.
See Replica.Begin for transaction initiation and DBTX for available operations within transactions. For read-only operations that don't require transactions, use query methods directly on Database.RO.
func UnmarshalNullableJSONTo ¶
UnmarshalNullableJSONTo unmarshals JSON data from database columns into Go types. It handles the common pattern where database queries return JSON as []byte that needs to be deserialized into structs, slices, or maps.
The function accepts 'any' type because database drivers return interface{} for JSON columns, even though the underlying value is typically []byte.
Returns:
- (T, nil) on successful unmarshal
- (zero, nil) if data is nil or empty []byte (these are valid null/empty states)
- (zero, error) if type assertion fails or JSON unmarshal fails
Example usage:
settings, err := UnmarshalNullableJSONTo[Type](row.Type)
if err != nil {
logger.Error("failed to unmarshal type", "error", err)
return err
}
Types ¶
type Config ¶
type Config struct {
// The primary DSN for your database. This must support both reads and writes.
PrimaryDSN string
// The readonly replica will be used for most read queries.
// If omitted, the primary is used.
ReadOnlyDSN string
// Namespace prefixes all Prometheus metrics emitted by the database
// replicas. For example, "appointments" produces metrics such as
// "appointments_database_operations_total". The same namespace is used
// for both primary and read-only replicas. Leave it empty to emit metrics
// without an application prefix, such as "database_operations_total".
Namespace string
}
Config defines the parameters needed to establish database connections. It supports separate connections for read and write operations to allow for primary/replica setups.
type DBTX ¶
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
DBTX is an interface that abstracts database operations for both direct connections and transactions. It allows query methods to work with either a database or transaction, making transaction handling more flexible.
This interface is implemented by both sql.DB and sql.Tx, as well as the custom Replica type in this package.
type DBTx ¶
DBTx represents a database transaction with commit and rollback capabilities. It extends DBTX with transaction-specific methods.
type Database ¶
type Database interface {
// RW returns the write (primary) replica for write operations
RW() *Replica
// RO returns the read replica for read operations
// If no read replica is configured, it returns the write replica
RO() *Replica
// Close properly terminates all database connections
Close() error
}
Database defines the interface for database operations, providing access to read and write replicas and the ability to close connections.
type Replica ¶
type Replica struct {
// contains filtered or unexported fields
}
Replica wraps a standard SQL database connection and implements the gen.DBTX interface to enable interaction with the generated database code.
func NewReplica ¶
func NewReplica(db *sql.DB, mode string, options ...ReplicaOption) *Replica
NewReplica wraps db with tracing and Prometheus instrumentation. Metrics are enabled by default without a namespace prefix.
func (*Replica) Begin ¶
Begin starts a transaction and returns it. This method provides a way to use the Replica in transaction-based operations.
func (*Replica) ExecContext ¶
ExecContext executes a SQL statement and returns a result summary. It's used for INSERT, UPDATE, DELETE statements that don't return rows.
func (*Replica) PrepareContext ¶
PrepareContext prepares a SQL statement for later execution.
func (*Replica) QueryContext ¶
QueryContext executes a SQL query that returns rows.
type ReplicaOption ¶
type ReplicaOption func(*replicaConfig)
ReplicaOption configures a Replica.
func WithMetrics ¶
func WithMetrics(enabled bool) ReplicaOption
WithMetrics enables or disables Prometheus database metrics.
func WithMetricsNamespace ¶
func WithMetricsNamespace(namespace string) ReplicaOption
WithMetricsNamespace sets the Prometheus namespace used by database metrics. An empty namespace means that metric names have no namespace prefix.
type TracedTx ¶
type TracedTx struct {
// contains filtered or unexported fields
}
TracedTx wraps a sql.Tx to add tracing to all database operations within a transaction
func (*TracedTx) ExecContext ¶
ExecContext executes a SQL statement within the transaction with tracing
func (*TracedTx) PrepareContext ¶
PrepareContext prepares a SQL statement within the transaction with tracing
func (*TracedTx) QueryContext ¶
QueryContext executes a SQL query within the transaction with tracing
func (*TracedTx) QueryRowContext ¶
QueryRowContext executes a SQL query that returns a single row within the transaction with tracing