Migrations
Foundation migrations describe historical changes to application tables. Foundation orders registered migrations, acquires a database advisory lock, applies the remaining schema changes, and records each successful migration. Doctrine supplies schema inspection, comparison, and platform SQL.
Create and change a table
Section titled “Create and change a table”After configuring DatabaseProvider, install the development CLI and generate an application provider:
The generator creates a table and its initial migration, and adds their registrations to the generated provider. Register that provider after DatabaseProvider in your application’s composition root.
The table owns only its stable name. The migration owns the initial schema:
Review the generated schema before running it:
The first run creates the migration ledger automatically. The command prefix comes from your WP-CLI configuration. Programmatic installation or upgrade code injects Migrator and calls $migrator->migrate() at its chosen upgrade boundary. A public plugin should run this after WordPress and its providers are ready, and record its installed application version only after migration succeeds. Run the same upgrade workflow for each affected site.
Register migrations explicitly
Section titled “Register migrations explicitly”Providers contribute migration objects lazily. Registration order does not determine execution order:
All providers contribute to one globally sorted collection. IDs are compared in ascending byte order and stored exactly, independently of class names and Strauss scoping. IDs must be unique, nonblank, unpadded, and no more than 191 bytes. 0 and latest are reserved targets. Generated timestamps provide ordering; custom IDs, including numeric timestamps, must occupy the lexical position their dependencies require.
Add a later change
Section titled “Add a later change”Generate an alteration for an existing table:
Its up() declares only this migration’s changes:
change() replaces a column’s complete definition. Restate any default, nullability, unsigned flag, comment, or other attribute you want to retain. Use dropColumn('name'), dropIndex('name'), index('name', ...$columns), and unique('name', ...$columns) for explicit removals and index changes. An index replacement declares the old index’s removal and the new definition under the same name.
The generated down() throws IrreversibleMigration until you supply a safe inverse. A generic migration does the same. Only an explicit --create=Reports_Table generates an initial-table declaration whose inverse drops the entire table. --create and --table are mutually exclusive; a migration’s name never implies destructive behavior.
Column declarations
Section titled “Column declarations”| Declaration | Meaning |
|---|---|
bigIncrements('id') |
Unsigned auto-incrementing BIGINT primary key |
string('name', 191) |
VARCHAR with an explicit maximum length |
text('body'), longText('body') |
TEXT or LONGTEXT |
integer('count'), bigInteger('count') |
Integer columns |
unsignedInteger('count'), unsignedBigInteger('count') |
Unsigned integers |
boolean('active') |
Boolean storage |
decimal('amount', 12, 4) |
Exact decimal precision and scale |
binary('token', 16) |
VARBINARY with an explicit length |
dateTime('updated_at', 6) |
DATETIME with fractional precision from 0 to 6 |
Columns support nullable(), notNull(), unsigned(), default(), comment(), and change(). Date/time declarations additionally support useCurrent() and useCurrentOnUpdate(). Use strings for exact decimal defaults. Table declarations support primary(...$columns) and comment().
up() and down() must be pure schema declarations. Foundation may replay them many times, including during previews. Do not query the live database, perform application work, or put existence guards in them.
Transform existing data
Section titled “Transform existing data”Implement the optional MigratesData contract alongside Migration when a forward migration must transform rows:
Foundation runs this callback after schema changes and before writing history. It must be safe to repeat if the process stops or recording fails. You may use the shared connection’s transactional() for a bounded data operation. Complete that transaction before returning; an open transaction interrupts the migration and is rolled back. DDL remains outside that transaction. Preview reports that a data callback exists but does not execute it. The data contract has no automatic reverse callback: supply schema rollback only when reversing remains safe for the resulting data, or throw IrreversibleMigration.
Deploy and recover
Section titled “Deploy and recover”| Operation | Command |
|---|---|
| Inspect pending, applied, and missing migrations | wp your-plugin migrate |
| Preview pending SQL | wp your-plugin migrate --run --dry-run |
| Apply pending migrations | wp your-plugin migrate --run |
| Reconcile to a registered ID | wp your-plugin migrate --run --to=<id> |
| Reverse the highest applied ID | wp your-plugin migrate --rollback |
| Reverse several applied IDs | wp your-plugin migrate --rollback --step=2 |
| Reconcile to a rollback target | wp your-plugin migrate --rollback --to=<id> |
| Reverse all applied migrations | wp your-plugin migrate --rollback --to=0 |
| Reverse and rerun everything | wp your-plugin migrate --refresh |
Programmatic equivalents are Migrator::status(), preview($target), migrate($target), rollback($steps), and refresh(). Migration operations return step objects containing the stable ID, direction, SQL, and whether a forward data callback was involved. Optional DescribesMigration::describe() supplies a status description.
An explicit target reverses applied IDs above it in descending order, then applies pending IDs through it in ascending order. latest applies all pending migrations. Rollback counts IDs, not deployment batches. Developers own dependencies and choosing a safe target, especially when a newly enabled package adds an older ID. Missing applied migration classes must be restored before execution can continue.
Interrupted schema changes
Section titled “Interrupted schema changes”MySQL DDL can commit before the history write. A retry compares the actual schema with the migration’s declared change: compatible existing additions and already-absent removals count as completed work. An interrupted index replacement resumes its missing work. Undeclared columns, indexes, and constraints are retained during alterations.
An incompatible existing declaration stops the run with IncompatibleSchema; Foundation does not silently reconcile unrelated drift. Inspect and correct the mismatch before retrying. LedgerFailure means a history write failed after migration work; fix the storage failure and retry with the same declarations. Any data callback must tolerate repetition.
Concurrent upgrades
Section titled “Concurrent upgrades”One database advisory lock covers planning, schema execution, data callbacks, and history writes. The lock survives DDL commits and lasts until release or session termination. It has no lease TTL to configure. MigrationAlreadyRunning means another session is migrating this application’s site ledger: defer and retry after it finishes. This is distinct from a database failure.
MigrationInterrupted means ownership or the starting session could not be confirmed. Foundation stops and releases only the original session’s lock where possible. Never change sites or sessions during a migration, even temporarily. These two exceptions live under Database\Exceptions; schema and history exceptions live under Database\Migration\Exceptions.
All migration participants must reach the same primary database server. Advisory locks are local to that server; a proxy that moves statements between sessions or servers cannot provide this guarantee. Preview and status are observations and can become stale before a later run.
Customize generation and test
Section titled “Customize generation and test”The defaults are Database\Tables, Database\Migrations, and Database beneath your Composer application namespace. Use the CLI generator configuration to choose project namespaces, or command options --namespace and --path for individual output. Stub overrides live under foundation/stubs/database/. Keep migrations in production autoloaded code even when the generator itself is installed with --dev.
Test create, alteration, rollback, and retry against real database tables. Include a failure after successful DDL but before history recording, then verify that retry preserves existing rows and records the migration once. Register a fresh container per test and use application-specific test table names.