Documentation
¶
Overview ¶
Package migrate provides the platform's standard database.Migrator: embedded SQL migrations with the operational discipline consumers otherwise hand-roll — an instance-based provider (no global goose state, so parallel tests never race), and on Postgres a session advisory lock that serializes concurrently booting replicas, with probe timeouts tightened so a waiting replica notices the winner promptly instead of goose's leisurely default. Lock and unlock timeouts are configurable via WithLockTimeout and WithUnlockTimeout.
Writing a migration ¶
Put plain SQL in a numbered file — 00001_add_users.sql — and embed the directory. The leading number orders migrations and must be unique. That is the whole contract: the `-- +goose Up` annotation is inserted for you if you omit it, so nothing in a routine migration has to name the migration library.
Only Up is ever applied; this package exposes no Down, so a Down section is inert if present. goose's remaining annotations still work when you need them: fence a statement whose body contains semicolons (a PL/pgSQL function, a DO block) between `-- +goose StatementBegin` and `-- +goose StatementEnd`, or the splitter will tear it apart. New rejects an unfenced dollar-quoted body rather than let that happen quietly. `-- +goose NO TRANSACTION` and `-- +goose ENVSUB` also work, and stay above the inserted annotation.
Migrations are read and checked once, when New is called, so a malformed file fails construction rather than the first Migrate.
Generated migrations ¶
A platform package that owns a table can render its own DDL, and WithGeneratedMigration splices that text into the sequence as if it were a file — so the table is created by your normal migration run instead of by DDL copied into your repository. outbox/migrations is the first of these. The version stays yours to pick, because numbering belongs to whoever owns the sequence; a version a file on disk already uses fails New rather than the first Migrate.
Locking ¶
The advisory lock ID is derived from a caller-supplied lock key: deployments sharing a database serialize on the same key, while schema-isolated parallel tests pass distinct keys (their schema name) and migrate concurrently instead of queueing on a global ID.
Wire it through database/config by passing the Migrator to NewDatabase with RunMigrations enabled, or call Migrate directly with any *sql.DB.
Index ¶
- Constants
- type Migrator
- type Option
- func WithGeneratedMigration(version uint64, name, body string) Option
- func WithLockKey(key string) Option
- func WithLockTimeout(probeInterval, timeout time.Duration) Option
- func WithLogger(logger logging.Logger) Option
- func WithMetricsProvider(metricsProvider metrics.Provider) Option
- func WithTracerProvider(tracerProvider tracing.TracerProvider) Option
- func WithUnlockTimeout(probeInterval, timeout time.Duration) Option
- func WithoutLock() Option
Constants ¶
const ( // DefaultLockProbeInterval is how often a waiting process re-checks the // advisory lock. DefaultLockProbeInterval = time.Second // DefaultLockTimeout is how long Migrate waits to acquire the lock. DefaultLockTimeout = time.Minute // DefaultUnlockProbeInterval is how often a process re-tries releasing the // advisory lock. DefaultUnlockProbeInterval = time.Second // DefaultUnlockTimeout is how long Migrate waits to release the lock. DefaultUnlockTimeout = 30 * time.Second )
Lock-wait defaults. Migrate probes every second rather than goose's 5s default so a waiting replica notices the winner promptly, and gives up after a minute — long enough for a peer's migrations, short enough that a genuinely stuck lock fails the deploy instead of hanging it.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Migrator ¶
type Migrator struct {
// contains filtered or unexported fields
}
Migrator applies embedded goose SQL migrations. Construct with New; the zero value is not usable.
func New ¶
New builds a Migrator over an fs.FS of SQL migration files, usually an embed.FS subtree. Files are named like 00001_description.sql; the leading number orders them and must be unique.
The `-- +goose Up` annotation is optional: New inserts it into any file that omits one, so a migration can be plain SQL in a numbered file. Files that do carry it are left exactly as written, and goose's other annotations (StatementBegin/StatementEnd, NO TRANSACTION, ENVSUB) work as documented upstream either way. Migrations are read once, here, so a malformed one fails construction rather than the first Migrate.
Only Up sections are ever applied — nothing in this package runs a Down — so a Down section, if present, is inert.
func (*Migrator) Migrate ¶
Migrate implements database.Migrator: it applies all pending migrations, and is idempotent — an up-to-date database is a no-op. Concurrent callers against one Postgres database serialize on the session advisory lock, so racing replicas wait for the winner instead of erroring.
type Option ¶
type Option func(*Migrator)
Option configures a Migrator.
func WithGeneratedMigration ¶
WithGeneratedMigration adds a migration whose SQL comes from code rather than from a file in the migrations filesystem. It exists for platform packages that own a table and can render its DDL — outbox is the first — so a consumer does not have to copy that DDL into their repository and keep it in sync.
The version is the caller's to choose, and that is deliberate: migration numbering belongs to whoever owns the sequence, and a platform-chosen number would sooner or later collide with a consumer's. Pick one in your sequence, then never change it — goose keys applied migrations by version, so renumbering an applied migration makes it look unapplied. A version already claimed by a file on disk fails New rather than the first Migrate.
The SQL is annotated and validated exactly like a file would be, so it may contain several statements separated by semicolons:
ddl, err := outboxmigrations.SQL(dialect.Postgres, outbox.DefaultTableName) // ... m, err := migrate.New(dialect.Postgres, myMigrations, migrate.WithGeneratedMigration(37, "create_outbox_messages", ddl), )
func WithLockKey ¶
WithLockKey partitions the Postgres advisory lock ID. Deployments sharing a database should share a key (the default empty key is fine); schema-isolated parallel tests pass their schema name so they migrate concurrently instead of queueing on one global lock.
func WithLockTimeout ¶
WithLockTimeout sets how long Migrate waits to acquire the Postgres advisory lock, re-checking every probeInterval. Raise the timeout for a deployment whose migrations legitimately run longer than a minute, so replicas queued behind the winner do not give up mid-deploy.
goose measures the probe interval in whole seconds, so probeInterval must be a positive whole number of seconds and timeout must be at least one probe interval; New rejects anything else. Defaults are DefaultLockProbeInterval and DefaultLockTimeout.
func WithLogger ¶
WithLogger attaches a logger. Goose's own progress output is routed through it too, so migration logs are structured and attributable instead of going to the standard library's global logger.
func WithMetricsProvider ¶
WithMetricsProvider attaches a metrics provider, enabling the database_migrator_* instruments.
func WithTracerProvider ¶
func WithTracerProvider(tracerProvider tracing.TracerProvider) Option
WithTracerProvider attaches a tracer provider. Migrate is worth tracing: it is typically the longest blocking step in service startup, and on Postgres it can spend up to a minute waiting on the advisory lock behind a peer that is migrating.
func WithUnlockTimeout ¶
WithUnlockTimeout sets how long Migrate waits to release the Postgres advisory lock, re-trying every probeInterval. It carries the same whole-seconds constraint as WithLockTimeout. Defaults are DefaultUnlockProbeInterval and DefaultUnlockTimeout.
func WithoutLock ¶
func WithoutLock() Option
WithoutLock disables the Postgres session advisory lock. Only safe when exactly one process can be migrating at a time.