introspect

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package introspect reads a Postgres schema out of pg_catalog and returns the same *schema.Registry the DSL produces.

That symmetry is the point. A registry from here can be handed to migrate.Diff as the current state, which makes generating a migration and adopting an existing database the same machinery pointed in opposite directions (ADR-0014). It is also the only way to check the diff engine against a real database: render a schema to DDL, apply it, read it back, and the diff between what went in and what came out must be empty.

Why this is not in migrate

migrate does not connect to a database and says so in its own documentation: it produces files, and a runner applies them. This package does connect, so it is separate, and migrate stays a pure function over two data structures.

The connection

Everything here works through a sqlb.Executor, so a pool, a connection or a transaction the caller already holds all work, and reading a catalog uses the same handle as querying a table (ADR-0040).

What cannot be represented

The DSL is narrower than Postgres, and the failure that matters is dropping something quietly — a schema that looks complete, describes the database incorrectly, and produces a migration that reverses work nobody meant to reverse. So every construct this cannot express is collected into a Report rather than skipped. Read it. A Report with entries means the emitted schema is not the whole database.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Options

type Options struct {
	// Schema is the Postgres schema to read. Defaults to "public".
	Schema string

	// Module, when set, produces a module registry (schema.NewModule) and
	// strips the module's prefix from every table name it finds, so that a
	// database whose tables are called billing_invoices imports as a module
	// named billing holding a table called invoices.
	//
	// Tables without the prefix are left alone and reported, since a module
	// registry would silently rename them on the way back out.
	Module string

	// Only limits the import to the named tables. Empty reads everything,
	// which is what an import of a database sqlb is taking over wants.
	//
	// A drift gate wants the opposite. An incremental adoption declares a
	// handful of tables while the database holds dozens, and diffing a
	// declaration of five tables against an import of sixty-nine reports the
	// other sixty-four as tables to drop — so the gate has to narrow one side,
	// and this is where it is narrowed (issue #54). Names are storage names,
	// before any module prefix is stripped, because that is what the database
	// calls them.
	//
	// A named table that is not in the database is reported rather than
	// ignored: a typo in this list would otherwise silently shrink what the
	// gate checks, which is the one failure a gate must not have.
	Only []string

	// Exclude drops the named tables from the import. It applies after Only,
	// so the two compose — read this schema except the queue tables — and it
	// is the right shape for the migration-history table every runner keeps,
	// which no declaration will ever describe.
	Exclude []string
}

Options control what is read and how it is named.

type Report

type Report struct {
	Skipped []Skip

	// Notes records a construct that survived, but not in the shape the
	// database holds it — a decision this package made rather than a gap it
	// left. A foreign key on an import-breaking cycle is the case: it is
	// imported as the enforced ExternalRef a declaration would also be forced
	// to write, which is faithful, but the reader should know which side was
	// chosen.
	//
	// Deliberately not Skipped. Empty and Err are about whether the registry
	// describes the database, and a note does not change that answer — putting
	// one there would fail a round-trip that is in fact clean.
	Notes []string

	// Extensions is every non-plpgsql extension the database has installed.
	//
	// It is here because an extension is *invisible* to this package's normal
	// contract rather than skipped by it. A construct the DSL cannot express
	// gets a Skip and the reader reconciles it; an extension was never read at
	// all, so a clean Report and a clean Diff both claimed everything was
	// represented about a schema that could not be created — the DDL naming
	// uuid_generate_v4() applies only where uuid-ossp already exists.
	//
	// The failure that produced this field surfaced at the furthest possible
	// point from its cause and in the wrong vocabulary: 228 identical "function
	// uuid_generate_v4() does not exist" errors, one per dependent table, with
	// the actual cause — two missing extensions — named nowhere (issue #115).
	//
	// Deliberately not Skipped, for the same reason Notes is not: Empty and Err
	// answer whether the registry describes the *tables*, and every adoption
	// using pgvector would otherwise report a gap it cannot close. Diff still
	// renders no CREATE EXTENSION; this is the list to create first.
	Extensions []string
}

Report collects everything in the database that the DSL cannot express.

It exists because the dangerous failure here is a quiet one. A schema missing a construct still compiles, still validates, and still produces a migration — one that proposes undoing whatever it failed to see. ADR-0014 names silently dropping as the failure mode to watch for hardest, so nothing is dropped without an entry here.

An empty Report means the registry describes the database completely. A non-empty one means it does not, and the difference is yours to reconcile.

func Registry

func Registry(ctx context.Context, db sqlb.Executor, opts Options) (*schema.Registry, *Report, error)

Registry reads the database and returns the schema it describes, along with a Report of everything that could not be represented.

The registry is validated before it is returned: a registry that does not validate would produce DDL for a schema that cannot exist, and finding that out here beats finding it out from a migration.

Capabilities are not inferred. Nothing in a database says which columns should be filterable or exposed over REST, and guessing would publish columns nobody chose to publish — so everything imports with no capabilities at all and widening them is a deliberate, reviewable edit (ADR-0014).

func (*Report) Empty

func (r *Report) Empty() bool

Empty reports whether everything in the database was represented.

func (*Report) Err

func (r *Report) Err() error

Err returns an error describing everything skipped, or nil if nothing was.

It is offered rather than returned from Registry because whether an unrepresentable construct is fatal depends on what you are doing. Adopting a database wants to see the list and carry the remainder over by hand; round-tripping a schema this package generated wants any entry at all to be a failure.

func (*Report) String

func (r *Report) String() string

type Skip

type Skip struct {
	Table  string
	Object string // the constraint, index or column it concerns
	Reason string
	// Def is the definition Postgres reports, so the construct can be carried
	// across by hand without going back to the database to look it up.
	Def string
}

Skip is one construct that did not survive the import, where it was, and what to do about it.

Jump to

Keyboard shortcuts

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