Migrations
Foundation migrations apply ordered database changes and record each successful run in a WordPress-backed ledger. Prefer the bundled WP-CLI command during deployment so initialization, locking, execution, and status reporting follow one path.
Create migrations
Section titled “Create migrations”Generate the database feature
Section titled “Generate the database feature”Install the generator as a development dependency:
Generate the application provider before its tables and migrations:
The --migration flag creates the table class and its initial create-table migration together. Both classes are also added to the generated database provider when it exists.
The generators use the project’s Composer namespace and create this feature structure by default:
When src/Database/Provider.php exists, the table and migration generators add their container registrations automatically. Register that provider in the application’s ordered provider list as shown in Database configuration.
Understand migration classes
Section titled “Understand migration classes”Every migration has one permanent identifier and two operations:
| Member | Purpose |
|---|---|
id() |
Returns the byte-exact identifier stored in the migration ledger. Never change it after deployment. |
up() |
Applies the schema or data change. |
down() |
Reverses the change, or throws IrreversibleMigration when no safe inverse exists. |
Constructor injection is available for application tables and other services. Foundation resolves each registered migration through the container when a migration operation runs.
Customize generated output
Section titled “Customize generated output”The conventional generator paths and names require no extra options. When a project uses a different structure, the table generator’s --namespace and --path options customize the table class; its migration remains in the conventional Database\\Migrations namespace and path. Pass --migration-id=<id> only when the generated timestamp identifier must be replaced.
The table generator derives the unprefixed WordPress table name from the class name. Pass --table-name=report_entries when that unprefixed name should differ from the default; Foundation applies the active WordPress prefix at runtime, producing a physical name such as wp_report_entries. Do not include wp_ or another site prefix in this option, because --table-name=wp_report_entries could produce wp_wp_report_entries. The migration generator’s --table option has a separate, class-oriented meaning: it selects the existing table class whose schema should be reconciled.
Project-specific stubs can override the defaults at:
Define table schemas
Section titled “Define table schemas”Create a table definition
Section titled “Create a table definition”The generated src/Database/Tables/Reports_Table.php owns its stable, unprefixed table name and desired schema. Its inherited name() method asks the database service to apply the current WordPress table prefix and validate the resulting physical name when the table is used.
Choose column types
Section titled “Choose column types”Use the named helpers for common WordPress table columns:
| Method | Database definition | Typical use |
|---|---|---|
bigIncrements( 'id' ) |
Unsigned BIGINT, auto-incrementing primary key |
Numeric row identifiers |
string( 'name', 191 ) |
VARCHAR with a configurable length |
Names, states, and short values |
unsignedInteger( 'count' ) |
Unsigned INT |
Non-negative counters and identifiers |
integer( 'position' ) |
Signed INT |
Counts and positions |
tinyInteger( 'enabled', 1 ) |
TINYINT |
Flags and small numeric values |
bigInteger( 'external_id' ) |
Signed BIGINT |
Large numeric values |
dateTime( 'created_at' ) |
DATETIME, optionally with precision from 1 to 6 |
WordPress-compatible timestamps |
text( 'excerpt' ) |
TEXT |
Medium text values |
longText( 'payload' ) |
LONGTEXT |
Serialized payloads and large text values |
Column modifiers can be combined on the declaration being configured. Inside src/Database/Tables/Reports_Table.php, for example:
Available modifiers are unsigned(), nullable(), notNull(), default(), autoIncrement(), and comment(). An explicit default( null ) is valid only on a nullable column.
Prefer bigIncrements() for the usual generated primary key. When applying autoIncrement() manually, use an integer column without a default, define only one auto-increment column in the table, and make it the first column in a primary, unique, or regular index. Foundation validates these requirements before executing schema SQL.
Use column() when the named helpers do not cover the required MySQL type. In src/Database/Tables/Reports_Table.php, import StellarWP\Foundation\Database\Table\Column with the other imports, then add the custom columns inside definition():
Add indexes
Section titled “Add indexes”In src/Database/Tables/Reports_Table.php, declare indexes after their columns. Index names must be unique within the table, and composite index columns are stored in the order provided:
In src/Database/Tables/Report_Lookup_Table.php, use primary() only for a custom primary key. bigIncrements() already creates the table’s primary key:
Apply an initial table definition
Section titled “Apply an initial table definition”The generated src/Database/Migrations/Create_Reports_Table.php passes the table object to Schema. The schema service uses dbDelta() and verifies the resulting definition before the migration is recorded as successful.
The --migration flag explicitly selects a create-table migration. Its generated down() method therefore drops the table and all of its data when the migration is rolled back.
You can generate the initial migration separately when the table class already exists:
Pass a fully qualified class when the table is outside the default Database\\Tables namespace:
Foundation never infers table ownership from the migration name. Only --create selects the destructive create-table rollback, so a migration named Create_Reports_Table without that option remains a generic, irreversible migration.
Migration IDs are permanent, byte-exact identifiers. The generator prefixes them with a sortable timestamp so migration history is easy to inspect, but execution follows provider contribution order rather than sorting by ID. Register providers and migrations in dependency order, and do not change an ID after the migration has been deployed.
Reconcile a later table change
Section titled “Reconcile a later table change”For later schema changes, update the table’s desired definition and generate a reconciliation migration with the table it changes. For example, in src/Database/Tables/Reports_Table.php, remove the existing $table->index( 'status', 'status' ); declaration, then add published_at and its replacement composite index:
The generated src/Database/Migrations/Update_Reports_Schema.php receives Reports_Table and calls Schema::createOrUpdate() from up(). Because dbDelta() cannot remove the old index, update the generated method to remove that unsupported physical state before reconciling the complete definition. Its down() method remains irreversible until the developer supplies a safe inverse.
The table class represents the application’s current desired schema, not a historical snapshot stored with each migration. On a fresh installation, the original create-table migration may therefore create the latest table shape before later reconciliation migrations run. Keep reconciliation migrations idempotent, and do not make data migrations depend on observing an exact historical table definition.
Use Schema::execute() for trusted schema SQL when an upgrade requires a specific intermediate state or when dbDelta() cannot express the change reliably. Implement down() explicitly only when the inverse preserves the intended data and schema.
Write data migrations
Section titled “Write data migrations”For equality-based data changes, use the table’s write methods so the migration needs only the table it changes. For example, src/Database/Migrations/Backfill_Report_Status.php can update existing rows without coordinating a separate database service:
Generate a generic migration without --create or --table, then add the table constructor dependency manually. The --table option means “reconcile this table’s current schema,” so using it would also generate a Schema::createOrUpdate() call.
Choose the raw SQL API based on the statement. Database::execute() accepts WordPress placeholders followed by their bindings, so use it when a data migration includes request, configuration, or stored values. Schema::execute() accepts only a complete SQL string and does not bind placeholders; reserve it for trusted schema SQL whose identifiers and literals are fully controlled by the application.
Register migrations
Section titled “Register migrations”The generators update an existing database provider automatically. When registering classes by hand, bind table services and contribute migrations in dependency order from src/Database/Provider.php:
Foundation executes migrations in contribution order, so list schema prerequisites before data migrations that depend on them. If several feature providers contribute migrations, register those providers in the order their migrations must run.
Run migrations
Section titled “Run migrations”The migration command accepts one operation at a time:
| Goal | Command |
|---|---|
| Show migration status | wp your-plugin migrate |
| Create or reconcile migration storage | wp your-plugin migrate --initialize |
| Run every pending migration | wp your-plugin migrate --run |
| Roll back the latest batch | wp your-plugin migrate --rollback |
| Roll back and rerun all configured migrations | wp your-plugin migrate --refresh |
| Remove only the migration ledger | wp your-plugin migrate --drop-store |
The destructive --refresh and --drop-store operations prompt for confirmation. Add --yes only in an environment where the operation has already been approved.
Initialize migration storage
Section titled “Initialize migration storage”Create or reconcile Foundation’s migration ledger and lock table before running migrations:
Run this idempotent command during every deployment. Replace your-plugin with the configured command prefix; applications using the default prefix run wp nx migrate --initialize.
On WordPress multisite, run the command once for each site by passing WP-CLI’s --url global argument. Each site owns its migration ledger and lock table. See Use database services on multisite before migrating from code that calls switch_to_blog().
Apply pending migrations
Section titled “Apply pending migrations”Running the command without an operation displays migration status:
The runner acquires the configured migration lock, executes pending migrations in provider contribution order, and records each successful migration in one batch.
Run during deployment
Section titled “Run during deployment”A typical deployment initializes the Foundation tables, reviews pending work, runs it, and then confirms the final status:
--initialize is idempotent, so keep it in every deployment rather than branching between first installs and upgrades. Treat a failed command as a failed deployment step; do not continue serving code that expects a migration which did not complete.
Roll back or rebuild
Section titled “Roll back or rebuild”Roll back the latest applied batch:
Roll back every configured migration and run them again:
Drop only Foundation’s migration ledger when intentionally resetting migration history:
Run migrations from PHP
Section titled “Run migrations from PHP”WP-CLI is the preferred deployment interface. For controlled environments that cannot invoke WP-CLI, resolve the same Migrator service from the application container:
The result exposes the migration IDs that were run, rolled back, or skipped through its ran, rolledBack, and skipped properties. The programmatic API follows the same ledger and lock rules as the command. Do not run migrations during every normal WordPress request.
Testing
Section titled “Testing”Use wpunit tests for table definitions, schema reconciliation, and migrations that execute against WordPress. Use integration when the test proves contributions from multiple providers, and use wpcli for the real migration command lifecycle.
Create and remove application tables within the test lifecycle so tests exercise the real wpdb and dbDelta() behavior rather than a PHP fake.
For example, a project base test case that exposes the application container can resolve the real schema and table services in tests/wpunit/Database/ReportsTableTest.php:
Keep migration orchestration tests separate from table-definition tests. A migration test should initialize an isolated ledger, run the configured migration through Migrator, and assert both the schema effect and recorded status. Use the wpcli suite when the behavior under test is the command output, confirmation, or exit status.