Documentation
¶
Overview ¶
Package rung applies versioned SQL migrations to PostgreSQL, MySQL and MariaDB.
A migration is a pair of files, NNNNNN_name.up.sql and NNNNNN_name.down.sql. A ledger table named _migrations records which versions have been applied. Files are read from an io/fs.FS, so they can be embedded in the binary that expects them:
//go:embed postgres mysql var files embed.FS
The model ¶
Migrations are a deploy step, not something a server does to itself. The account that applies them holds DDL privileges; the account the running service connects as does not. Keeping the two apart means a bug in a request handler cannot alter the schema, and a rolling restart or a crash loop cannot change the database underneath a running instance.
A server should therefore report drift rather than fix it:
pending, err := m.Pending(ctx, db)
if len(pending) > 0 {
log.Warn("database schema is out of date; run `migrate up` before serving traffic",
"pending_versions", pending)
}
Dialects ¶
What differs between databases lives behind Dialect and nowhere else. Importing a dialect package registers it:
import (
_ "github.com/gruberchris/rung/dialect/mysql" // mysql, mariadb
_ "github.com/gruberchris/rung/dialect/postgres" // postgres, postgresql, pgx
_ "github.com/gruberchris/rung/dialect/sqlite" // sqlite, sqlite3
)
d, err := rung.For(cfg.Driver)
This package itself imports only the standard library; the database drivers come with the dialect packages, so a PostgreSQL-only program never compiles the MySQL driver into its binary.
Reporting ¶
A Migrator narrates through a Reporter rather than through a logger, so the caller decides whether a migration starting is a line of prose, a structured log record, or nothing at all. See the render package for ready implementations, and clicmd for a complete cobra command tree.
Transactions ¶
Each migration runs in one transaction together with its ledger row, so a file that fails halfway leaves no partially-built schema and no ledger row claiming it succeeded. MySQL is the caveat worth knowing: it commits implicitly on most DDL, so a migration that fails partway through several CREATE TABLEs cannot be fully rolled back there. The ledger row is still correct, because it is only written on success, so a failed migration is re-attempted rather than skipped -- which is why migration files are best written with IF NOT EXISTS.
Index ¶
- Constants
- Variables
- func Names() []string
- func RebindDollar(query string) string
- func RebindQuestion(query string) string
- func Register(d Dialect, names ...string)
- type Dialect
- type Migration
- type Migrator
- func (m *Migrator) Dialect() Dialect
- func (m *Migrator) Dir() string
- func (m *Migrator) Down(ctx context.Context, db *sql.DB) error
- func (m *Migrator) Expected() (int, error)
- func (m *Migrator) Load() ([]Migration, error)
- func (m *Migrator) Pending(ctx context.Context, db *sql.DB) ([]int, error)
- func (m *Migrator) Statuses(ctx context.Context, db *sql.DB) ([]Status, error)
- func (m *Migrator) Up(ctx context.Context, db *sql.DB, target int) error
- type Option
- type Reporter
- type Status
Constants ¶
const LedgerTable = "_migrations"
LedgerTable is the name of the table recording which migrations have been applied. It is fixed rather than configurable: it is a schema contract with every database this package has already migrated.
Variables ¶
var ErrNothingToRollback = errors.New("no migrations to roll back")
ErrNothingToRollback reports an exhausted ledger.
It is an error rather than a silent success so that a caller rolling back repeatedly -- "undo two more", "undo everything" -- has a way to know it has finished. Treating an empty ledger as success gives such a loop no termination condition.
Functions ¶
func Names ¶
func Names() []string
Names lists every registered driver name, including aliases, sorted. It is intended for error messages and help text.
func RebindDollar ¶
RebindDollar converts ? placeholders into PostgreSQL's numbered $1, $2, … form. It is exported so that third-party dialects can reuse it.
It does not parse SQL: a literal question mark inside a string literal is rewritten too. Queries with such literals should be written in the target dialect's own syntax rather than passed through Rebind.
func RebindQuestion ¶
RebindQuestion returns the query unchanged, for dialects that already use ? placeholders. It exists so that every Dialect implementation states its placeholder syntax explicitly rather than by omission.
func Register ¶
Register makes a Dialect available under one or more driver names.
It is intended to be called from a dialect package's init function, so that importing that package is what makes its names resolvable:
func init() { rung.Register(Dialect{}, "postgres", "postgresql", "pgx") }
Register panics if d is nil, if no names are given, or if a name is already registered, all of which are programming errors detectable at startup.
Types ¶
type Dialect ¶
type Dialect interface {
// Name is the canonical driver name, such as "postgres" or "mysql".
Name() string
// MigrationsDir is the directory within the file set holding this
// dialect's migrations. It is a single path element, not a path.
MigrationsDir() string
// OpenForMigrations returns a database handle able to execute a file
// containing several statements.
//
// This is deliberately separate from however an application opens its own
// pool. Both supported drivers refuse multi-statement execution by
// default, in different ways and for different reasons, and that default
// is what lets a driver reject an injected statement. Relaxing it is
// appropriate for a migration tool and never for serving traffic.
OpenForMigrations(dsn string) (*sql.DB, error)
// Rebind converts a query written with ? placeholders into this dialect's
// syntax. Queries are written once, with ?, and translated here.
Rebind(query string) string
// LedgerDDL creates the _migrations table if it does not already exist.
LedgerDDL() string
// LedgerExistsQuery reports whether the _migrations table exists, as a
// single boolean column, scoped to the connected database.
//
// The read-only paths use this because they must not create the table they
// claim only to inspect: a database with no ledger has simply had nothing
// applied.
LedgerExistsQuery() string
}
Dialect is everything that differs between the supported databases.
The rule this interface exists to enforce: nothing else may branch on which database is in use. Migration files stay in each dialect's own directory, and what genuinely diverges -- connection handling, placeholder syntax, the ledger's DDL -- lives here, selected once from a configured driver name.
Implementations must be safe for concurrent use and are expected to be stateless value types.
func For ¶
For returns the Dialect registered under a driver name.
Matching ignores case and surrounding space, and dialects register generous aliases: "postgresql" is what a deployment is likely to call it, "pgx" is the driver, and "mariadb" is what somebody running MariaDB will write even though the MySQL dialect serves it.
type Migrator ¶
type Migrator struct {
// contains filtered or unexported fields
}
Migrator applies one dialect's migrations from one file set.
A Migrator holds no database handle: the handle is passed to each call, so one Migrator can serve a short-lived command and a long-running server alike. It is safe for concurrent use.
func New ¶
New returns a Migrator reading d's migrations out of fsys.
Both d and fsys must be non-nil. The directory read is d.MigrationsDir() unless WithDir says otherwise.
func (*Migrator) Down ¶
Down rolls back the highest version recorded in the ledger.
It reports ErrNothingToRollback when the ledger is empty, so that a caller rolling back repeatedly can stop.
func (*Migrator) Expected ¶
Expected returns the highest version in the file set: the schema this build was written against. It is zero when the file set carries no migrations.
func (*Migrator) Load ¶
Load reads the file set and returns its migrations in version order.
Files are named NNNNNN_name.up.sql and NNNNNN_name.down.sql. Anything that does not parse as that is ignored, so a README or a .gitkeep alongside the migrations is harmless.
A version with only one of its two halves is skipped rather than reported: applying an up with no matching down would create a state this package cannot reverse.
func (*Migrator) Pending ¶
Pending returns the versions in the file set that have not been applied, in ascending order. It does not create the ledger, which is what makes it safe for a server to call at startup to report drift.
func (*Migrator) Statuses ¶
Statuses lists every migration in the file set with its applied state, in version order. It does not create the ledger.
func (*Migrator) Up ¶
Up applies every migration not already recorded in the ledger, in version order.
A target above zero bounds the run: migrations up to and including that version are applied and the rest are left pending. Because migrations are ordered, the first version past the target ends the run rather than being skipped -- applying a later migration while leaving an earlier one pending would produce a schema that no sequence of migrations describes.
Up is idempotent. Applying an up-to-date database is a no-op.
type Option ¶
type Option func(*Migrator)
Option configures a Migrator.
func WithDir ¶
WithDir overrides the directory migrations are read from, which defaults to the dialect's Dialect.MigrationsDir.
Use it for a file set that does not follow the convention -- a legacy layout naming the directory "postgresql", or "." for a flat directory holding a single dialect's files.
func WithReporter ¶
WithReporter directs progress events to r. Without it a Migrator is silent.
type Reporter ¶
type Reporter interface {
// Applying is called immediately before a migration's up file runs.
Applying(m Migration)
// Applied is called after a migration and its ledger row have committed.
Applied(m Migration)
// Skipped is called for a migration already recorded in the ledger.
Skipped(m Migration)
// RollingBack is called immediately before a migration's down file runs.
RollingBack(m Migration)
// RolledBack is called after a rollback and its ledger deletion commit.
RolledBack(m Migration)
// StoppedAtTarget is called when Up halts at a version bound, reporting the
// requested target and the version that was not applied.
StoppedAtTarget(target, next int)
}
Reporter receives progress events as migrations are applied or rolled back.
A Migrator narrates through this rather than through a *slog.Logger, so the caller decides how progress appears: prose on a terminal, structured records in a service log, or nothing at all. A library that logs has already chosen its caller's output format.
Implementations must be safe to call with a zero-valued Migration and must not retain the value. A Migrator never calls a Reporter concurrently.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package clicmd builds a complete cobra command tree for applying migrations.
|
Package clicmd builds a complete cobra command tree for applying migrations. |
|
cmd
|
|
|
rung
command
Command rung applies versioned SQL migrations from a directory.
|
Command rung applies versioned SQL migrations from a directory. |
|
dialect
|
|
|
mysql
Package mysql provides the MySQL dialect, which also serves MariaDB.
|
Package mysql provides the MySQL dialect, which also serves MariaDB. |
|
postgres
Package postgres provides the PostgreSQL dialect.
|
Package postgres provides the PostgreSQL dialect. |
|
sqlite
Package sqlite provides the SQLite dialect.
|
Package sqlite provides the SQLite dialect. |
|
Package render turns migration progress and status into output.
|
Package render turns migration progress and status into output. |
|
Package reset drops every table in a database, including the migration ledger.
|
Package reset drops every table in a database, including the migration ledger. |