Documentation
¶
Overview ¶
Package compat reads the contracts a migration declares about itself: that it is BACKWARD-COMPATIBLE, or deliberately BREAKS ROLLBACK (this file), and which foreign migration history a baseline ADOPTS (adopt.go).
THE CONTRACT. A migration's .up.sql may carry the line
-- forge:backward-compatible
which is its author's statement that the PREVIOUS release's code keeps working against the schema this migration leaves behind — the "expand" half of expand/contract. An ADD COLUMN that old code never reads is backward-compatible; a DROP COLUMN old code still SELECTs is not.
WHY A DECLARATION AND NOT AN INFERENCE. Whether N-1 code survives a schema is a fact about that code, not about the SQL, and nothing short of running it can prove it. An inference from the SQL would be wrong in both directions: `DROP TABLE` of a table nothing reads is safe, and `ADD COLUMN ... NOT NULL` without a default breaks every old INSERT. So the author, who knows, states it once, in the file, in review — and forge enforces what follows from it: the migrator records the declared versions in the database as it applies them, so an OLDER binary that meets a schema ahead of it — the previous release's pods, still serving or rescheduled, during every rolling deploy — can prove the extra versions are safe rather than hoping (see migratekit.SchemaAheadError).
WHY THIS IS ITS OWN PACKAGE. It is a comment parser every project's migrator links; keeping it free of golang-migrate lets any tool read the declaration without that dependency, so it imports nothing but the standard library.
Index ¶
Constants ¶
const ( LockTimeoutDirective = "-- forge:lock-timeout" StatementTimeoutDirective = "-- forge:statement-timeout" )
LockTimeoutDirective and StatementTimeoutDirective are the lines a migration carries to override the migrator's timeouts for that one file:
-- forge:lock-timeout 30s -- forge:statement-timeout 2h — backfills 40M rows
The value is a Go duration (`500ms`, `30s`, `5m`, `1h30m`), or `0` / `off` to disable the timeout for the file. A rationale may follow the value. Exported so remediation text quotes the spelling the parser matches.
const AdoptsDirective = "-- forge:adopts"
AdoptsDirective is the line a baseline carries to declare the history it replaces, followed by the tool and its versions. Exported so the linter, errors and docs quote the spelling the parser matches.
const BreaksRollbackDirective = "-- forge:breaks-rollback"
BreaksRollbackDirective is the line a migration carries to declare the opposite: the PREVIOUS release does NOT keep working on the schema this migration leaves — a dropped or renamed column old code reads, a NOT NULL column old code does not write. It takes a reason on the same line:
-- forge:breaks-rollback the 2026-09 release stopped reading plans.legacy_tier
The migrator treats it exactly as it treats a migration that declares nothing (an older binary refuses to serve over it). What it adds is that the author SAID so, in review, with why: a project that requires every migration to declare itself (`database.migration_safety.require_compat_declaration`) can tell a deliberate break from a forgotten declaration.
const Directive = "-- forge:backward-compatible"
Directive is the exact line a migration carries to declare itself backward-compatible. Exported so remediation text, the linter and the scaffolded comment all quote the same spelling the parser matches.
const ToolForge = "forge"
ToolForge names forge's own migrator: a history `forge db squash` replaced, recorded in schema_migrations and schema_migrations_applied.
const ToolGoose = "goose"
ToolGoose names pressly/goose, whose history lives in goose_db_version.
Variables ¶
This section is empty.
Functions ¶
func Declared ¶
Declared reports whether one migration's up SQL declares itself backward-compatible. A file that ALSO declares breaks-rollback contradicts itself, and is read the safe way: not compatible, so an older binary refuses to serve over it rather than trusting half of the claim.
func ParseTimeout ¶ added in v1.0.7
ParseTimeout reads one timeout value: a positive Go duration of at least a millisecond, or `0` / `off` for "no timeout" (returned as 0). It is the grammar of both the directives and the `forge db migrate up` flags.
Types ¶
type Adoption ¶
type Adoption struct {
// Tool is the migration tool whose history this replaces (ToolGoose).
Tool string
// Versions is every version that tool recorded for the history this
// migration replaces: ascending, distinct, each positive.
Versions []int64
// Line is the 1-based line of the first directive, for messages.
Line int
}
Adoption is the foreign history one migration declares it replaces.
func ParseAdoption ¶
ParseAdoption reads the adoption one migration's up SQL declares. No directive is (nil, nil): almost every migration adopts nothing.
The grammar is strict, and deliberately so. Every database the binary meets parses it at boot — a fresh test database included — so a malformed list fails the first CI run instead of waiting for the one production database it was written for.
type AdoptionSyntaxError ¶
AdoptionSyntaxError is a malformed adoption directive. It carries the line so the linter can point at it; the migrator refuses the whole migration set, because a baseline whose adoption cannot be read would run its SQL against a database that already has the schema.
func (*AdoptionSyntaxError) Error ¶
func (e *AdoptionSyntaxError) Error() string
type Declaration ¶ added in v0.1.46
type Declaration struct {
// Line is the 1-based line it is on.
Line int
// Reason is the text after the directive, with the separator a reader
// types (`—`, `-`, `:`) trimmed; "" when there is none. A reason that
// starts with `<` is a template placeholder (`<why …>`), not a reason,
// and reads as "".
Reason string
}
Declaration is one compatibility directive line in a migration.
func Declarations ¶ added in v0.1.46
func Declarations(upSQL string) (compatible, breaksRollback []Declaration)
Declarations returns every backward-compatible and every breaks-rollback line in one migration's up SQL. A file is meant to carry exactly one of the two; the linter reports anything else.
type Migration ¶
type Migration struct {
// Version is the numeric prefix — the value golang-migrate records in
// schema_migrations.
Version uint
// Name is the filename, for messages.
Name string
// Descriptor is the filename between the version prefix and `.up.` —
// `retire_plan_compute_free` in `00091_retire_plan_compute_free.up.sql`.
// It is what identifies WHICH migration a version is: two branches that
// both claim 91 differ here, while a change of zero-padding does not.
Descriptor string
// BackwardCompatible is whether the file carries Directive (and not
// BreaksRollbackDirective as well; see Declared).
BackwardCompatible bool
// BreaksRollback is whether the file carries BreaksRollbackDirective.
BreaksRollback bool
// Adopts is the foreign history this migration replaces, from its
// AdoptsDirective lines; nil when it declares none. See adopt.go.
Adopts *Adoption
// Timeouts is the file's lock_timeout / statement_timeout override, from
// its TimeoutDirectives lines; the zero value declares none. See
// timeouts.go.
Timeouts Timeouts
}
Migration is one up migration's version and what it declares.
func Scan ¶
Scan reads every up migration in dir of fsys, in version order.
A directory holding no up migrations returns an empty slice, not an error: a project that has not written one yet is a normal state. An unreadable directory IS an error, because "no migrations" and "could not look" call for different fixes.
type TimeoutSyntaxError ¶ added in v1.0.7
TimeoutSyntaxError is a malformed timeout directive. It carries the line so the migrator's message can point at it.
func (*TimeoutSyntaxError) Error ¶ added in v1.0.7
func (e *TimeoutSyntaxError) Error() string
type Timeouts ¶ added in v1.0.7
Timeouts is the per-file override one migration's up SQL declares. A nil field is "not declared", which is not the same as a declared 0: 0 disables the timeout for that file.
func ParseTimeouts ¶ added in v1.0.7
ParseTimeouts reads the timeout overrides one migration's up SQL declares. No directive is the zero Timeouts and no error: almost every migration declares none.
The grammar is strict for the same reason ParseAdoption's is: the migrator parses every embedded file at boot, so a malformed or doubled directive fails the first run that migrates a fresh database instead of waiting for the production table it was written for.