Files
popertotsandSisyphus 08c8828bb0 refactor(db): set UTC timezone in pool config and add Querier docs
Set UTC timezone in pgxpool config to ensure consistent timestamp handling. Add detailed doc comment to Querier interface clarifying QueryRow vs Querier distinction.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:42:49 +01:00

98 lines
3.8 KiB
Go

package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
// Querier is implemented by *pgxpool.Pool, pgx.Tx, and *PoolProxy.
// Fixture functions and internal helpers that need to run queries should
// accept Querier to remain decoupled from transaction state.
//
// DISTINCTION: QueryRow vs Querier
// - QueryRow() (lowercase 'r') is a METHOD on Querier. It returns pgx.Row.
// Call it like: row := q.QueryRow(ctx, sql, args...)
// - Querier (uppercase 'Q') is an INTERFACE. It declares the QueryRow method.
// Accept Querier when a helper must work with both pool + transactions.
// - pgx.Row (singular) is the RETURN TYPE of QueryRow(). It is NOT a Querier.
// pgx.Row only has Scan(). You cannot pass a pgx.Row where Querier is expected.
//
// Common mistake:
// // WRONG — pgx.Row does not implement Querier:
// func helper(ctx, row pgx.Row) { ... }
// // CORRECT — accept Querier, call QueryRow inside:
// func helper(ctx, q db.Querier) { row := q.QueryRow(ctx, sql, args...); ... }
type Querier interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
// PoolProxy wraps *pgxpool.Pool and routes DB operations through an active
// transaction stored in context.Context. If no transaction is present,
// it delegates to the underlying pool directly.
//
// This enables per-test transactions: tests store a pgx.Tx in the request
// context, and all handler DB calls (via db.Conn.Exec/Query/QueryRow) route
// through that transaction automatically, rolling back on test cleanup.
type PoolProxy struct {
pool *pgxpool.Pool
}
// NewPoolProxy creates a PoolProxy wrapping the given pool.
func NewPoolProxy(pool *pgxpool.Pool) *PoolProxy {
return &PoolProxy{pool: pool}
}
// Pool returns the underlying pool, used for test setup and startup code.
func (p *PoolProxy) Pool() *pgxpool.Pool { return p.pool }
// Exec runs a query, routing through a context transaction if one is active.
func (p *PoolProxy) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
if tx := TxFromContext(ctx); tx != nil {
return tx.Exec(ctx, sql, args...)
}
return p.pool.Exec(ctx, sql, args...)
}
// Query runs a query, routing through a context transaction if one is active.
func (p *PoolProxy) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
if tx := TxFromContext(ctx); tx != nil {
return tx.Query(ctx, sql, args...)
}
return p.pool.Query(ctx, sql, args...)
}
// QueryRow runs a query, routing through a context transaction if one is active.
func (p *PoolProxy) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row {
if tx := TxFromContext(ctx); tx != nil {
return tx.QueryRow(ctx, sql, args...)
}
return p.pool.QueryRow(ctx, sql, args...)
}
// Begin starts a transaction. If the context already has an active transaction,
// it creates a savepoint (nested transaction) instead. This allows production
// code that calls db.Conn.Begin() to work inside per-test transactions.
func (p *PoolProxy) Begin(ctx context.Context) (pgx.Tx, error) {
if tx := TxFromContext(ctx); tx != nil {
return tx.Begin(ctx)
}
return p.pool.Begin(ctx)
}
// Ping always goes to the underlying pool — it's a health check, not a query.
func (p *PoolProxy) Ping(ctx context.Context) error {
return p.pool.Ping(ctx)
}
// Acquire always goes to the underlying pool — pgx.Tx has no Acquire method.
// The only production caller is the payment advisory lock, which needs a
// dedicated connection orthogonal to any transaction context.
func (p *PoolProxy) Acquire(ctx context.Context) (*pgxpool.Conn, error) {
return p.pool.Acquire(ctx)
}