postgres

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Index

Constants

View Source
const (
	LockKeyBillingScheduler int64 = 76540001
	LockKeyDunningScheduler int64 = 76540002
	LockKeyOutboxDispatcher int64 = 76540003
	LockKeyEmailDispatcher  int64 = 76540004
	// LockKeyWebhookRetry gates the webhook delivery retry worker (P5):
	// the claim lease is arithmetic-sized, but leader gating makes the
	// sizing non-critical on multi-replica deploys — the same posture
	// both outbox dispatchers already take.
	LockKeyWebhookRetry int64 = 76540005
	// LockKeyBootstrap serializes tenant bootstrap (ADR-073): taken as
	// pg_advisory_xact_lock inside RunBootstrap's single tx so the
	// first-tenant existence check and the owner-email uniqueness
	// pre-check are authoritative, not check-then-insert TOCTOUs.
	LockKeyBootstrap int64 = 76540006
	// LockKeyMigrateHybrid serializes the ENTIRE hybrid migration loop
	// (ADR-073) — deliberately NOT golang-migrate's derived lock id
	// (same-id on a second session would deadlock the library's own
	// Lock()). Held for the loop's whole duration so per-iteration
	// version reads are authoritative across racing replicas.
	LockKeyMigrateHybrid int64 = 76540007
	// LockKeyTopologyCheck is used ONLY by VerifyAdvisoryLockTopology's
	// boot probe — acquired and released within the check, never held.
	LockKeyTopologyCheck int64 = 76540008
)

Reserved advisory-lock keys for singleton periodic roles. Keys are stable once deployed — changing a value lets an old binary and a new binary each acquire concurrently, which defeats the point. Values are arbitrary but namespaced high enough (>1e7) to not collide with any key the golang-migrate library picks when it hashes schema names.

View Source
const (
	DefaultQueryTimeout     = 5 * time.Second
	DefaultMigrationTimeout = 60 * time.Second
)

Variables

This section is empty.

Functions

func EscapeLike

func EscapeLike(s string) string

EscapeLike returns s with all LIKE/ILIKE metacharacters escaped. Use for every operator-typed search term that reaches an ILIKE pattern — without it, a term like "100%" matches everything and "_" matches any single character.

func IsCheckViolation

func IsCheckViolation(err error) bool

IsCheckViolation reports whether err is a Postgres check-constraint violation (SQLSTATE 23514). Used to translate DB-level invariant failures (e.g. the test_clocks livemode CHECK, or subscriptions_test_clock_requires_testmode) into user-facing 400s instead of leaking raw SQL error text.

func IsForeignKeyViolation

func IsForeignKeyViolation(err error) bool

func IsUniqueViolation

func IsUniqueViolation(err error) bool

func Livemode

func Livemode(ctx context.Context) bool

Livemode reads the livemode flag from ctx. Absent a value, defaults to true — the RLS policy interprets unset as "live mode" so background workers and bootstrap tooling that don't propagate mode operate safely against production data by default. Callers that need to know whether the value was set explicitly should use WithRequiredLivemode at their entry point instead of inspecting the return value here.

func NewID

func NewID(prefix string) string

NewID generates a prefixed, time-sortable ID (e.g., vlx_cus_cv2q6ktjml6ng3v2q0tg). Uses xid: globally unique, 20-char, URL-safe, naturally ordered by creation time.

func NullableFloat64

func NullableFloat64(v *float64) any

func NullableString

func NullableString(v string) any

func NullableStringPtr

func NullableStringPtr(v *string) any

NullableStringPtr converts a *string into a sql-friendly value: nil (becomes SQL NULL) when the pointer is nil or points at an empty string, otherwise the dereferenced value. Saves callers from juggling sql.NullString for optional reference columns.

func NullableTime

func NullableTime(v *time.Time) any

func Rollback

func Rollback(tx *sql.Tx)

func UniqueViolationConstraint

func UniqueViolationConstraint(err error) string

UniqueViolationConstraint returns the constraint name if err is a Postgres unique-violation (SQLSTATE 23505), otherwise "". Use this to disambiguate multiple unique constraints on the same table — e.g. subscriptions has both (tenant_id, code) and a partial-unique (tenant_id, customer_id, plan_id) for live statuses, and the callers need to surface distinct errors.

func WithLivemode

func WithLivemode(ctx context.Context, live bool) context.Context

WithLivemode returns a derived context carrying the mode flag. BeginTx reads this to set app.livemode on the tx session, which the RLS policy uses to filter rows by mode alongside tenant.

func WithRequiredLivemode

func WithRequiredLivemode(ctx context.Context) context.Context

