Skip to main content

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.

source

var NewSchema

var NewSchema = pg.NewSchema

NewSchema validates a database namespace.

source

const maxIdentifierLength

const maxIdentifierLength = 63

maxIdentifierLength is PostgreSQL's limit, restated for the tests that assert the boundary.

source

var scripts

var scripts embed.FS

source

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.

source

type Script

type Script struct {
Version int
Name string
SQL string
}

Script is one migration, in the order it must be applied.

source

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.

source

func LoadControl

func LoadControl() ([]Script, error)

LoadControl reads the embedded registry migrations, ordered by version.

source

func load

func load(dir string) ([]Script, error)

source

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.

source

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.

source

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.

source

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.

source

func apply

func apply(ctx context.Context, pool statements, schema Schema, all []Script) (applied []int, err error)

source