Documentation
¶
Overview ¶
Package migrations runs schema changes: the Migration contract, the registry they declare themselves in, the migrator that applies them and the repository table that records what ran.
A migration does not run at boot ¶
`aru migrate` is a step of the deployment pipeline, run once, by one process. Called from the start-up path of an application with N replicas it becomes N migrators racing over one table, and the ones that lose report a duplicate key on a table they were in the middle of creating. Nothing in this package makes that convenient: there is no MigrateOnBoot, and there will not be one.
The other half belongs to the migration, and no code can check it: every migration is compatible with the binary that is still serving traffic while the rollout finishes. A new column is nullable or carries a default, because the old binary's INSERT does not mention it. Removing a column takes two releases -- the first stops writing it, the second drops it -- because the old binary's SELECT still names it. A rename is an add, a backfill, and a drop, which is three releases.
Discovery: a registry, not a directory ¶
A package nothing imports is not in the binary, so a scan of a migrations directory at run time would find files the compiler never saw, and in a deployed container would find nothing at all, because the source is not there.
So a migration registers itself:
func init() { migrations.Register(CreateUsersTable{}) }
and main.go blank-imports the package they live in. That import is the loading step, moved to link time, with the compiler checking that every registered migration implements Migration before anything runs. Register carries the argument in full.
The alternative considered and rejected was embedding the directory and treating a migration as SQL text. It reads well until the first migration that has to read rows before it writes -- a backfill, a conditional drop -- and then there are two kinds of migration.
Order comes from the name and from nothing else: GetName answers "2026_08_11_000000_create_users_table", the registry sorts by that string, and two machines therefore apply the same migrations in the same order.
There is no Grant here ¶
Every path to application rows carries an auth.Grant and filters by auth.Tenant(g), on reads as much as on writes. A migration is not such a path: it is DDL, run by a pipeline step, in a process with no request and no subject -- there is nothing a Grant could be built from, and inventing a subject to satisfy a signature would be worse than not having one. The repository table is framework metadata, not tenant data, for the same reason.
Neither the migrator nor the creator holds a filesystem: the migrator reads the registry rather than a directory, and the creator's stubs are string constants rather than files, which is also why `aru make:migration` works from any working directory.
Index ¶
- Constants
- func DownStatements(ctx context.Context, migration Migration) ([]string, error)
- func IsolationLockName(connection string) string
- func Register(migration Migration, path ...string)
- func RegisteredPaths() []string
- func ResolveConnectionsUsing(callback func(resolver Resolver, name string) (Connection, error))
- func UpStatements(ctx context.Context, migration Migration) ([]string, error)
- func WithoutMigrations(names []string)
- type BaseMigration
- type Connection
- type DatabaseMigrationRepository
- func (r *DatabaseMigrationRepository) CreateRepository(ctx context.Context) error
- func (r *DatabaseMigrationRepository) Delete(ctx context.Context, migration MigrationRecord) error
- func (r *DatabaseMigrationRepository) DeleteRepository(ctx context.Context) error
- func (r *DatabaseMigrationRepository) GetConnection() (Connection, error)
- func (r *DatabaseMigrationRepository) GetConnectionResolver() Resolver
- func (r *DatabaseMigrationRepository) GetLast(ctx context.Context) ([]MigrationRecord, error)
- func (r *DatabaseMigrationRepository) GetLastBatchNumber(ctx context.Context) (int, error)
- func (r *DatabaseMigrationRepository) GetMigrationBatches(ctx context.Context) (map[string]int, error)
- func (r *DatabaseMigrationRepository) GetMigrations(ctx context.Context, steps int) ([]MigrationRecord, error)
- func (r *DatabaseMigrationRepository) GetMigrationsByBatch(ctx context.Context, batch int) ([]MigrationRecord, error)
- func (r *DatabaseMigrationRepository) GetNextBatchNumber(ctx context.Context) (int, error)
- func (r *DatabaseMigrationRepository) GetRan(ctx context.Context) ([]string, error)
- func (r *DatabaseMigrationRepository) GetTable() string
- func (r *DatabaseMigrationRepository) Log(ctx context.Context, file string, batch int) error
- func (r *DatabaseMigrationRepository) RepositoryExists(ctx context.Context) bool
- func (r *DatabaseMigrationRepository) SetSource(name string)
- type Dispatcher
- type IsolationLock
- type Migration
- type MigrationCreator
- func (c *MigrationCreator) AfterCreate(callback func(table, path string))
- func (c *MigrationCreator) Create(name, path, table string, create bool) (string, error)
- func (c *MigrationCreator) GetClassName(name string) string
- func (c *MigrationCreator) GetDatePrefix() string
- func (c *MigrationCreator) GetPath(name, path string) string
- func (c *MigrationCreator) StubPath() string
- type MigrationRecord
- type MigrationRepositoryInterface
- type MigrationResult
- type Migrator
- func (m *Migrator) DeleteRepository(ctx context.Context) error
- func (m *Migrator) FireMigrationEvent(event any)
- func (m *Migrator) GetConnection() string
- func (m *Migrator) GetMigrationFiles(paths []string) map[string]Migration
- func (m *Migrator) GetMigrationName(path string) string
- func (m *Migrator) GetRepository() MigrationRepositoryInterface
- func (m *Migrator) HasRunAnyMigrations(ctx context.Context) bool
- func (m *Migrator) IsolateWith(issue func(name string) IsolationLock) *Migrator
- func (m *Migrator) Path(path string)
- func (m *Migrator) Paths() []string
- func (m *Migrator) RepositoryExists(ctx context.Context) bool
- func (m *Migrator) Reset(ctx context.Context, paths []string, pretend bool) ([]string, error)
- func (m *Migrator) Resolve(name string) (Migration, error)
- func (m *Migrator) ResolveConnection(connection string) (Connection, error)
- func (m *Migrator) Rollback(ctx context.Context, paths []string, options Options) ([]string, error)
- func (m *Migrator) Run(ctx context.Context, paths []string, options Options) ([]string, error)
- func (m *Migrator) RunIsolated(ctx context.Context, paths []string, options Options) (applied []string, ran bool, err error)
- func (m *Migrator) RunPending(ctx context.Context, migrations []Migration, options Options) error
- func (m *Migrator) SetConnection(name string)
- func (m *Migrator) SetOutput(output io.Writer) *Migrator
- func (m *Migrator) UsingConnection(name string, callback func() error) error
- type Options
- type PretendingConnection
- type Resolver
- type ReversibleMigration
- type TransactionalConnection
Constants ¶
const DefaultPath = "database/migrations"
DefaultPath is the group a migration lands in when it registers without naming one.
It is spelled like a path so that `aru migrate --path=...` takes something a person recognises. Nothing opens it: it is a key.
const DefaultTable = "migrations"
DefaultTable is the table name to pass to NewDatabaseMigrationRepository unless a project has a reason not to.
Variables ¶
This section is empty.
Functions ¶
func DownStatements ¶ added in v0.5.0
DownStatements returns the statements a migration's Down would run, without running any of them. A migration that is not reversible returns none.
func IsolationLockName ¶ added in v0.6.0
IsolationLockName is the name of the lock an isolated run against connection takes.
The connection is in the name, so two databases migrate at the same time and two processes pointed at one database do not. Pass the resolved name rather than the string a flag carried, or an unset --database and the name it resolves to become two locks over one schema.
The name carries no tenant. A lock per tenant would let N replicas each migrate for a different tenant at once, which is the problem and not the solution, and a schema belongs to no single tenant anyway.
func Register ¶
Register records a migration so the Migrator can find it.
Why discovery is a registry and not a directory scan ¶
A package that nothing imports is not in the binary at all, so a directory scan at run time would find files the compiler never saw -- and, worse, would find nothing at all in a deployed binary, where the source directory does not exist. `aru migrate` running against a scratch container with no repository checked out is the normal case, not the exotic one.
The two candidates were an embed of the directory and a registry filled from init(). Embedding would mean the migration is data -- SQL text -- which reads well until the first migration that has to backfill a column by reading rows, and then there are two kinds of migration. So:
func init() { migrations.Register(CreateUsersTable{}) }
and main.go blank-imports the package the migrations live in. That import is the loading step, moved to link time -- with the compiler checking, before anything runs, that every registered migration actually implements Migration.
Registering the same name twice panics rather than picking one. Two migrations under one name is a copied file somebody forgot to rename, and it would apply one of them and record the other.
func RegisteredPaths ¶
func RegisteredPaths() []string
RegisteredPaths answers the groups something has registered under, sorted.
`aru migrate --path=` reads it to say what the choices are, which is the difference between a usable error and "no migrations found".
func ResolveConnectionsUsing ¶
func ResolveConnectionsUsing(callback func(resolver Resolver, name string) (Connection, error))
ResolveConnectionsUsing registers the callback ResolveConnection uses to resolve a connection.
func UpStatements ¶ added in v0.5.0
UpStatements returns the statements a migration's Up would run, without running any of them.
It is what a generator writes to a file and what a test asserts on: both need the text a migration would send, and neither has a server. A migration that reads before it writes sees no rows here, because Select returns none, so what comes back is the path taken over an empty result.
func WithoutMigrations ¶
func WithoutMigrations(names []string)
WithoutMigrations sets names to leave pending however many times migrate runs.
It is package-level rather than a method because the test suite sets it once for a process.
Types ¶
type BaseMigration ¶
type BaseMigration struct {
// Connection is the connection to run on, empty for the default. It is
// exported because a migration sets it directly.
Connection string
// OutsideTransaction inverts WithinTransaction.
//
// The conventional default for running inside a transaction is true, and
// a Go bool defaults to false, so carrying the name over directly would
// have made "I did not think about this" mean "do not wrap this in a
// transaction" -- the opposite of the intended default. The flag is
// inverted so the zero value keeps the safe default, and the name says
// which way it points. Set it for the statement an engine refuses inside
// a transaction: CREATE INDEX CONCURRENTLY is the one that bites first.
OutsideTransaction bool
}
BaseMigration is the half of a Migration that is the same for every one of them: the connection name, the guard, and whether it runs in a transaction.
A migration embeds it and writes GetName and Up:
type CreateUsersTable struct{ migrations.BaseMigration }
func (CreateUsersTable) GetName() string {
return "2026_08_11_000000_create_users_table"
}
func (CreateUsersTable) Up(ctx context.Context, conn migrations.Connection) error {
_, err := conn.Statement(ctx, `CREATE TABLE users (...)`, nil)
return err
}
func (BaseMigration) GetConnection ¶
func (m BaseMigration) GetConnection() string
GetConnection returns m.Connection.
func (BaseMigration) ShouldRun ¶
func (m BaseMigration) ShouldRun() bool
ShouldRun returns true unless a migration overrides it to say otherwise.
func (BaseMigration) WithinTransaction ¶
func (m BaseMigration) WithinTransaction() bool
WithinTransaction returns the opposite of m.OutsideTransaction.
type Connection ¶
type Connection interface {
// GetName returns the connection's name.
GetName() string
// Statement runs a statement that returns neither rows nor a count,
// which is every DDL statement there is.
Statement(ctx context.Context, query string, bindings []any) (bool, error)
// Select runs a query, for the migration that has to read before it
// writes -- a backfill, a check that a column is empty before dropping
// it.
Select(ctx context.Context, query string, bindings []any) ([]map[string]any, error)
}
Connection is what a migration runs against.
It is narrowed to the two calls a schema change makes. The interface is declared here rather than imported because the database package resolves migrations, and naming the concrete type would close the cycle.
There is no auth.Grant on it, and that is a decision rather than an oversight. The path to application rows is the one that needs one: every List, Find, Get, Paginate, report and export goes through a Policy and filters by auth.Tenant(g). A migration is not on that path -- it is DDL, run by `aru migrate` as a pipeline step, in a process with no request, no subject and therefore nothing a Grant could be built from. Giving one to a migration would mean inventing a fake subject, which is worse than not having one.
type DatabaseMigrationRepository ¶
type DatabaseMigrationRepository struct {
// contains filtered or unexported fields
}
DatabaseMigrationRepository is the table that remembers which migrations have run.
Its queries are written out as SQL rather than built, because that is what this framework does everywhere else and because the four statements involved are the same on all three engines.
func NewDatabaseMigrationRepository ¶
func NewDatabaseMigrationRepository(resolver Resolver, table string) *DatabaseMigrationRepository
NewDatabaseMigrationRepository creates a DatabaseMigrationRepository.
func (*DatabaseMigrationRepository) CreateRepository ¶
func (r *DatabaseMigrationRepository) CreateRepository(ctx context.Context) error
CreateRepository creates the migrations table.
The column types are the portable spellings: INTEGER and VARCHAR(255). VARCHAR rather than TEXT because migration is what every query here filters on and MySQL refuses a TEXT column in a key without a prefix length -- the mistake that once stopped `aru migrate` on its own first statement.
func (*DatabaseMigrationRepository) Delete ¶
func (r *DatabaseMigrationRepository) Delete(ctx context.Context, migration MigrationRecord) error
Delete forgets that migration ran.
func (*DatabaseMigrationRepository) DeleteRepository ¶
func (r *DatabaseMigrationRepository) DeleteRepository(ctx context.Context) error
DeleteRepository drops the migrations table.
func (*DatabaseMigrationRepository) GetConnection ¶
func (r *DatabaseMigrationRepository) GetConnection() (Connection, error)
GetConnection returns the connection the repository reads and writes the table on.
func (*DatabaseMigrationRepository) GetConnectionResolver ¶
func (r *DatabaseMigrationRepository) GetConnectionResolver() Resolver
GetConnectionResolver returns the resolver the repository reaches connections through.
func (*DatabaseMigrationRepository) GetLast ¶
func (r *DatabaseMigrationRepository) GetLast(ctx context.Context) ([]MigrationRecord, error)
GetLast returns the most recent batch, in the order a rollback wants it.
func (*DatabaseMigrationRepository) GetLastBatchNumber ¶
func (r *DatabaseMigrationRepository) GetLastBatchNumber(ctx context.Context) (int, error)
GetLastBatchNumber returns the highest batch number recorded, or zero for an empty table.
It reads every row and takes the max in Go rather than asking the engine for MAX(batch), which avoids a NULL on an empty table and a driver that scans NULL into an int. The table never holds more than a few hundred rows, so reading them is not a cost worth avoiding.
func (*DatabaseMigrationRepository) GetMigrationBatches ¶
func (r *DatabaseMigrationRepository) GetMigrationBatches(ctx context.Context) (map[string]int, error)
GetMigrationBatches returns the name-to-batch map `migrate:status` prints.
func (*DatabaseMigrationRepository) GetMigrations ¶
func (r *DatabaseMigrationRepository) GetMigrations(ctx context.Context, steps int) ([]MigrationRecord, error)
GetMigrations returns the last steps applied, newest first.
The `batch >= 1` filter is not redundant: a batch of zero is what `migrate --pretend` and hand-written rows leave behind, and neither should be rolled back.
func (*DatabaseMigrationRepository) GetMigrationsByBatch ¶
func (r *DatabaseMigrationRepository) GetMigrationsByBatch(ctx context.Context, batch int) ([]MigrationRecord, error)
GetMigrationsByBatch returns every migration recorded under batch, newest first.
func (*DatabaseMigrationRepository) GetNextBatchNumber ¶
func (r *DatabaseMigrationRepository) GetNextBatchNumber(ctx context.Context) (int, error)
GetNextBatchNumber returns one past the highest batch number recorded.
func (*DatabaseMigrationRepository) GetRan ¶
func (r *DatabaseMigrationRepository) GetRan(ctx context.Context) ([]string, error)
GetRan returns the names of every applied migration, ordered by batch and then by name.
func (*DatabaseMigrationRepository) GetTable ¶
func (r *DatabaseMigrationRepository) GetTable() string
GetTable is the table name the repository was built with, exported because a caller in another package cannot reach the field directly.
func (*DatabaseMigrationRepository) Log ¶
Log records that a migration ran, in a batch.
The id is computed rather than left to the engine: the three engines spell auto-increment three different ways -- SERIAL, AUTO_INCREMENT, AUTOINCREMENT -- so a portable CREATE TABLE cannot have one. Nothing reads the id for anything but order, and migrations run one at a time in a pipeline step, so max + 1 is the whole of it.
func (*DatabaseMigrationRepository) RepositoryExists ¶
func (r *DatabaseMigrationRepository) RepositoryExists(ctx context.Context) bool
RepositoryExists reports whether the migrations table exists.
It selects from the table rather than asking the engine's catalogue, which would need three different queries for three engines: a table that is not there cannot be selected from, and that works everywhere.
func (*DatabaseMigrationRepository) SetSource ¶
func (r *DatabaseMigrationRepository) SetSource(name string)
SetSource sets the connection the repository reads and writes the table on.
type Dispatcher ¶
type Dispatcher interface {
// Dispatch fires an event.
Dispatch(event any)
}
Dispatcher is where the Migrator sends the events of a run -- started, ended, one migration started, one skipped, nothing to do.
It is one method because that is all the Migrator ever does with an event dispatcher: it publishes and never subscribes. Declaring the whole contract would make anything that wants to watch a migration implement listener registration it does not use, and would make this package depend on a dispatcher rather than on the idea of one. A Migrator with none dispatches nothing and migrates the same.
type IsolationLock ¶ added in v0.6.0
type IsolationLock interface {
// Get takes the lock, runs fn if it took it, releases it afterwards, and
// reports whether it took it.
//
// A lock somebody else holds is (false, nil) and not an error: another
// process is doing the work, which is the expected answer to asking and not
// a fault.
Get(ctx context.Context, fn func(context.Context) error) (bool, error)
}
IsolationLock is one named lock, held for as long as a migration run takes.
It is declared here rather than imported for the reason Dispatcher is: the Migrator takes a lock and never issues one, so what it needs is the idea of a lock rather than a lock package.
type Migration ¶
type Migration interface {
// GetName is the migration's identity: "2026_08_11_000000_create_users_table".
//
// The date prefix is not decoration. It is what makes two machines apply
// the same migrations in the same order, and it is the whole of the
// ordering rule -- the registry sorts by this string and nothing else.
GetName() string
// Up applies the change.
Up(ctx context.Context, conn Connection) error
// GetConnection returns the connection this migration runs on, empty for
// the default.
GetConnection() string
// ShouldRun reports whether the migration should run at all. A
// migration that returns false is skipped and NOT recorded, so it is
// reconsidered on the next run.
ShouldRun() bool
// WithinTransaction reports whether the migration should run inside a
// transaction.
//
// It is a method rather than a field because Go's zero value for a bool
// is false and the conventional default is true: a struct field would
// turn every migration that did not think about it into one that runs
// unprotected.
WithinTransaction() bool
}
Migration is one schema change.
The whole surface is this interface, and BaseMigration answers the half that is the same for every migration -- embed it and only Up, Down and GetName are left to write.
GetName exists because a migration is code and code has no path to read a name off: it says its own name, and that string is what lands in the repository table.
func Registered ¶
Registered returns the migrations of the given groups, sorted by name.
It reads the registry rather than a directory, keyed by migration name and sorted by that key. No group named means every group.
type MigrationCreator ¶
type MigrationCreator struct {
// contains filtered or unexported fields
}
MigrationCreator is what `aru make:migration` writes the file with.
It fills a stub: the name takes a date prefix, the stub is chosen by whether a table was named and whether it is being created, a custom stub directory wins over the built-in one, and the post-create hooks run afterwards.
func NewMigrationCreator ¶
func NewMigrationCreator(customStubPath string) *MigrationCreator
NewMigrationCreator builds a MigrationCreator.
It holds no filesystem: the file operations involved are two calls to the standard library.
func (*MigrationCreator) AfterCreate ¶
func (c *MigrationCreator) AfterCreate(callback func(table, path string))
AfterCreate registers a hook to run after a migration file is written.
func (*MigrationCreator) Create ¶
func (c *MigrationCreator) Create(name, path, table string, create bool) (string, error)
Create writes the migration and returns the path it was written to.
table empty means a migration with no table in mind. create says whether the named table is being created or altered, which is the difference between the two table-shaped stubs.
func (*MigrationCreator) GetClassName ¶
func (c *MigrationCreator) GetClassName(name string) string
GetClassName returns the Go type name a migration file for name would declare.
It is exported here because a caller with no filesystem access -- a test, a generator that prints rather than writes -- has no other way to ask.
func (*MigrationCreator) GetDatePrefix ¶
func (c *MigrationCreator) GetDatePrefix() string
GetDatePrefix returns the current time formatted as the migration file's date prefix: "2006_01_02_150405" in Go's reference-time layout.
It is UTC rather than local: a team spread over two time zones otherwise generates prefixes that sort against the order the migrations were actually written in.
func (*MigrationCreator) GetPath ¶
func (c *MigrationCreator) GetPath(name, path string) string
GetPath returns the file path a migration for name would be written to, under path.
func (*MigrationCreator) StubPath ¶
func (c *MigrationCreator) StubPath() string
StubPath returns the custom stub directory, the only one there is a path to: the built-in stubs are string constants compiled into the binary, with no directory of their own to name.
type MigrationRecord ¶
type MigrationRecord struct {
// ID is the row's ordinal.
ID int
// Migration is the migration's name, the same string GetName answers.
Migration string
// Batch is the run it belonged to. One `aru migrate` is one batch, which
// is what makes a rollback undo a deploy rather than a single file.
Batch int
}
MigrationRecord is one row of the repository table: an id, a migration name, and the batch it ran in.
type MigrationRepositoryInterface ¶
type MigrationRepositoryInterface interface {
// GetRan answers getRan: the names of every applied migration, ordered by
// batch and then by name.
GetRan(ctx context.Context) ([]string, error)
// GetMigrations answers getMigrations: the last N applied, most recent
// first.
GetMigrations(ctx context.Context, steps int) ([]MigrationRecord, error)
// GetMigrationsByBatch answers getMigrationsByBatch.
GetMigrationsByBatch(ctx context.Context, batch int) ([]MigrationRecord, error)
// GetLast answers getLast: the whole of the most recent batch, in reverse
// order, which is the order a rollback undoes them in.
GetLast(ctx context.Context) ([]MigrationRecord, error)
// GetMigrationBatches answers getMigrationBatches: name to batch, for the
// status table.
GetMigrationBatches(ctx context.Context) (map[string]int, error)
// Log answers log: record that a migration ran.
Log(ctx context.Context, file string, batch int) error
// Delete answers delete: forget that one did.
Delete(ctx context.Context, migration MigrationRecord) error
// GetNextBatchNumber answers getNextBatchNumber.
GetNextBatchNumber(ctx context.Context) (int, error)
// CreateRepository answers createRepository: create the table.
CreateRepository(ctx context.Context) error
// RepositoryExists answers repositoryExists.
RepositoryExists(ctx context.Context) bool
// DeleteRepository answers deleteRepository: drop the table.
DeleteRepository(ctx context.Context) error
// SetSource answers setSource: the connection to read and write the table
// on.
SetSource(name string)
}
MigrationRepositoryInterface is the record of which migrations have run.
Every method returns an error, because every one of them talks to the database: a caller that cannot tell "no migrations have run" from "the connection is gone" writes a migrate command that reports success on a dead database.
type MigrationResult ¶
type MigrationResult int
MigrationResult is how one migration turned out: Success, Failure or Skipped. Its String is the word the Migrator prints beside the migration's name.
Skipped is the case worth knowing about, and it is not a failure: a migration whose ShouldRun returns false is passed over deliberately -- the guard exists for a migration that only applies to one engine, or one deployment -- and the run carries on.
const ( // Success is a migration that ran without error. Success MigrationResult = 1 // Failure is a migration that returned an error. Failure MigrationResult = 2 // Skipped is what a migration whose ShouldRun returns false gets instead // of running. Skipped MigrationResult = 3 )
The three cases of MigrationResult.
func (MigrationResult) String ¶
func (r MigrationResult) String() string
String is the label the console prints.
type Migrator ¶
type Migrator struct {
// contains filtered or unexported fields
}
Migrator runs migrations up and down and keeps the repository in step with the schema.
It does not run at boot, and this is where that is enforced ¶
`aru migrate` is a step of the deployment pipeline, never a call in the start-up path of the process. With N replicas rolling, calling Run from main means N migrators racing each other over the same table, and the one that loses reports a duplicate key on a table it was creating. There is no Migrate-on-boot helper here to make that easy, and there will not be one.
A pipeline that cannot promise it runs the step once uses RunIsolated, which takes a lock named after the connection and lets the process that does not get it finish successfully having migrated nothing.
The other half of the same rule is the migration's own: every migration is compatible with the binary that is still running while the rollout finishes. A new column is nullable or has a default; removing one takes two releases, the first stopping the writes and the second dropping the column.
func NewMigrator ¶
func NewMigrator(repository MigrationRepositoryInterface, resolver Resolver, dispatcher Dispatcher) *Migrator
NewMigrator creates a Migrator.
There is no filesystem argument: a migration is code, and the registry replaced the glob. See Register for the whole of that decision.
func (*Migrator) DeleteRepository ¶
DeleteRepository drops the migration repository's table.
func (*Migrator) FireMigrationEvent ¶
FireMigrationEvent dispatches event, if a dispatcher was given.
func (*Migrator) GetConnection ¶
GetConnection returns the default connection name.
func (*Migrator) GetMigrationFiles ¶
GetMigrationFiles answers every migration of the given paths, keyed by name.
There is nothing on disk to glob -- see Register -- so this reads the registry and keys by GetName.
func (*Migrator) GetMigrationName ¶
GetMigrationName returns the name of a migration, given either the name itself or a path that ends in it.
It still takes a path-shaped string because `aru migrate --without=` and the squashed-schema paths hand it one, and because a person copying a file name out of a log should get the right answer.
func (*Migrator) GetRepository ¶
func (m *Migrator) GetRepository() MigrationRepositoryInterface
GetRepository returns the repository migrations are recorded in.
func (*Migrator) HasRunAnyMigrations ¶
HasRunAnyMigrations reports whether the repository exists and has at least one migration recorded.
func (*Migrator) IsolateWith ¶ added in v0.6.0
func (m *Migrator) IsolateWith(issue func(name string) IsolationLock) *Migrator
IsolateWith sets where RunIsolated gets its lock, and returns the Migrator so the call can be chained onto the constructor.
The issuer is handed the lock's name and answers a lock nobody holds yet. How long that lock lives is the issuer's to decide, because it is the deadlock protection and nothing else: a process that dies mid-migration blocks every later run until its lock expires, so the duration is sized above the longest migration run there is rather than against the usual one.
migrator.IsolateWith(func(name string) migrations.IsolationLock {
return locks.Lock(name, time.Hour)
})
func (*Migrator) RepositoryExists ¶
RepositoryExists reports whether the migration repository's table exists.
func (*Migrator) Resolve ¶
Resolve returns the migration registered under name.
The registry already holds the instance, because a Go migration is registered rather than discovered -- so this is a lookup, and an unknown name is an error rather than a construction failure.
func (*Migrator) ResolveConnection ¶
func (m *Migrator) ResolveConnection(connection string) (Connection, error)
ResolveConnection returns the named connection, or the default connection when connection is empty, through the registered resolver callback when one was set.
func (*Migrator) Rollback ¶
Rollback undoes the last batch, or the batch or step count options names.
func (*Migrator) Run ¶
Run applies everything that has not been applied yet, and returns the names of what it applied.
func (*Migrator) RunIsolated ¶ added in v0.6.0
func (m *Migrator) RunIsolated(ctx context.Context, paths []string, options Options) (applied []string, ran bool, err error)
RunIsolated applies what Run applies, and only while no other process is migrating the same connection.
What the process that does not get the lock sees is the whole of the design: applied is empty, ran is false, and err is nil. It migrated nothing, and that is not a failure -- the schema is being changed by whoever got there first, and the caller's job is to carry on and let the application start. Reporting it as an error instead would fail every deployment that rolls more than one replica, which is the shape of deployment this exists to serve.
So the answer to branch on is ran and never err. A run that took the lock answers what Run answered; a run that did not answers nothing, and both are success.
A Migrator with no issuer refuses instead of migrating: a run that says it is isolated and is not is worse than one that does not start, because the failure it hides is two migrators racing over one table.
func (*Migrator) RunPending ¶
RunPending applies the given migrations, in the order they arrive.
It stops at the first failure. Applying later migrations over a schema that a failed one left half-changed turns one clear error into a database nobody can get back.
func (*Migrator) SetConnection ¶
SetConnection replaces the default connection name.
type Options ¶
type Options struct {
// Pretend prints the statements a run would execute, and runs none of
// them.
Pretend bool
// Step gives every migration its own batch on the way up, so each can be
// rolled back on its own.
Step bool
// Steps is how many migrations to roll back on the way down.
Steps int
// Batch rolls back one named batch.
Batch int
}
Options is the set of flags the Migrator's methods take.
Step serves two different purposes depending on direction: a bool on the way up (one batch per migration) and an int on the way down (how many to undo). Two fields is the same information with the ambiguity removed, and the ambiguity is worth removing -- `--step` on migrate and `--step=3` on rollback are not the same flag.
type PretendingConnection ¶
type PretendingConnection interface {
Connection
// Pretend runs callback and returns the statements it would have run,
// without executing them.
Pretend(ctx context.Context, callback func() error) ([]string, error)
}
PretendingConnection is a Connection that can run a callback without letting any of its statements reach the server.
Pretend returns the query log rather than the callback's result.
type Resolver ¶
type Resolver interface {
// Connection returns the named connection, or an error for an unknown
// name.
Connection(name string) (Connection, error)
// GetDefaultConnection returns the default connection name.
GetDefaultConnection() string
// SetDefaultConnection replaces the default connection name.
SetDefaultConnection(name string)
}
Resolver answers a connection by name, narrowed to what the repository and the Migrator ask of it.
It is declared here rather than imported for the reason every interface in this component is: the database package resolves migrations, so it imports this one, and naming its concrete resolver here would close the cycle.
type ReversibleMigration ¶
type ReversibleMigration interface {
Migration
// Down reverses what Up applied.
Down(ctx context.Context, conn Connection) error
}
ReversibleMigration is a Migration that can be rolled back.
A type assertion is what tests for it: a migration without a matching Down is simply not reversed. That happens at compile time for the migration and at run time for the Migrator -- which is strictly better than a dynamic method check, because a Down with the wrong signature is a build failure rather than a rollback that silently does nothing.
type TransactionalConnection ¶
type TransactionalConnection interface {
Connection
// SupportsSchemaTransactions reports whether the schema grammar supports
// rolling DDL back.
SupportsSchemaTransactions() bool
// Transaction runs callback inside a transaction.
Transaction(ctx context.Context, callback func() error) error
}
TransactionalConnection is a Connection that can wrap a migration in a transaction.
Two things decide whether a migration is wrapped: the schema grammar's support for transactional DDL, asked for here, and the migration's own WithinTransaction flag. A connection that does not satisfy this interface runs its migrations unwrapped -- MySQL has no transactional DDL, so a failed migration there leaves half a schema whatever anybody wants.