WithRequiredLivemode asserts that ctx has an explicit livemode set, and returns ctx unchanged if so. Panics otherwise. Call at the top of any background worker or scheduler path that opens a TxTenant — it catches "I forgot to fan out per mode" at the fan-out site instead of 30 frames deeper, where the bug would surface as silent test-mode data loss.

Types

type AdvisoryLock

type AdvisoryLock struct {
	// contains filtered or unexported fields
}

AdvisoryLock is a held Postgres session-scoped advisory lock. Release MUST be called — preferably via defer — to free the lock and return the underlying connection to the pool. Sleeping the connection out of the pool is fine: we hold one conn per lock for as long as the tick runs (seconds).

func (*AdvisoryLock) KeepaliveSettings added in v0.3.0

func (l *AdvisoryLock) KeepaliveSettings(ctx context.Context) (idle, interval, count, windowSecs int, err error)

KeepaliveSettings reports the TCP keepalive values in force on the connection holding this lock, and the resulting worst-case seconds before Postgres reaps the session if the holder's host vanishes without closing the socket. Exported so tests can assert the detection window stays bounded — the failure it guards against is silent, so nothing else would notice a regression.

func (*AdvisoryLock) Release

func (l *AdvisoryLock) Release()

Release frees the lock and returns the connection to the pool. Uses a fresh background context so shutdown-triggered cancellation on the tick context doesn't leave the lock held until the connection ages out.

Safe to call multiple times; no-op if the lock was never acquired.

type DB

type DB struct {
	Pool         *sql.DB
	QueryTimeout time.Duration
}

DB wraps *sql.DB with query timeout and RLS-aware transactions.

func NewDB

func NewDB(pool *sql.DB, queryTimeout time.Duration) *DB

func (*DB) BeginTx

func (db *DB) BeginTx(ctx context.Context, mode TxMode, tenantID string) (*sql.Tx, error)

func (*DB) TryAdvisoryLock

func (db *DB) TryAdvisoryLock(ctx context.Context, key int64) (*AdvisoryLock, bool, error)

func (*DB) VerifyAdvisoryLockTopology

func (db *DB) VerifyAdvisoryLockTopology(ctx context.Context) error

VerifyAdvisoryLockTopology is the boot-time self-check that the connection topology actually supports session-scoped advisory locks.

Every singleton worker (billing scheduler, dunning, both outbox dispatchers, webhook retry) gates its tick on pg_try_advisory_lock held across a SESSION. Behind a transaction-mode pooler (PgBouncer transaction/statement mode, RDS Proxy with pinning disabled) each statement can run on a DIFFERENT server session: the unlock then executes on a session that doesn't hold the lock, the original server session keeps it forever, and every future tick on every replica skips as "another leader holds the lock" — billing silently halts. That failure mode produces no error anywhere, so it MUST be caught at boot, not diagnosed from a week of missing invoices.

Two probes, both deterministic-clean on direct Postgres and on session-mode PgBouncer (each client conn = one server session):

  1. Same pinned conn must keep one backend PID across statements — a PID flip mid-conn is transaction pooling, full stop.
  2. A lock held on conn A must be invisible-to-acquire on conn B, and A's unlock must return true. B acquiring A's key means both client conns share a server session; unlock=false means the unlock ran on a session that never took the lock. Either way the leader gate is broken.

A nil return does not prove the pooler is safe under load (a lucky route can hide pooling), but any error is definite misconfiguration — callers should refuse to start.

func (*DB) WithTenantTx

func (db *DB) WithTenantTx(ctx context.Context, tenantID string, fn func(tx *sql.Tx) error) error

WithTenantTx runs fn inside one tenant-scoped (TxTenant) transaction: commit when fn returns nil, rollback otherwise. It exists as the seam for cross-store coordinator transactions — a caller that must commit writes in two domain stores atomically (e.g. the billing engine's threshold fire + cycle re-anchor) passes the same *sql.Tx to each store's Tx variant. Unit tests fake the interface this satisfies with a runner that calls fn(nil); the store mocks ignore the tx handle.

type StringArray

type StringArray []string

StringArray is a []string that implements sql.Scanner and driver.Valuer for PostgreSQL TEXT[] columns.

func (*StringArray) Scan

func (a *StringArray) Scan(src interface{}) error

Scan parses a PostgreSQL TEXT[] value into a StringArray.

func (StringArray) Value

func (a StringArray) Value() (interface{}, error)

Value converts the StringArray to a PostgreSQL array literal.

type TxMode

type TxMode int

TxMode controls how a transaction sets RLS session variables.

const (
	TxTenant TxMode = iota // Sets app.tenant_id, enforces RLS
	TxBypass               // Sets app.bypass_rls = on, skips RLS
)

Jump to

Keyboard shortcuts

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