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) }