Documentation
¶
Overview ¶
Package bunmigrator builds a Postgres schema out of bun models and applies it through bun's migrator.
A Target is one migratable object -- an enum type (Enum), the shared updated_at trigger function (AutoUpdatedAt), one model's table (Table), or anything a caller implements itself -- and renders itself to SQL text rather than executing it. A Registry is the ordered list of them; Registry.Plan resolves selectors into the subset that has to run, pulling in dependencies; FS turns that into an in-memory migration directory, which Discover hands to bun and Apply runs.
Rendering to text rather than to database calls is what lets a dry run print the exact files a real run would execute, on a machine with no Postgres at all. WriteDir puts those same files on disk for the bun-migrator binary to apply later.
The rendered SQL is Postgres-specific and needs Postgres 14 or later.
Index ¶
- Constants
- Variables
- func Apply(ctx context.Context, db *bun.DB, ms *migrate.Migrations) (*migrate.MigrationGroup, error)
- func AutoUpdatedAtTrigger(table string) string
- func Discover(db *bun.DB, steps []Step) (*migrate.Migrations, error)
- func FS(db *bun.DB, steps []Step) (fstest.MapFS, error)
- func RenderDB() *bun.DB
- func Rollback(ctx context.Context, db *bun.DB, ms *migrate.Migrations) (*migrate.MigrationGroup, error)
- func Stem(seq int, slug string) string
- func WriteDir(dir string, fsys fs.FS) error
- type AutoUpdatedAt
- type Change
- type Enum
- type Registry
- type Step
- type Table
- type TableOption
- type Target
Constants ¶
const ( TxUpSuffix = ".tx.up.sql" TxDownSuffix = ".tx.down.sql" )
TxUpSuffix and TxDownSuffix name the two files a target becomes.
The .tx. infix asks bun to run the file's statements inside a transaction. Postgres supports transactional DDL, so a CREATE TABLE that fails partway leaves nothing behind.
const AutoUpdatedAtSelector = "AutoUpdatedAt"
AutoUpdatedAtSelector is the selector of the AutoUpdatedAt target, and what every table opting in with WithAutoUpdatedAt reports as a dependency.
const SplitDirective = "--bun:split"
SplitDirective separates statements within one migration file. bun splits on this and never on semicolons, which is what lets an enum target emit a DO block whose body is full of them.
Variables ¶
var ErrNoConnection = errors.New("bunmigrator: the rendering DB cannot connect")
ErrNoConnection is what the rendering DB's driver returns in place of dialing. Seeing it means something tried to execute a statement against a handle whose only job is turning models into SQL text.
Functions ¶
func Apply ¶
func Apply(ctx context.Context, db *bun.DB, ms *migrate.Migrations) (*migrate.MigrationGroup, error)
Apply runs every unapplied migration in ms against db, under bun's migration lock, and returns the group it applied. A zero group means the database was already up to date.
Bookkeeping is written only after each migration succeeds (migrate.WithMarkAppliedOnSuccess), so a failed Up never leaves a row claiming success. The trade is that a crash between a migration's commit and its bookkeeping write re-runs the migration, which is why every built-in target renders idempotent SQL.
The lock is released even when ctx is already cancelled: the unlock runs on a context derived with context.WithoutCancel, so a Ctrl-C mid-run does not strand the lock row and break every later run.
func AutoUpdatedAtTrigger ¶
AutoUpdatedAtTrigger renders the BEFORE UPDATE trigger that keeps the updated_at column of the named Postgres table current by calling the function AutoUpdatedAt creates. Table emits it for models that opt in with WithAutoUpdatedAt; it is exported so a custom Target can emit the same trigger.
The WHEN clause is what keeps the stamp meaningful: an UPDATE that writes the same values back is not a change, and without the guard it would still move updated_at. CREATE OR REPLACE TRIGGER (Postgres 14+) is the trigger spelling of IF NOT EXISTS, which every other target here uses.
func FS ¶
FS renders steps into an in-memory migration directory.
bun's Migration type keeps its up and down functions in an unexported field type, so a struct literal cannot carry them and Register takes its name from the calling file. Discover, which reads .up.sql/.down.sql out of any fs.FS, is the only exported way to hand bun both a name and a body -- so the generated SQL is written to a filesystem that exists only in memory.
The upshot is better than a workaround: --dry-run prints these exact files, so what it shows is not a description of what would run, it is what runs.
func RenderDB ¶
RenderDB returns a bun.DB that can build Postgres SQL but cannot run it.
Rendering needs a *bun.DB only for its dialect -- building a query never touches the connection -- but the obvious handle to reach for, pgdb.DB(), goes through env.Must("PG_USER") and panics when it is unset. A dry run on a machine with no Postgres is precisely the case this command exists to serve, so it gets a handle that provably cannot reach a database rather than one that merely happens not to.
func Rollback ¶
func Rollback(ctx context.Context, db *bun.DB, ms *migrate.Migrations) (*migrate.MigrationGroup, error)
Rollback rolls back the most recently applied group among the migrations in ms, under the same lock and bookkeeping rules as Apply, and returns the group it rolled back. A zero group means nothing was applied.
func Stem ¶
Stem returns the filename stem bun parses a migration's identity out of: the sequence number in four fixed digits, then the slug.
The padding is not cosmetic. bun keeps the leading digits as the migration's Name and orders migrations by comparing those strings, so unpadded numbers would run 10 before 2.
func WriteDir ¶
WriteDir writes every file in fsys into dir, creating dir if needed and overwriting files of the same name. It is what a command's --out flag does, and produces exactly the directory the bun-migrator binary applies.
Overwriting is deliberate, and why this is not os.CopyFS: a generator that fails on its second run into the same directory is useless.
Types ¶
type AutoUpdatedAt ¶
type AutoUpdatedAt struct {
// contains filtered or unexported fields
}
AutoUpdatedAt is an optional Target that creates the one plpgsql function behind automatically maintained updated_at columns. Register it once, then pass WithAutoUpdatedAt to each table that should get a BEFORE UPDATE trigger calling it; tables that do not opt in are untouched.
It exists because Postgres has no ON UPDATE clause -- a column DEFAULT is evaluated on INSERT and never again -- so keeping the stamp current is a trigger or nothing. One function serves every table because it names the column rather than the table.
func NewAutoUpdatedAt ¶
func NewAutoUpdatedAt(seq int) AutoUpdatedAt
NewAutoUpdatedAt returns the target that creates the shared trigger function. Give it a sequence number lower than any table that opts in.
func (AutoUpdatedAt) DependsOn ¶
func (f AutoUpdatedAt) DependsOn() []string
func (AutoUpdatedAt) Down ¶
func (f AutoUpdatedAt) Down(*bun.DB) ([]string, error)
Down drops the function. Bun rolls a group back in reverse name order, so every table whose trigger calls it has already been dropped -- and with it the trigger -- by the time this runs.
func (AutoUpdatedAt) Selector ¶
func (f AutoUpdatedAt) Selector() string
func (AutoUpdatedAt) Seq ¶
func (f AutoUpdatedAt) Seq() int
func (AutoUpdatedAt) Slug ¶
func (f AutoUpdatedAt) Slug() string
type Change ¶
A Change is what one migration did to the schema, in words: an Action such as ADDED, a Kind such as TABLE, and the object's Name. It is derived from the migration's name alone, so it is available to the bun-migrator binary, which never sees a Target.
func Changes ¶
func Changes(ms migrate.MigrationSlice, rollback bool) []Change
Changes describes what applying (or, with rollback set, rolling back) each migration in ms did, in the order they ran. Bun hands back a rolled-back group in ascending name order even though it ran the downs in reverse, so with rollback set the result is ms reversed.
A migration named verb_kind_object -- which is what every built-in target produces, e.g. create_table_users or create_type_post_status -- becomes "ADDED TABLE users" on apply and "DROPPED TABLE users" on rollback; alter becomes UPDATED and REVERTED, drop becomes DROPPED and RESTORED. A name that does not follow that shape, such as one supplied through --name, is reported as "APPLIED <name>" or "ROLLED BACK <name>" rather than guessed at.
type Enum ¶
type Enum struct {
// contains filtered or unexported fields
}
Enum is a Target that creates a Postgres enum type. Bun has no support for enum types at all, so unlike Table this renders its SQL by hand.
func (Enum) Down ¶
Down drops the type. Bun rolls a group back in reverse name order, so every table using this type has already been dropped by the time this runs.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
A Registry is every migratable target, in the order they run.
Build one with NewRegistry, which checks the invariants the rest of the package relies on. Sequence numbers should be literals, never derived from position: renumbering a target changes the name bun recorded it under, which would make an applied migration look unapplied and run it again. Leave gaps so a new enum can go in front of the tables that use it.
func NewRegistry ¶
NewRegistry returns a registry of targets, or an error when they break an invariant the planner relies on: sequence numbers must be unique and strictly ascending in declaration order, selectors must be distinct case-insensitively, and every dependency must be registered with a lower sequence number than its dependent.
That last rule is what makes sort-by-sequence a valid topological order, so Registry.Plan never has to compute one.
func (Registry) Plan ¶
Plan resolves selectors into the ordered steps a run applies.
With no selectors every registered target is planned. Named targets pull in their dependencies transitively, which is required rather than helpful: planning a table alone would produce a CREATE TABLE that fails on the enum type a column uses or on a table its foreign key references. Pulling extra targets in costs nothing against a database that already has them, since bun skips every migration it has already applied.
The result is sorted by sequence number, which puts enum types before the tables whose columns use them and each table after the ones it references. That sort is a valid topological order because NewRegistry rejects any dependency whose sequence number is not lower than its dependent's.
type Step ¶
Step is one migration in a plan: a target, and the filename stem it will be recorded under. The stem is a field rather than a method call so a caller can replace it -- the way the command's --name flag does -- without touching the registry.
type Table ¶
type Table struct {
// contains filtered or unexported fields
}
Table is a Target that creates one model's table. Unlike Enum it renders nothing itself: the column list is whatever the model's bun tags say, so the schema cannot drift from the struct it is derived from.
func NewTable ¶
func NewTable(seq int, selector, tableName string, mdl any, opts ...TableOption) Table
NewTable returns the target that creates tableName from mdl, which must be a typed nil pointer to the model, e.g. (*User)(nil). selector is the name a user types to pick this target, and what other targets name in DependsOn.
func (Table) DependsOn ¶
DependsOn lists the declared dependencies, plus the AutoUpdatedAt function when the table opted in with WithAutoUpdatedAt -- the trigger Up renders cannot be created before the function it calls.
func (Table) Down ¶
Down drops the table, and with it every index on it -- Postgres does not need those dropped separately.
Bun rolls a group back in reverse name order, so a referencing table is always dropped before the one it references and no CASCADE is needed.
func (Table) Up ¶
Up renders the CREATE TABLE, its indexes, and -- with WithAutoUpdatedAt -- the trigger that keeps the updated_at column current.
It goes through AppendQuery rather than the query's String method, which panics on a render error -- a malformed foreign key clause should be an error the CLI reports, not a stack trace.
type TableOption ¶
type TableOption func(*Table)
TableOption tunes a Table at construction.
func ColumnExpr ¶
func ColumnExpr(expr string) TableOption
ColumnExpr adds a raw entry to the column list, which is where a table-level CHECK goes. Putting it here rather than in a following ALTER TABLE keeps it inside the same CREATE TABLE IF NOT EXISTS, and so keeps the target idempotent.
func DependsOn ¶
func DependsOn(selectors ...string) TableOption
DependsOn names the selectors whose objects must exist first -- the enum types this table's columns use, and the tables its foreign keys reference.
func ForeignKey ¶
func ForeignKey(clause string) TableOption
ForeignKey adds a FOREIGN KEY constraint, spelled as bun wants it: `("user_id") REFERENCES "users" ("id")`.
func Index ¶
func Index(columns ...string) TableOption
Index adds a B-tree index over columns, in the order given.
Order is the whole content of a composite index: Postgres can use a leading subset of an index's columns and not a trailing one, so (portfolio_id, symbol) serves a lookup by portfolio_id alone -- replacing the single-column index rather than supplementing it -- while a lookup by symbol alone needs its own.
Primary keys and unique constraints are already indexes, so do not declare one over a column that has either.
func LiveUniqueIndex ¶
func LiveUniqueIndex(columns ...string) TableOption
LiveUniqueIndex adds a unique index over columns, restricted to rows that have not been soft-deleted:
CREATE UNIQUE INDEX ... ON "table" (...) WHERE ("deleted_at" IS NULL)
It is deliberately narrower than a general unique index plus a free-text predicate: every use means the same thing -- "unique among rows that have not been soft-deleted" -- and a name that says so cannot be misapplied to a table with no deleted_at column. Table.Up refuses to render one for a model without that column.
The predicate is the entire reason this exists, not an optimization on top of a plain unique index: a soft-deleted row that kept its slot in an ordinary UNIQUE constraint would make the value it held -- an email, a (portfolio_id, symbol) pair -- unreclaimable forever. Restricting the index to live rows lets a deleted row's value be claimed again by a live one.
func WithAutoUpdatedAt ¶
func WithAutoUpdatedAt() TableOption
WithAutoUpdatedAt gives the table a BEFORE UPDATE trigger that stamps its updated_at column, and makes it depend on the AutoUpdatedAt target, which must therefore be registered. The model must have an updated_at column; Table.Up refuses to render otherwise.
type Target ¶
type Target interface {
// Seq is the target's fixed position in the migration order. It is a
// literal in the registry, never derived from position in a slice, so
// inserting a target cannot renumber -- and thereby re-run -- an existing
// one.
Seq() int
// Slug is the descriptive half of the migration's filename, which bun
// keeps as the migration's comment.
Slug() string
// Selector is the name a user types on the command line.
Selector() string
// DependsOn lists the selectors whose objects must exist before this
// target's SQL will run: the enum types its columns use, and the tables
// its foreign keys point at.
DependsOn() []string
Up(db *bun.DB) ([]string, error)
Down(db *bun.DB) ([]string, error)
}
Target is one migratable object: a Postgres enum type, the shared updated_at trigger function, or one model's table.
Up and Down return SQL text rather than running it. That is the whole design in one line: `migrate --dry-run` prints what they return and a real run executes it, so the two cannot describe different schemas.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
bun-migrator
command
Command bun-migrator applies, rolls back and reports on a directory of bun SQL migrations.
|
Command bun-migrator applies, rolls back and reports on a directory of bun SQL migrations. |
|
examples
|
|
|
basic
command
Command basic is the smallest complete bun-migrator program: two models, a registry, and migratecmd.Models as the whole CLI.
|
Command basic is the smallest complete bun-migrator program: two models, a registry, and migratecmd.Models as the whole CLI. |
|
custom-target
command
Command custom-target shows implementing bunmigrator.Target yourself: Extension (extension.go) and View (view.go), registered alongside the built-in Enum and Table targets.
|
Command custom-target shows implementing bunmigrator.Target yourself: Extension (extension.go) and View (view.go), registered alongside the built-in Enum and Table targets. |
|
existing-app
command
Command existing-app simulates bolting bun-migrator onto an app that already has models, a database handle and a CLI.
|
Command existing-app simulates bolting bun-migrator onto an app that already has models, a database handle and a CLI. |
|
files-workflow
command
Command files-workflow is a Models command, meant to be run with --out to generate a migrations directory for the bun-migrator binary to apply later.
|
Command files-workflow is a Models command, meant to be run with --out to generate a migrations directory for the bun-migrator binary to apply later. |
|
startup-migrate
command
Command startup-migrate is a service that migrates its own schema before it starts serving, using only the bunmigrator library -- no urfave/cli, no migratecmd, because a service's own entry point rarely wants a sub-command tree just to run a migration at boot.
|
Command startup-migrate is a service that migrates its own schema before it starts serving, using only the bunmigrator library -- no urfave/cli, no migratecmd, because a service's own entry point rarely wants a sub-command tree just to run a migration at boot. |
|
Package migratecmd provides two urfave/cli commands over bunmigrator.
|
Package migratecmd provides two urfave/cli commands over bunmigrator. |
|
Package pgconn opens a Postgres bun.DB from a DSN or from the PG_* environment variables, for callers that do not already have a handle.
|
Package pgconn opens a Postgres bun.DB from a DSN or from the PG_* environment variables, for callers that do not already have a handle. |