pgtest

package
v0.1.45 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package pgtest provides a REAL ephemeral postgres for generate-time schema introspection and for hermetic tests that need a database.

Why real postgres

forge is postgres-pinned. The schema a project's migrations declare — schema-qualified DDL (CREATE TABLE controlplane.foo), postgres-only types (TIMESTAMPTZ, JSONB, TEXT[], BIGSERIAL), '::type' casts, multi-ADD ALTERs, generated/identity columns — only round-trips faithfully on real postgres. The previous in-memory SQLite "shadow" approximated this and froze the ORM the moment a project used a construct SQLite couldn't parse (the controlplane.-schema bug). Real postgres needs no normalization: migrations apply verbatim.

The shared instance

Booting postgres is expensive, so this package boots ONE server per process (sync.Once) and hands every caller its own freshly-created, uniquely-named database on that server. Databases are cheap; the server boot is the cost, paid once. This mirrors how the e2e corpus builds the forge binary once via sync.Once and runs fixtures in parallel against it.

By default the server is an embedded-postgres binary (github.com/fergusstrange/embedded-postgres) — a real postgres downloaded and cached under the user cache dir on first use, no Docker required. Set FORGE_TEST_POSTGRES_URL to a base postgres DSN (postgres://user:pass@host:port/postgres?sslmode=disable) to point at an already-running server instead (a dev docker-compose, CI service container, or a detected local postgres) and skip the embedded boot.

Index

Constants

View Source
const EnvBaseURL = "FORGE_TEST_POSTGRES_URL"

EnvBaseURL names the env var that, when set, points pgtest at an already-running postgres instead of booting embedded-postgres. The value is a base DSN whose database is "postgres" (the maintenance DB pgtest connects to in order to CREATE DATABASE).

Variables

This section is empty.

Functions

func AcquireShared

func AcquireShared() (baseURL string, release func(), err error)

AcquireShared returns the base maintenance DSN of the postgres server this process should use for scratch databases, plus a release that must be called when the process is done with it.

Resolution: an explicit FORGE_TEST_POSTGRES_URL wins (external server, no teardown responsibility); otherwise the cross-process shared embedded instance is booted or reused. Multiple acquisitions in one process share a single underlying reference — the LAST release performs the cross-process detach (and teardown when this is the last live user across all processes).

It is additive to the pgtest API. New/NewURL use it internally, which is what makes a plain `go test ./...` fan-out share ONE instance across its per-package binaries — no orchestrator required. (`forge test` used to call it directly to pre-resolve a server and export the DSN to its subprocesses; with that command gone the in-process path is the only caller, and it gives the same sharing.)

func CanReach

func CanReach(baseURL string) bool

CanReach reports whether a postgres server is reachable at baseURL with working credentials — a fast open/ping/close with a short timeout. It creates and mutates NOTHING; a malformed URL or any connect/ping failure (server down, wrong password, missing role) returns false. Callers use it to pick among candidate servers before committing one to NewAtURL, so an unreachable or wrong-credential candidate is skipped rather than turned into a hard CREATE DATABASE error.

func EnsureDatabase

func EnsureDatabase(appDSN string) error

EnsureDatabase makes the application database named in appDSN exist, idempotently — the RUNTIME counterpart to NewAtURL's throwaway scratch database. Where NewAtURL creates a uniquely-named database and DROPS it on cleanup (generate-time schema introspection), EnsureDatabase creates the ONE named database the app will actually run against and NEVER drops it: a fresh `forge env up` against a scaffolded dev DSN would otherwise die with `FATAL: database "<project>" does not exist (SQLSTATE 3D000)` because nothing had issued CREATE DATABASE.

It reuses the SAME maintenance-DB mechanism as NewAtURL: appDSN is reduced to its SERVER coordinates against the maintenance database "postgres" (the app's own database is never opened), a short maintenance connection is opened there (openBase), and CREATE DATABASE is issued only when the target is absent. A concurrent creator winning the race between the existence check and the CREATE (duplicate_database, SQLSTATE 42P04) is a no-op, not an error — the post-condition (the database exists) holds either way.

The maintenance connection FAILING (server down, wrong credentials) is a HARD error: the app cannot boot without it, so the caller should surface it loudly rather than let the app fail later with an opaque connect error. appDSN that will not parse, names no host, or names no database is likewise an error.

func New

func New() (*sql.DB, func(), error)

New creates a fresh, uniquely-named, empty database on the shared server and returns an open *sql.DB connected to it plus a cleanup function that closes the connection and drops the database. The first call boots the shared server (embedded download on the very first run of a new machine).

Callers own the returned cleanup; tests typically defer it or register it with t.Cleanup. The connection is configured for postgres (database/sql driver "postgres").

func NewAtURL

func NewAtURL(baseURL string) (*sql.DB, func(), error)

NewAtURL creates a fresh, uniquely-named, empty database on the ALREADY-RUNNING postgres server addressed by baseURL and returns an open *sql.DB connected to it plus a cleanup that drops it. It is the "give me a scratch database on THIS server" primitive: unlike New it never boots embedded-postgres and never consults FORGE_TEST_POSTGRES_URL — the CALLER has already resolved which server to use (that policy lives above this package; pgtest stays a dumb ephemeral-postgres utility).

baseURL is a DSN whose database is the maintenance DB (conventionally "postgres"): NewAtURL connects there to CREATE the scratch database and never touches whatever application database also lives on the server. The scratch database is dropped-if-exists BEFORE creating it, so a leftover from a crashed prior run (same pid+counter) can never wedge a fresh run; cleanup drops it again.

Callers own the returned cleanup; the generate pipeline defers it.

func NewURL

func NewURL() (dsn string, cleanup func(), err error)

NewURL creates a fresh, uniquely-named, empty database on the shared server like New, but returns its connection DSN (a postgres:// URL) instead of an open *sql.DB. Use this when the consumer is a separate process that connects itself via DATABASE_URL — e.g. an e2e test that boots a generated server. The returned cleanup drops the database; the caller must not hold connections past it.

func RecreateDatabase

func RecreateDatabase(appDSN string) error

RecreateDatabase DROPs the application database named in appDSN and creates it again, empty — the destructive counterpart to EnsureDatabase, and the mechanism behind `forge db reset`.

The three functions here now span the lifecycle, and the difference between them is what they do to EXISTING data. NewAtURL makes a uniquely-named scratch database and drops it on cleanup (generate-time introspection). EnsureDatabase creates the ONE named database only when it is absent and never drops it (runtime boot). RecreateDatabase drops it WHETHER OR NOT it exists and creates it fresh: the post-condition is an empty database, which is exactly what a caller recovering from a wedged migration state needs — there is no dirty flag to clear and no offending row to repair when the database itself is new.

It reuses the same maintenance-DB mechanism as the other two: appDSN is reduced to its SERVER coordinates against the maintenance database "postgres" (the target is never opened, which is required — postgres will not drop a database you are connected to), and the name is quoted as an identifier so a project called "control-plane" is dropped verbatim rather than parsed as SQL.

Lingering backends on the target are terminated first. A connection held by a running dev server makes DROP DATABASE block indefinitely, and a reset that hangs with no explanation is worse than one that fails loudly.

THIS DESTROYS DATA AND HAS NO GUARD OF ITS OWN. Deciding that the target is a scratch dev database — that the environment is development, that the DSN is the one that environment declares, and that a human confirmed it — is the CALLER's job, and forge's CLI does all three before calling this. As with everything in pgtest, this is the dumb mechanic; the policy lives above it.

func ReserveLoopbackPort

func ReserveLoopbackPort() (int, error)

ReserveLoopbackPort binds an ephemeral loopback TCP port, reads the number the OS assigned, and releases it.

Exported because every harness that has to hand a concrete port to a process it is about to spawn needs exactly this: embedded-postgres here, and the service binary a generated e2e harness starts. Without it each harness re-implements the same bind-:0/read/close trick.

The port is FREE at the moment of return, not reserved — the caller must bind it promptly and tolerate the (small) race where another process claims it in between. There is no portable way to close that race; an OS-assigned ephemeral port is still far better than a hardcoded one.

func Shutdown

func Shutdown()

Shutdown releases this process's shared-server reference: it closes the maintenance connection and detaches from the cross-process pool. When this is the LAST live user of the shared embedded instance, the detach tears the server down cleanly (stop + remove data dir + release IPC). It is a cheap no-op when this process never opened a database, and is idempotent.

The forge CLI defers Shutdown so every `forge generate` process cleans up its share on exit; ordinary `go test` binaries that never call it are backstopped by reapStaleInstances. (`forge test` used to be the other deferring caller. It is gone — projects run their suite through `task test` — so the per-package `go test` binaries a suite spawns now reach the pool directly, which is the path reapStaleInstances covers.)

func StartEmbedded

StartEmbedded builds and starts an embedded postgres from cfg, fetching its binary (on a cold cache) through a retrying relay. A download failure comes back as a *FetchError naming the URL and the upstream status, never as the library's "no version found".

cfg must not set BinaryRepositoryURL: the relay takes that slot and forwards to Maven Central, the library's default.

Types

type FetchError

type FetchError struct {
	// URL is the upstream artifact the library asked for.
	URL string
	// Status is the last HTTP status the repository returned, 0 when the
	// request never got a response (DNS, connection refused, TLS, timeout).
	Status int
	// Attempts is how many times forge tried.
	Attempts int
	// Err is the last transport error, when Status is 0.
	Err error
}

FetchError is a postgres binary download that failed. It replaces the library's "no version found matching X", which it returns for any non-200.

func (*FetchError) Error

func (e *FetchError) Error() string

func (*FetchError) Unwrap

func (e *FetchError) Unwrap() error

Jump to

Keyboard shortcuts

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