internal/migrate/migrate.go
internal/migrate · 218 lines · 14 declarations · source
This file carries the package documentation, rendered on the package page.
Declarations
type Schema
type Schema = pg.Schema
Schema and NewSchema live in the pg adapter, which is where every other statement builder needs them. Aliased rather than re-declared: two definitions of a validated identifier is two places for the validation to drift.
var NewSchema
var NewSchema = pg.NewSchema
NewSchema validates a database namespace.
const maxIdentifierLength
const maxIdentifierLength = 63
maxIdentifierLength is PostgreSQL's limit, restated for the tests that assert the boundary.
var scripts
var scripts embed.FS
const memoryScripts, controlScripts, ControlSchema
const (
memoryScripts = "sql"
controlScripts = "control"
// ControlSchema is where the registry lives. Fixed rather than configurable: it is one schema
// for the whole deployment, and a name that can vary is a name a grant can be written against
// wrongly.
ControlSchema Schema = "control"
)
Memory and control have independent migration sequences within one instance. Schema parameters support upgrades of existing namespace names and isolated database fixtures, not tenant routing.
type Script
type Script struct {
Version int
Name string
SQL string
}
Script is one migration, in the order it must be applied.
func Load
func Load() ([]Script, error)
Load reads the embedded migrations, ordered by version.
A duplicate version is an error rather than a last-one-wins, because two files claiming the same version apply in filename order on one machine and in another order on the next.
func LoadControl
func LoadControl() ([]Script, error)
LoadControl reads the embedded registry migrations, ordered by version.
func load
func load(dir string) ([]Script, error)
func Apply
func Apply(ctx context.Context, pool *pgxpool.Pool, schema Schema) (applied []int, err error)
Apply upgrades an instance memory namespace. Each migration and its version marker commit in one transaction, so failure leaves the last fully applied version. Project creation never calls it.
func withNamespaceLock
func withNamespaceLock(ctx context.Context, pool *pgxpool.Pool, schema Schema, fn func(on statements) error) error
withNamespaceLock runs fn holding the one lock that serialises changes to a memory namespace's structure and grants.
Bootstrap runs on every start and the chart starts replicas together, so two processes can migrate one namespace at once. Each migration is its own transaction, which keeps one migration whole but lets two runners interleave: both find version N unapplied, and the loser fails on an object the winner just created. The grants after the migrations race the same way. So the lock is a session-level advisory lock held across all of it, rather than a lock per transaction.
Taken on a connection of its own, and everything under it runs on that connection, for the reason EstablishPlanes gives: a caller that borrows a second connection while holding the first deadlocks once the pool has as many waiters as connections.
func applyControl
func applyControl(ctx context.Context, on statements) ([]int, error)
applyControl upgrades the fixed instance credential namespace using its separate migration set.
Unexported, and there is deliberately no exported wrapper. Its one caller is bootstrap, which already holds a connection for its session lock and passes that connection in rather than borrowing another from the pool — see `statements`. An exported form taking a pool was a second way in that nothing used, and a second way in is one somebody eventually calls from a path holding a lock, which is the deadlock that comment describes.
type statements
type statements interface {
Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
Begin(ctx context.Context) (pgx.Tx, error)
}
statements is what applying needs from a pool, and a single pooled connection offers the same. A caller that already holds one connection for a session lock runs its migrations on that connection rather than borrowing more from the pool: with N such callers on a pool of N, every connection is held by a waiter and the lock holder waits for one that never comes back.
func apply
func apply(ctx context.Context, pool statements, schema Schema, all []Script) (applied []int, err error)