Documentation
¶
Overview ¶
Package shadow builds a schema by replaying a migration history into an empty database, and reading back what the migrations actually produced.
It answers the question migrate.Diff needs answered and cannot answer itself: what is the current schema? Reading production is the obvious source and the worse one. It tells you what the database looks like, not whether the migration history produces it — so a hand-applied hotfix, a migration edited after it ran, or a statement someone skipped are all invisible, and the next generated migration is computed against a state no migration file describes (ADR-0014).
Replaying into a scratch database is a different claim: this is the schema the checked-in history builds. Comparing that with production is drift detection, and it needs no extra API — it is migrate.Diff between the two registries, and an empty result is the claim that the history and the database agree.
This is not a migration runner ¶
sqlb does not apply migrations, and this does not change that. A runner tracks which migrations have run, applies the outstanding ones to a database people depend on, and must never get it wrong. This applies all of them, in order, to an empty database nobody depends on, and throws away the result. The two have almost nothing in common except the word "apply".
What follows from that: no version table is read or written, nothing is skipped, and Down sections are never executed.
The database is yours ¶
Build takes a connection to an empty database and will not create or drop one. Creating databases needs credentials beyond what the rest of sqlb asks for, and dropping the wrong one is unrecoverable — so the destructive half of "scratch database" stays with the caller, who knows which ones are scratch.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Normalize ¶ added in v0.6.0
Normalize rewrites every CHECK expression and every partial-index predicate in reg into the spelling Postgres stores, so that reg can be compared with a registry that introspect produced.
db must be connected to a database in which reg's tables already exist — which is what the shadow database is, immediately after Build. Everything happens inside a transaction that is always rolled back, so nothing is added to that database and nothing is left behind on a failure.
It is idempotent. Normalising an expression Postgres already normalised yields the same string, so running it over a registry introspect built is safe and does nothing.
An expression that cannot be probed is left exactly as declared, and named in the returned slice. That is the right default rather than an error: a check referring to a column this migration is about to add cannot be evaluated against the table as it stands today, and it is also, necessarily, a check the diff should report as new. Failing the whole run for it would make the command unusable at the moment it is most useful.
reg is modified in place. The caller is the one holding the declared registry, and the normalised form is what it wants from here on — this is the last step before a diff.
Which is worth one warning, because the registry a project hands over is usually schema.DefaultRegistry() and that is a global. In `sqlb migrate` it does not matter: the process runs one verb and exits. In a long-lived program — or a test binary shared with tests that render DDL from the same registry — this rewrites declarations underneath them. The rewritten expression is semantically identical and Postgres stores the same thing either way, so what changes is the text, not the schema; but it does change.
Types ¶
type DB ¶ added in v0.4.0
DB is what a replay needs: statements, and a transaction to group each file's statements in. *pgxpool.Pool and *pgx.Conn both satisfy it.
type Options ¶
type Options struct {
// Dir is the migration directory. Required.
Dir string
// Format is the migration format the directory is written in. Defaults to
// migrate.Goose.
//
// A custom Format is not supported here: rendering a file and reading one
// back are different problems, and this package only knows how to read the
// three that ship.
Format migrate.Format
// Schema and Module are passed through to introspect when the replayed
// database is read back.
Schema string
Module string
// Only and Exclude are passed through too, and narrow what is read back
// rather than what is replayed. The whole history always runs — replaying a
// subset of it would build a schema no file describes, which is the failure
// this package exists to catch — so these narrow the reading only.
//
// A module adopting a few tables at a time needs both halves: the history
// builds sixty-nine tables and the declaration covers five, and without this
// the report is about the sixty-four nobody asked about.
Only []string
Exclude []string
}
Options configures a replay.
type Result ¶
type Result struct {
// Files are the migration filenames applied, in the order they ran.
Files []string
// Statements is how many statements were executed in total.
Statements int
}
Result reports what was replayed, so a failure or a surprise can be traced to a file rather than to "the migrations".
func Build ¶
func Build(ctx context.Context, db DB, opts Options) (*schema.Registry, *introspect.Report, *Result, error)
Build applies every migration in the directory to db and returns the schema they produce.
db must be connected to an **empty** database. Replaying a history onto a schema that already has tables in it produces a registry describing neither one, and the migration generated from it would be wrong in a way nothing downstream could detect — so a non-empty database is refused rather than worked around.
The introspect.Report is the same one introspect.Registry returns: a non-empty one means the replayed schema uses constructs the DSL cannot express, so the registry does not describe it completely.