Migrations
ORM::ActiveRecord includes commands to migrate your database. Migrations include adding and removing tables as well as adding and removing columns and indexes.
Migration files contain either a single change method (see
Reversible migrations) or a pair of up and down
methods. The up method is the forward change you want to
perform. The down method should contain what you want to happen if you
decide to rollback the changes from the up method.
Examples
db/migrate/001-create-users.raku
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
db/migrate/002-create-pages.raku
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Column types
create-table and add-column accept the same column-type vocabulary. The
canonical name (the adjective on the right-hand side) is what you pass; the
DDL produced is adapter-aware (see Adapters for the per-engine
differences).
| Type | Notes |
|---|---|
:string |
Variable-length text. Accepts limit => N (defaults to 255). |
:text |
Unbounded text. No limit. |
:integer |
Whole-number column. |
:boolean |
True/False. Storage varies by adapter (BOOLEAN, TINYINT(1), INTEGER 0/1). |
:datetime |
Timestamp without explicit timezone semantics. |
:timestamp |
Synonym for :datetime. |
:reference |
Foreign-key column. The column declared as user => { :reference } becomes user_id INTEGER plus an index. See the pages / subscriptions examples above. |
Every column type accepts a default => $value option to set a column-level
default.
1 2 3 4 5 6 7 | |
Adding and removing columns
add-column adds a single column to an existing table. The column spec uses
the same shape as inside create-table:
1 2 | |
remove-column is the inverse and takes the bare column name:
1 | |
A typical pair lives in up / down:
1 2 3 4 5 6 7 8 9 | |
Changing columns
After a table exists, four methods alter the shape of a column in place:
| Method | Effect |
|---|---|
change-column |
Replace the column's type (and optionally its default / null / comment) |
change-column-default |
Set or drop the column's default value |
change-column-null |
Toggle the NOT NULL constraint |
change-column-comment |
Set or clear the column comment |
1 2 3 4 5 6 7 8 9 10 11 | |
Reversibility
Inside change, only operations whose inverse is unambiguous run on down:
| Operation | Reversible in change? |
|---|---|
change-column-null(t, c, $bool) |
Yes — the bool is toggled on down |
change-column-default(t, c, :from, :to) |
Yes — from/to are swapped on down |
change-column-comment(t, c, :from, :to) |
Yes — from/to are swapped on down |
change-table-comment(t, :from, :to) |
Yes — from/to are swapped on down |
change-column |
No — raises X::IrreversibleMigration on down |
change-column-default(t, c, $value) |
No — raises unless the from:/to: form is used |
change-column-comment(t, c, $value) |
No — raises unless the from:/to: form is used |
change-table-comment(t, $value) |
No — raises unless the from:/to: form is used |
For the irreversible cases, provide explicit up / down pairs or wrap the
call in reversible. The from: / to: shorthand keeps a default / comment
change inside change:
1 2 3 4 5 6 | |
Table comments
change-table-comment sets a comment on the whole table:
1 2 | |
Adapter differences
| Operation | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
change-column |
ALTER ... TYPE |
MODIFY COLUMN (re-emits the column definition with introspected null / default / comment merged) |
Not supported — raises |
change-column-default |
ALTER ... SET/DROP DEFAULT |
ALTER ... SET/DROP DEFAULT |
Not supported — raises |
change-column-null |
ALTER ... SET/DROP NOT NULL |
MODIFY COLUMN with introspected type |
Not supported — raises |
change-column-comment |
COMMENT ON COLUMN ... |
MODIFY COLUMN ... COMMENT '...' |
Silent no-op (SQLite has no column comments) |
change-table-comment |
COMMENT ON TABLE ... |
ALTER TABLE ... COMMENT = '...' |
Silent no-op (SQLite has no table comments) |
SQLite has no ALTER COLUMN and would need a table rebuild for type / default
/ null changes. Those methods raise rather than silently doing the wrong thing
— use reversible with raw execute if you need parity in a SQLite-only
migration.
Adding and removing indexes
add-index creates an index on a column (or set of columns). The simplest
form indexes one column:
1 | |
The generated index name is <table>_<column>_idx. To make it unique, pass
{ :unique } as the value:
1 | |
For composite indexes pass an angle-quoted list of column names. The index
name becomes <table>_<col1>_<col2>_idx:
1 2 | |
remove-index mirrors the single-column form and reconstructs the same name
internally:
1 | |
Renaming tables, columns, indexes
rename-table moves a table, rename-column renames a column in place, and
rename-index renames an existing index. All three are reversible — the
inverse swaps the from / to identifiers, so a change method can call them
directly:
1 2 3 4 5 6 7 | |
rename-index takes the table name first because MySQL's RENAME INDEX
syntax is rooted at the table. PostgreSQL and SQLite ignore the table name
but follow the same call signature for cross-adapter parity. SQLite has no
native ALTER INDEX RENAME — the adapter looks up the original
CREATE INDEX SQL, drops it, and re-runs it with the new identifier.
References
A reference is the "I belong to X" shortcut for a foreign-key column.
add-reference :user adds a user_id column plus a matching index. The
inverse — remove-reference :user — drops the index and the column.
1 2 3 4 5 | |
add-belongs-to is an alias for add-reference; remove-belongs-to is the
inverse alias. Options accepted by both:
| Option | Default | Effect |
|---|---|---|
null |
True |
Set to False to make <name>_id (and <name>_type) NOT NULL. |
index |
True |
Set to False to skip the index. |
unique |
False |
Make the index unique. |
polymorphic |
False |
Add both <name>_id and <name>_type; the index becomes composite (<name>_type, <name>_id). |
type |
'integer' |
Override the integer type (e.g. bigint). |
foreign-key |
False |
Also add an ALTER TABLE ... ADD CONSTRAINT FOREIGN KEY — see below. |
to-table |
<name>s |
The referenced table when foreign-key is set and the inferred plural is wrong. |
on-delete |
(none) | Forwarded to the FK clause; only valid with foreign-key => True. |
on-update |
(none) | Same. |
fk-name |
(auto) | Override the generated FK constraint name. |
1 2 3 | |
add-reference index => True (the default) names the index
<table>_<name>_id_idx for a regular reference and
<table>_<name>_type_<name>_id_idx for a polymorphic one.
Foreign keys
add-foreign-key and remove-foreign-key mutate the FK constraint without
touching the column. Use them when the column already exists (e.g. on a
legacy table) or when you want a constraint between two tables that don't
follow the <name>s plural inference.
1 2 3 4 5 6 7 8 | |
Options for add-foreign-key(from, to, ...):
| Option | Default | Effect |
|---|---|---|
column |
<singular_to_table>_id |
Source column on from. |
primary-key |
id |
Target column on to. |
name |
fk_<from_table>_<column> |
Override the constraint name. |
on-delete |
(none) | cascade, nullify / set-null, set-default, restrict, no-action. |
on-update |
(none) | Same vocabulary as on-delete. |
validate |
True |
On PostgreSQL, False emits NOT VALID so the constraint is enforced on new rows only. Other adapters ignore this option. |
remove-foreign-key accepts either the explicit name: or to-table: plus
(optionally) column: so it can derive the same name add-foreign-key
would have generated:
1 2 | |
validate-foreign-key(table, name) runs ALTER TABLE ... VALIDATE
CONSTRAINT on PostgreSQL (for the deferred NOT VALID workflow). On MySQL
it is a no-op because MySQL validates every constraint on creation.
Reversibility
add-reference and add-foreign-key are reversible inside change —
down calls remove-reference / remove-foreign-key with the same
options. The standalone remove-reference and remove-foreign-key
operations are irreversible inside change (no way to derive the original
options); supply explicit up / down pairs.
Adapter differences
| Operation | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
add-reference (column + idx) |
Yes | Yes | Yes |
add-reference :foreign-key |
ALTER TABLE ... ADD CONSTRAINT |
ALTER TABLE ... ADD CONSTRAINT |
Raises — declare the FK in create-table instead |
add-foreign-key direct |
Yes | Yes | Raises — declare the FK in create-table instead |
remove-foreign-key |
ALTER TABLE ... DROP CONSTRAINT |
ALTER TABLE ... DROP FOREIGN KEY |
Raises |
validate :False (NOT VALID) |
Yes | No-op (always validates) | n/a |
Check, unique, and exclusion constraints
These DSL methods emit ALTER TABLE ... ADD CONSTRAINT / DROP CONSTRAINT
statements for table-level constraints beyond foreign keys.
1 2 3 4 5 6 7 8 9 10 | |
add-check-constraint(table, expression, ...)
| Option | Default | Effect |
|---|---|---|
name |
chk_<table>_<expr-hash> |
Override the constraint name. |
validate |
True |
On PostgreSQL, False emits NOT VALID so the constraint only applies to new rows. On MySQL, False emits NOT ENFORCED. |
remove-check-constraint(table, ...) accepts name: or expression: —
when only the expression is given it re-derives the same name
add-check-constraint would have generated:
1 2 | |
validate-check-constraint(table, name) runs ALTER TABLE ... VALIDATE
CONSTRAINT on PostgreSQL (for the deferred NOT VALID workflow) and
ALTER TABLE ... ALTER CHECK name ENFORCED on MySQL. It is irreversible
inside change — supply explicit up / down if needed.
add-unique-constraint(table, ...)
| Option | Default | Effect |
|---|---|---|
columns |
(required) | List of column names covered by the constraint. |
name |
uq_<table>_<col1>_<col2>... |
Override the constraint name. |
deferrable |
False |
PostgreSQL DEFERRABLE — defer checking to end of transaction. |
initially-deferred |
False |
Implies deferrable; emits DEFERRABLE INITIALLY DEFERRED. |
remove-unique-constraint(table, ...) accepts name: or columns: —
when only columns: is given it re-derives the same name:
1 2 | |
add-exclusion-constraint(table, expression, ...) (PostgreSQL only)
| Option | Default | Effect |
|---|---|---|
using |
gist |
Access method (gist, btree, spgist, etc.). |
name |
excl_<table>_<expr-hash> |
Constraint name. |
where |
(none) | Optional WHERE (...) predicate for a partial exclusion constraint. |
deferrable |
False |
Same as for unique constraints. |
initially-deferred |
False |
Same as for unique constraints. |
1 2 3 4 | |
remove-exclusion-constraint requires the explicit name: (the
expression cannot always be normalised back to the auto-name).
Reversibility
| Operation | Inside change |
|---|---|
add-check-constraint |
Reversed by remove-check-constraint with the same name. |
remove-check-constraint |
Irreversible — supply explicit up / down. |
validate-check-constraint |
Irreversible. |
add-unique-constraint |
Reversed by remove-unique-constraint with the same name. |
remove-unique-constraint |
Irreversible. |
add-exclusion-constraint |
Reversed when name: is supplied; otherwise irreversible. |
remove-exclusion-constraint |
Irreversible. |
Adapter differences
| Operation | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
add-check-constraint |
ALTER TABLE ... ADD CONSTRAINT |
ALTER TABLE ... ADD CONSTRAINT |
Raises — declare the CHECK in create-table instead |
remove-check-constraint |
ALTER TABLE ... DROP CONSTRAINT |
ALTER TABLE ... DROP CHECK |
Raises |
validate-check-constraint |
VALIDATE CONSTRAINT |
ALTER CHECK name ENFORCED |
No-op |
add-check :validate => False |
NOT VALID |
NOT ENFORCED |
n/a |
add-unique-constraint |
ALTER TABLE ... ADD CONSTRAINT |
ALTER TABLE ... ADD CONSTRAINT |
Raises — use add-index :unique => True instead |
remove-unique-constraint |
ALTER TABLE ... DROP CONSTRAINT |
ALTER TABLE ... DROP INDEX |
Raises |
add-exclusion-constraint |
ALTER TABLE ... ADD CONSTRAINT |
Raises | Raises |
remove-exclusion-constraint |
ALTER TABLE ... DROP CONSTRAINT |
Raises | Raises |
Timestamps
add-timestamps adds created_at and updated_at columns and manages them
automatically: created_at is set on insert, updated_at is set on every
save.
The exact column type is adapter-aware:
| Adapter | Generated DDL |
|---|---|
| PostgreSQL | TIMESTAMPTZ NOT NULL DEFAULT now() |
| MySQL | DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) (microsecond precision) |
| SQLite | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
You can also declare datetime columns explicitly with :datetime (or
:timestamp):
1 | |
To remove the timestamp columns:
1 | |
Reversible migrations
Most schema operations have an obvious inverse: create-table ↔ drop-table,
add-column ↔ remove-column, add-index ↔ remove-index,
add-timestamps ↔ remove-timestamps. Instead of writing both up and down,
define a single change method and let the runner derive the rollback:
1 2 3 4 5 6 7 8 9 10 11 | |
Running this migration forward calls each operation in change in order.
Rolling it back records each call, then plays the inverses in reverse order
— so articles loses its timestamps first, then the table is dropped.
reversible for asymmetric blocks
When an operation needs different code in each direction (typically raw
SQL), wrap it in reversible. The block is invoked with a direction
helper that exposes up and down:
1 2 3 4 5 6 7 8 9 10 | |
Going up adds the column then runs the up-block. Going down runs the down-block first, then removes the column.
revert to undo a previous block
revert takes a block and performs the inverse of every operation
inside, in reverse order. It undoes an earlier migration without copy-pasting
the original:
1 2 3 4 5 6 7 8 9 | |
Up: drops the legacy_audit_log table.
Down: re-creates it with the original definition.
execute is irreversible inside change
execute runs raw SQL. There is no way to derive its inverse
automatically, so calling it inside change makes the migration
irreversible — the rollback will raise X::IrreversibleMigration. Either
provide an up/down pair, or wrap the SQL in reversible and supply
both directions.
Irreversible migrations
Some forward changes can't be undone automatically — a destructive drop, a
column rewrite that loses data, an enum-narrowing. Mark the down method as
irreversible by calling self.irreversible-migration:
1 2 3 4 5 6 7 8 9 | |
The runner reports the offending file and aborts the rollback if it ever hits this. See Errors » X::IrreversibleMigration.
The ar command
ar is the command-line tool for creating, migrating, and checking your
database(s). It reads the same configuration as the rest of the ORM
(DATABASE_URL, or config/application.json — see Adapters).
| Command | What it does |
|---|---|
ar |
Run all outstanding up migrations (same as ar migrate). |
ar migrate |
Run all outstanding up migrations against the configured database(s). |
ar createdb |
Create the configured database(s); does not migrate. |
ar check |
Report whether the database(s) exist and are fully migrated; exit non-zero if not. Changes nothing. |
ar up[:N] |
Run all pending migrations, or just N of them. |
ar down[:N] |
Roll back all migrations, or just N of them. |
ar reset [--yes] |
Drop every table (see Reset). |
ar --version |
Print the installed version. |
ar --help |
Print usage. |
Run Migrations
With no arguments, ar runs all outstanding up methods:
1 | |
You can also migrate up or down a specific number of migrations:
1 2 3 4 | |
Creating databases
ar createdb creates the database(s) named in your configuration without
running any migrations — useful for a fresh checkout before the first ar:
1 2 | |
For a multi-database config (more than one named connection in the active
environment) it creates every one. SQLite files are created on first connect,
so createdb is effectively a no-op there.
Checking readiness
ar check verifies, without changing anything, that every database the active
environment expects exists and has all migrations applied. It exits non-zero
and prints a single summary if anything is missing or behind:
1 2 3 4 5 | |
Parallel test databases
createdb, migrate, and check accept --parallel, which targets the test
environment's per-worker database copies instead of the single base database.
The worker count comes from the test environment's parallel key in
config/application.json:
1 2 3 | |
This is the machinery behind parallel test runs — see Tests.
Reset
ar reset drops every table in the database (including the bookkeeping
migrations table) so the next ar run reapplies every migration from
scratch. The drop ignores foreign-key dependencies: PostgreSQL uses
DROP TABLE ... CASCADE, MySQL temporarily flips FOREIGN_KEY_CHECKS = 0,
SQLite turns off PRAGMA foreign_keys.
Reset is destructive. It prints the tables it is about to drop and prompts:
1 2 3 4 5 6 7 | |
Pressing Y or Enter proceeds (the default is yes). Anything else
aborts immediately:
| Reply | Outcome |
|---|---|
Y / y |
Drop tables |
<enter> (empty) |
Drop tables |
n |
Abort |
| anything else | Abort |
To bypass the prompt in scripts, pass --yes (or set AR_ASSUME_YES=1):
1 2 | |
ar reset does not re-run migrations. Pair it with a plain ar to drop
all tables and re-run every migration:
1 | |