pgdesign v0.26.0 /internal/testdb
On this page

Package testdb provides ephemeral test database management, CI workflow generation with optional partman support, and test skip guards.

#internal/testdb

#internal/testdb

#ConnectionEnv

Go go
const ConnectionEnv = "PGDESIGN_DB"

ConnectionEnv is the single environment variable every database-backed test resolves its connection string from. It is the same variable the CLI registers with strictcli's WithConnectionEnv, so a test and the binary it exercises can never disagree about where the database is.

#NamePrefix

Go go
const NamePrefix = "_test_"

NamePrefix is inserted between the base name and the timestamp.

#RandLen

Go go
const RandLen = 8

RandLen is the number of random characters in the suffix.

#RandCharset

Go go
const RandCharset = "abcdefghijklmnopqrstuvwxyz0123456789"

RandCharset is the character set for the random suffix.

#MaxNameLen

Go go
const MaxNameLen = 63

MaxNameLen is PostgreSQL's maximum identifier length.

#SuffixLen

Go go
const SuffixLen = 25

SuffixLen is the fixed-length suffix: _test_ (6) + timestamp (10) + _ (1) + random (8) = 25

#TemplateFS

Go go
var TemplateFS embed.FS

go:embed templates/*.tmpl

#CITemplateFS

Go go
var CITemplateFS embed.FS

go:embed templates/ci/*.tmpl

#Barrier

Go go
type Barrier struct

Barrier is an in-process rendezvous for coordinating two goroutines around a critical section — used by migration tests that must observe a state WHILE another goroutine holds it (e.g. "attempt apply while an upgrade transaction is open and the advisory lock is held").

The blocked party calls Arrive from inside the critical section: it signals its arrival (unblocking WaitArrived) and then waits until Release is called. The observer calls WaitArrived to know the critical section is entered, performs its assertion, then calls Release to let the blocked party proceed. This is a pure sync primitive — no subprocess kills, no backend-kill machinery.

#PartmanInfo

Go go
type PartmanInfo struct

PartmanInfo holds metadata about a detected pg_partman installation.

#CITemplateOptions

Go go
type CITemplateOptions struct

CITemplateOptions configures CI template rendering beyond basic placeholders.

#Manager

Go go
type Manager struct

Manager manages ephemeral test database lifecycles.

#EphemeralDB

Go go
type EphemeralDB struct

EphemeralDB represents a created ephemeral test database.

#CreateOptions

Go go
type CreateOptions struct

CreateOptions configures how an ephemeral database is created.

#NewBarrier

Go go
func NewBarrier() *Barrier

NewBarrier creates an unarmed barrier.

#DatabaseURL

Go go
func DatabaseURL() (string, bool)

DatabaseURL reports the configured connection string and whether one exists.

There is deliberately NO default. A suite with no PGDESIGN_DB has no database and every database-backed test skips: it never probes localhost, so it can never reach, write to, or drop databases from a PostgreSQL server that happens to be running on the developer's own machine. The database a test runs against is either one the caller named or one the suite booted for itself -- never one it stumbled into.

#RequireDB

Go go
func RequireDB() bool

RequireDB reports whether this lane has declared that a database must exist.

#RequirePartman

Go go
func RequirePartman() bool

RequirePartman reports whether this lane has declared that pg_partman must be available.

#RequireTCPLanes

Go go
func RequireTCPLanes() bool

RequireTCPLanes reports whether this lane has declared that the JDBC conformance lanes must run -- that is, that the database it points at is reachable over TCP and not only over a unix socket.

#RequireURL

Go go
func RequireURL(t testing.TB) string

RequireURL returns the connection string for a test that needs a database, skipping t when there is none (or failing it under PGDESIGN_REQUIRE_DB=1).

It is the TB-bound counterpart of [DatabaseURL]: the resolution and the verdict live in one place, so no package can grow its own idea of where the database is.

#SkipIfNoTCPHost

Go go
func SkipIfNoTCPHost(t testing.TB, dbURL string)

SkipIfNoTCPHost skips t when dbURL reaches PostgreSQL over a unix socket rather than over a host and port.

It exists for the JDBC lanes and for nothing else. pgjdbc has no unix-socket transport at all, so a generated Java or Kotlin wrapper cannot reach the ephemeral cluster this suite boots -- the cluster listens on a socket and deliberately opens no TCP port. That is a property of the driver, not a defect in the generated wrapper, which says the same thing in its own toJdbcUrl. Point PGDESIGN_DB at a TCP-reachable server to run these lanes.

When PGDESIGN_REQUIRE_TCP_LANES=1 the skip becomes a hard failure, the same way PGDESIGN_REQUIRE_DB and PGDESIGN_REQUIRE_PARTMAN work. Without it these two lanes were the only database-backed tests in the suite that no lane could declare, so a regression in the generated Java or Kotlin wrapper stayed invisible even in a run that required everything else.

#MainNoDatabase

Go go
func MainNoDatabase(cause error) int

MainNoDatabase is the exit code for a TestMain that has no database to give its tests: 0, which skips the binary, unless PGDESIGN_REQUIRE_DB=1 declares that this lane must have one -- then it is 1, because a lane that provisions PostgreSQL and silently runs no database tests is the failure this whole arrangement exists to prevent.

cause explains what went wrong, and is nil when the DSN was simply never set.

#RunWithCluster

Go go
func RunWithCluster(run func() int) int

RunWithCluster boots one ephemeral PostgreSQL cluster for the whole test binary, exports its base URL under PGDESIGN_DB, calls run, and shuts the cluster down again. It returns run's exit code, so a TestMain is:

func TestMain(m *testing.M) { os.Exit(testdb.RunWithCluster(func() int { return m.Run() })) }

The cluster is a real postmaster on a private tmpfs directory listening on a unix socket and nothing else. It inherits the host's extension library, so pg_partman and pgvector are available exactly when the machine has them installed -- which is what the CI lane provisions instead of a service container.

Three cases, and no fourth:

- PGDESIGN_DB is already set. The caller named a server; that choice is never second-guessed and no cluster is booted. - PostgreSQL is not usable on this machine. PGDESIGN_DB stays unset and every database-backed test skips -- unless PGDESIGN_REQUIRE_DB=1, which makes it a hard failure. - The cluster fails to start for any other reason. That is a hard failure, never a quiet fall back to skipping.

#CreateInvalidIndex

Go go
func CreateInvalidIndex(ctx context.Context, conn *pgx.Conn, indexName, table, column string) error

CreateInvalidIndex deterministically leaves an INVALID index (pg_index.indisvalid = false) named indexName on table(column), modeling the catalog state after an interrupted CREATE INDEX CONCURRENTLY — with NO backend kill, no SIGKILL, no faulttest machinery.

The technique is unique-CIC-over-duplicate-data: table(column) must already contain duplicate values, and CREATE UNIQUE INDEX CONCURRENTLY builds the index in two phases; the second (validation) phase detects the duplicate and fails, but Postgres leaves the half-built index in the catalog marked invalid. This is exactly the recoverable state the create-index resume protocol must handle (pg_index.indisvalid check + DROP-rebuild, roadmap L8).

conn must be in autocommit mode (CONCURRENTLY cannot run inside a transaction). The expected duplicate-key failure is swallowed; any OTHER error (including the build unexpectedly succeeding, or the index ending up valid) is returned so a mis-set-up fixture fails loudly rather than silently.

#Unavailable

Go go
func Unavailable(t testing.TB, format string, args ...any)

Unavailable delivers the verdict for a database that WAS named but could not be used: the server refused the dial, the URL would not parse, a manager could not be built from it. It skips t normally and FAILS t under PGDESIGN_REQUIRE_DB=1.

It exists because "resolve the DSN through RequireURL, then t.Skipf on the first probe failure" was the shape most database-backed packages grew independently, and it silently defeated the require gate: the lane declared that a database must exist, RequireURL agreed one was named, and the test skipped anyway the moment the server did not answer -- so a provisioning regression produced a green run full of skips, which is precisely what the require gate exists to prevent. Every failure downstream of DSN resolution goes through here, so the skip-or-fail decision lives in ONE place.

#RequireManager

Go go
func RequireManager(t testing.TB) *Manager

RequireManager returns an ephemeral-database Manager for the configured server, having first proved the server answers.

It is the one way a test obtains a Manager: no package builds one from a raw [NewManager] call, so no package can decide on its own that a broken database is a reason to skip. Absent DSN -> [RequireURL]'s verdict; named-but-unusable -> [Unavailable]'s verdict.

#RequireEphemeralDB

Go go
func RequireEphemeralDB(t testing.TB) *EphemeralDB

RequireEphemeralDB creates a throwaway database for t and registers its teardown. It is [RequireManager] followed by [Manager.SetupForTest], which is what nearly every database-backed test actually wants.

#RequireConn

Go go
func RequireConn(t testing.TB, ctx context.Context) *pgx.Conn

RequireConn opens a connection to the configured database itself (not to a throwaway one) and closes it when t finishes.

Tests that need to touch the server directly -- resetting a schema, running a fixture as the maintenance user -- use this instead of reading the connection env and dialing by hand, so the absent/unusable verdicts are the same ones every other database-backed test gets.

#MainManager

Go go
func MainManager() (mgr *Manager, code int, ok bool)

MainManager resolves the database for a whole test binary and returns a Manager for it. It is the TestMain-level counterpart of [RequireManager], where there is no testing.TB to skip or fail and the verdict has to be an exit code.

Three outcomes, and no fourth:

- No DSN at all. mgr is nil, ok is false, and code is [MainNoDatabase]'s: 0 so the binary's DB-free tests can still run, or 1 under PGDESIGN_REQUIRE_DB=1. An absent database is the ONLY legitimate reason a binary runs no database tests. - A DSN that cannot be turned into a working Manager. ok is false and code is 1, ALWAYS -- regardless of PGDESIGN_REQUIRE_DB. A named database that does not work is a broken lane, not an absent one; "best-effort setup, skip everything on failure" is exactly how a green run with zero database coverage happens, and it is banned here. - A working Manager. mgr is non-nil, ok is true, code is 0.

#MainFailed

Go go
func MainFailed(cause error) int

MainFailed is the exit code for a TestMain whose database setup FAILED after a DSN was resolved: 1, unconditionally. It is the counterpart of [MainNoDatabase], which reports the one benign case (no DSN at all); every other setup outcome is a failure and never a skip.

#SkipIfNoPostgres

Go go
func SkipIfNoPostgres(t testing.TB)

SkipIfNoPostgres skips the test unless a PostgreSQL server has been named.

The connection string comes from PGDESIGN_DB and from nowhere else: there is no default target. A test binary that wants a database of its own boots one in TestMain through [RunWithCluster], which exports the ephemeral cluster's DSN under the same variable. With neither, the test skips -- it does not go looking for a server on localhost, so this suite can never connect to, create databases in, or drop databases from whatever PostgreSQL a developer happens to be running.

When the PGDESIGN_REQUIRE_DB=1 environment variable is set, the test fails instead of skipping. This converts a silent skip into a hard failure, which is what CI lanes that provision PostgreSQL declare.

#SkipIfNoPartman

Go go
func SkipIfNoPartman(t testing.TB) *PartmanInfo

SkipIfNoPartman skips the test if pg_partman is not available in the PostgreSQL server. It probes pg_available_extensions for the pg_partman extension and records the detected version. This is separate from SkipIfNoPostgres: a CI lane can have Postgres without partman.

The DSN is resolved exactly as [SkipIfNoPostgres] resolves it: PGDESIGN_DB or nothing. An ephemeral cluster inherits the host's extension library, so pg_partman is available to it precisely when the machine has the package installed -- which is what the CI lane provisions on the runner host.

When the PGDESIGN_REQUIRE_PARTMAN=1 environment variable is set, the test fails instead of skipping.

SkipIfNoPartman does NOT call SkipIfNoPostgres internally -- callers should call both guards if they need both checks.

#SupportedLanguages

Go go
func SupportedLanguages() []string

SupportedLanguages returns the list of supported language names.

#RenderTemplate

Go go
func RenderTemplate(lang, ddlPath, baseURL, baseName string) ([]byte, error)

RenderTemplate reads a template for the given language and substitutes placeholders.

#WrapperOutputPath

Go go
func WrapperOutputPath(lang string) string

WrapperOutputPath returns the conventional path for a language's test wrapper.

#RenderCITemplate

Go go
func RenderCITemplate(provider, pgVersion string, languages []string, opts CITemplateOptions) ([]byte, error)

RenderCITemplate reads a CI workflow template for the given provider and substitutes placeholders. Only "github-actions" is supported.

#NewManager

Go go
func NewManager(baseURL string) (*Manager, error)

NewManager creates a Manager from a base database URL. The base URL identifies the Postgres server and provides credentials. The database name from the URL becomes the base for ephemeral DB names.

#GenerateName

Go go
func GenerateName(baseName string) string

GenerateName creates an ephemeral database name from a base name.

#ParseName

Go go
func ParseName(name string) (baseName string, created time.Time, random string, ok bool)

ParseName extracts the base name, timestamp, and random suffix from an ephemeral DB name.

#Barrier.Arrive

Go go
func (b *Barrier) Arrive()

Arrive signals that the blocked party has reached the barrier, then blocks until Release is called. It must be called exactly once.

#Barrier.WaitArrived

Go go
func (b *Barrier) WaitArrived()

WaitArrived blocks until the blocked party has called Arrive.

#Barrier.Release

Go go
func (b *Barrier) Release()

Release unblocks the party waiting in Arrive. It must be called exactly once.

#CreateOptions.Validate

Go go
func (o CreateOptions) Validate() error

Validate checks that CreateOptions is valid.

#Manager.Create

Go go
func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*EphemeralDB, error)

Create creates a new ephemeral test database.

#Manager.Drop

Go go
func (m *Manager) Drop(ctx context.Context, db *EphemeralDB) error

Drop destroys an ephemeral test database and closes tracked connections.

#Manager.DropByName

Go go
func (m *Manager) DropByName(ctx context.Context, name string) error

DropByName drops an ephemeral database by name without requiring a full EphemeralDB struct. This is useful for CLI teardown and GC operations where the database was not created by this Manager instance.

#EphemeralDB.Connect

Go go
func (db *EphemeralDB) Connect(ctx context.Context) (*pgx.Conn, error)

Connect opens a tracked connection to the ephemeral database.

#EphemeralDB.Pool

Go go
func (db *EphemeralDB) Pool(ctx context.Context) (*pgxpool.Pool, error)

Pool opens a tracked connection pool to the ephemeral database.

#Manager.ApplyDDL

Go go
func (m *Manager) ApplyDDL(ctx context.Context, dbName string, ddl io.Reader) error

ApplyDDL connects to the named database and executes DDL statements from the reader.

#Manager.ListOrphans

Go go
func (m *Manager) ListOrphans(ctx context.Context, olderThan time.Duration) ([]*EphemeralDB, error)

ListOrphans finds ephemeral databases that were created longer than olderThan ago.

#Manager.SetupForTest

Go go
func (m *Manager) SetupForTest(t testing.TB, opts CreateOptions) *EphemeralDB

SetupForTest creates an ephemeral database for a test and registers cleanup.

Search