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
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
const NamePrefix = "_test_"NamePrefix is inserted between the base name and the timestamp.
#RandLen
const RandLen = 8RandLen is the number of random characters in the suffix.
#RandCharset
const RandCharset = "abcdefghijklmnopqrstuvwxyz0123456789"RandCharset is the character set for the random suffix.
#MaxNameLen
const MaxNameLen = 63MaxNameLen is PostgreSQL's maximum identifier length.
#SuffixLen
const SuffixLen = 25SuffixLen is the fixed-length suffix: _test_ (6) + timestamp (10) + _ (1) + random (8) = 25
#TemplateFS
var TemplateFS embed.FSgo:embed templates/*.tmpl
#CITemplateFS
var CITemplateFS embed.FSgo:embed templates/ci/*.tmpl
#Barrier
type Barrier structBarrier 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
type PartmanInfo structPartmanInfo holds metadata about a detected pg_partman installation.
#CITemplateOptions
type CITemplateOptions structCITemplateOptions configures CI template rendering beyond basic placeholders.
#Manager
type Manager structManager manages ephemeral test database lifecycles.
#EphemeralDB
type EphemeralDB structEphemeralDB represents a created ephemeral test database.
#CreateOptions
type CreateOptions structCreateOptions configures how an ephemeral database is created.
#NewBarrier
func NewBarrier() *BarrierNewBarrier creates an unarmed barrier.
#DatabaseURL
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
func RequireDB() boolRequireDB reports whether this lane has declared that a database must exist.
#RequirePartman
func RequirePartman() boolRequirePartman reports whether this lane has declared that pg_partman must be available.
#RequireTCPLanes
func RequireTCPLanes() boolRequireTCPLanes 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
func RequireURL(t testing.TB) stringRequireURL 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
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
func MainNoDatabase(cause error) intMainNoDatabase 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
func RunWithCluster(run func() int) intRunWithCluster 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
func CreateInvalidIndex(ctx context.Context, conn *pgx.Conn, indexName, table, column string) errorCreateInvalidIndex 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
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
func RequireManager(t testing.TB) *ManagerRequireManager 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
func RequireEphemeralDB(t testing.TB) *EphemeralDBRequireEphemeralDB 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
func RequireConn(t testing.TB, ctx context.Context) *pgx.ConnRequireConn 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
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
func MainFailed(cause error) intMainFailed 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
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
func SkipIfNoPartman(t testing.TB) *PartmanInfoSkipIfNoPartman 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
func SupportedLanguages() []stringSupportedLanguages returns the list of supported language names.
#RenderTemplate
func RenderTemplate(lang, ddlPath, baseURL, baseName string) ([]byte, error)RenderTemplate reads a template for the given language and substitutes placeholders.
#WrapperOutputPath
func WrapperOutputPath(lang string) stringWrapperOutputPath returns the conventional path for a language's test wrapper.
#RenderCITemplate
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
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
func GenerateName(baseName string) stringGenerateName creates an ephemeral database name from a base name.
#ParseName
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
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
func (b *Barrier) WaitArrived()WaitArrived blocks until the blocked party has called Arrive.
#Barrier.Release
func (b *Barrier) Release()Release unblocks the party waiting in Arrive. It must be called exactly once.
#CreateOptions.Validate
func (o CreateOptions) Validate() errorValidate checks that CreateOptions is valid.
#Manager.Create
func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*EphemeralDB, error)Create creates a new ephemeral test database.
#Manager.Drop
func (m *Manager) Drop(ctx context.Context, db *EphemeralDB) errorDrop destroys an ephemeral test database and closes tracked connections.
#Manager.DropByName
func (m *Manager) DropByName(ctx context.Context, name string) errorDropByName 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
func (db *EphemeralDB) Connect(ctx context.Context) (*pgx.Conn, error)Connect opens a tracked connection to the ephemeral database.
#EphemeralDB.Pool
func (db *EphemeralDB) Pool(ctx context.Context) (*pgxpool.Pool, error)Pool opens a tracked connection pool to the ephemeral database.
#Manager.ApplyDDL
func (m *Manager) ApplyDDL(ctx context.Context, dbName string, ddl io.Reader) errorApplyDDL connects to the named database and executes DDL statements from the reader.
#Manager.ListOrphans
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
func (m *Manager) SetupForTest(t testing.TB, opts CreateOptions) *EphemeralDBSetupForTest creates an ephemeral database for a test and registers cleanup.