Documentation
¶
Overview ¶
Package dbaccess is the Postgres data-access layer abstraction (NFR-ARC-2): a typed query layer (pgx + sqlc) and versioned migrations (goose) behind a small Config, instead of raw provider-specific calls scattered across services. See ../README.md for a worked example of switching endpoints, and ./sqlc for the sample migration + typed queries this package ships as a reference for future services to pattern-match.
Index ¶
- Constants
- func Connect(ctx context.Context, cfg Config) (*pgxpool.Pool, error)
- func Migrate(ctx context.Context, dsn string, migrations fs.FS) error
- func MigrationsFS() fs.FS
- func UnscopedTables(ctx context.Context, pool *pgxpool.Pool, schema string, exemptTables ...string) ([]string, error)
- type Config
Constants ¶
const MigrationTimeout = 2 * time.Minute
MigrationTimeout bounds one deploy-time migration run. Each service's runMigrate applies it to the context it hands to Migrate.
WHY THE BOUND LIVES IN THE PROCESS (#551). The migrate Job used to be bounded only by its `activeDeadlineSeconds: 300`. Kubernetes starts that clock when the Job is created, so it pays for scheduling and image pull as well as the migration — and on the first deploy of a new version every Deployment in the release is rolling at once, so the node is pulling the whole image set while this Job queues behind it. On staging's v0.0.1-rc9 that was enough: `activities-migrate` died with DeadlineExceeded and failed the Helm release, Flux retried, and the same job passed in 36s — while `apiaries-migrate`, same SQL with the image already cached, took 6s. None of the 300s was ever spent migrating.
Kubernetes offers no way to exclude pull time from that clock, so the fix is not a larger number — it is to bound the thing we actually mean to bound. This deadline starts when the migration starts, so a cold deploy pays for its pull outside the budget, while a migration that hangs still fails fast and still fails the release. That last property is not incidental: #541 made a failed migration a loud, deploy-blocking event precisely because the previous design let one hide behind a healthy older ReplicaSet for 25 days.
Two minutes is roughly twenty times the slowest migration this codebase has produced (they run in seconds). Raise it deliberately, for a specific migration that genuinely needs longer — it is not a knob to widen when a deploy feels slow, and widening it no longer buys tolerance for a slow pull, which is what the old number was really being asked for.
The deadline reaches the server, not just the Go code: pgx forwards cancellation as a Postgres cancel request, so a migration stuck waiting on a lock it will never get — the realistic shape of "stuck" here — is aborted rather than left to be killed with the pod.
Variables ¶
This section is empty.
Functions ¶
func Connect ¶
Connect builds a pgx connection pool from cfg and fails fast: it pings the database before returning, so misconfiguration surfaces immediately rather than on the first query.
func Migrate ¶
Migrate applies all pending "up" migrations found in migrations (an os.DirFS or embed.FS rooted at a migrations directory) against dsn.
It goes through database/sql (not pgxpool) because that's what goose's Provider expects; the app's own queries still go through the pgxpool pool returned by Connect.
func MigrationsFS ¶
MigrationsFS returns this package's own demo migrations (see ./sqlc), rooted at the migrations directory as goose.NewProvider expects. It exists so the integration test — and any caller wanting to see the pattern in action — doesn't need to know this package's internal layout.
func UnscopedTables ¶
func UnscopedTables(ctx context.Context, pool *pgxpool.Pool, schema string, exemptTables ...string) ([]string, error)
UnscopedTables inspects every base table in schema and returns the ones that do NOT carry an organization_id column, excluding exemptTables (the documented tenancy exceptions — ADR-0002 "tenancy exception": a global identity table, or the organizations table itself, which IS the tenant root rather than something owned BY a tenant) and goose's own version table (never domain data).
This is the automated form of FR-TEN-2 / #30's AC "every owned row carries an organization_id": rather than a one-time manual audit, a service's own test suite can call this against its migrated schema so a future migration that adds an owned table without organization_id fails CI immediately, instead of depending on someone remembering to check (data-model.md §5).
Types ¶
type Config ¶
type Config struct {
Host string
Port string // defaults to "5432" if empty
User string
Password string
Database string
SSLMode string // e.g. "require", "disable"; defaults to "require"
// SearchPath, when set, is applied as the connection's schema search path.
// A least-privilege per-service role (schema-per-service, D-6) has no rights
// on `public`, so this points it at its own schema — where its tables live
// and where goose creates its version table.
SearchPath string
}
Config holds the connection details for a Postgres database. Populate it from environment/config/secrets — never hardcode credentials.
func (Config) DSN ¶
DSN renders cfg as a connection string consumable by both pgxpool (Connect) and goose's database/sql driver (Migrate).
func (Config) Validate ¶
Validate rejects a Config that must never reach DSN(). Exported because the migrate admin process (#541) needs it: it calls Migrate and then EXITS, never calling Connect, so the validation Connect performs would otherwise never run for that path at all.
That gap matters more there than in a serving process, not less — the migrate job authenticates as the database OWNER, so an injected connection option carries owner privileges. Before #541 a bad SearchPath was at least caught by Connect moments after Migrate in the same process; relatedly, see #285 on Migrate itself still taking a raw DSN rather than a Config it could validate.