Documentation
¶
Index ¶
- Constants
- func EscapeLike(s string) string
- func IsCheckViolation(err error) bool
- func IsForeignKeyViolation(err error) bool
- func IsUniqueViolation(err error) bool
- func Livemode(ctx context.Context) bool
- func NewID(prefix string) string
- func NullableFloat64(v *float64) any
- func NullableString(v string) any
- func NullableStringPtr(v *string) any
- func NullableTime(v *time.Time) any
- func Rollback(tx *sql.Tx)
- func UniqueViolationConstraint(err error) string
- func WithLivemode(ctx context.Context, live bool) context.Context
- func WithRequiredLivemode(ctx context.Context) context.Context
- type AdvisoryLock
- type DB
- func (db *DB) BeginTx(ctx context.Context, mode TxMode, tenantID string) (*sql.Tx, error)
- func (db *DB) TryAdvisoryLock(ctx context.Context, key int64) (*AdvisoryLock, bool, error)
- func (db *DB) VerifyAdvisoryLockTopology(ctx context.Context) error
- func (db *DB) WithTenantTx(ctx context.Context, tenantID string, fn func(tx *sql.Tx) error) error
- type StringArray
- type TxMode
Constants ¶
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.
const ( DefaultQueryTimeout = 5 * time.Second DefaultMigrationTimeout = 60 * time.Second )
Variables ¶
This section is empty.
Functions ¶
func EscapeLike ¶
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 ¶
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 IsUniqueViolation ¶
func Livemode ¶
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 ¶
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 NullableString ¶
func NullableStringPtr ¶
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 UniqueViolationConstraint ¶
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 ¶
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 ¶
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 ¶
DB wraps *sql.DB with query timeout and RLS-aware transactions.
func (*DB) TryAdvisoryLock ¶
func (*DB) VerifyAdvisoryLockTopology ¶
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):
- Same pinned conn must keep one backend PID across statements — a PID flip mid-conn is transaction pooling, full stop.
- 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 ¶
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.