new MigrationSetup(?array $config = null, array|string|null $projectRoot = null)No framework dependency. Migrations resolves the database connection itself — you never pass a PDO. ConfigLoader checks Flight::get('db') first (if Flight is loaded), then app/config/migrations.php. There is no other config file. See Configuration for details.
Pass $config to add extra paths/seeds and merge other settings at runtime. This array is additive: its paths and seeds.paths are appended to the resolved config (deduped) regardless of path_mode; other keys merge recursively. Pass $projectRoot to set the base directory for resolving relative paths (defaults to RUNWAY_PROJECT_ROOT, PROJECT_ROOT, or getcwd()).
The legacy positional form new MigrationSetup(null, $config) still works (the null first argument is treated as no config).
| Method | Description | Return |
|---|---|---|
runMigrate(?string $moduleName = null) |
Run pending migrations and seeds for all packages (or one if $moduleName given). Versions resolved internally from composer/installed.json and the seeds table. |
array<string, ModuleResult> |
runMigrate behavior:
- On first run, creates infrastructure tables (
migrations,seeds,migrations_lock) and writes a.migrations_installedmarker file in the project root. Subsequent requests skip table creation entirely (file check only, no DB). - Preloads all installed versions (from
composer/installed.json) and seeded versions (single query) in bulk. - Compares pending migration files and seed versions against what's been run. If nothing is pending, returns early — no lock acquired, no batch created.
- When work is pending: acquires the lock, runs migrations and seeds in one batch, releases the lock.
- Infrastructure errors (no DB, lock held) throw. Per-package errors are caught and recorded in the result.
- Returns only packages where something ran, keyed by package name.
Seed logic: fresh install (no prior seed record) runs the install block plus all versions <= installed version. Update runs only versions between the last seeded version and the installed version.
| Method | Description | Return |
|---|---|---|
runAll(bool $dryRun = false) |
Run all pending migrations across all discovered modules | MigrationResult[] |
runModule(string $moduleName, bool $dryRun = false) |
Run pending migrations for one module | MigrationResult[] |
rollbackLast(?string $module = null, bool $dryRun = false) |
Roll back most recent batch; scope to a module or global | MigrationResult[] |
purgeModule(string $moduleName, bool $dryRun = false) |
Run down() on all executed migrations for a module, remove tracking rows |
MigrationResult[] |
forceUnlock(bool $dryRun = false) |
Release the migration lock; $dryRun = true reports state without changing it |
array{is_locked: bool, locked_by: string|null, locked_at: string|null} |
setBreakpoint(string $version, string $package, bool $enabled = true) |
Set or clear the breakpoint flag on a migration; rollback stops before a set breakpoint | void |
getPendingMigrations() |
Return all pending migrations across all discovered modules | array<int, array{name: string, module: string, version: string, class: string}> |
getExecutedMigrations() |
Return all executed migrations from the tracking table | array<int, array{name: string, module: string, version: string, class: string, run_at: string, batch: int, breakpoint: bool}> |
Returned by runMigrate().
| Method | Return | Description |
|---|---|---|
getModuleName() |
string |
Composer package name passed to the lifecycle method |
isSuccess() |
bool |
false if any non-rollback migration failed or an exception was caught |
getMessage() |
?string |
Overall message; first failure message, or null if none set |
getVersion() |
?string |
Installed version of the package, set by runMigrate() |
getMigrationResults() |
MigrationResult[] |
Per-migration outcomes |
getSeedResults() |
SeedResult[] |
Per-seed-entry outcomes (seed failures do not flip isSuccess) |
hasMigrationFailure() |
bool |
true if any non-rollback migration result is a failure |
Value object for a single migration execution attempt.
| Method | Return | Description |
|---|---|---|
getName() |
string |
module:version (or module:version:rollback for rollback entries) |
getModule() |
string |
Composer package name |
isSuccess() |
bool |
Whether the migration ran successfully |
getMessage() |
?string |
'ok' on success, error string on failure |
Value object for a single seed entry (one table + rows pair).
| Method | Return | Description |
|---|---|---|
getModuleName() |
string |
Composer package name |
getContext() |
string |
'install' or a version string (e.g. '1.2.0') |
getTable() |
string |
Target table name |
isSuccess() |
bool |
Whether the seed entry was applied successfully |
getMessage() |
?string |
Row count on success, error string on failure |
MySQL table builder. Get an instance via $this->table() inside a migration class.
Supported column types: primary, integer, biginteger, string, char, text, mediumtext, longtext, boolean, datetime, timestamp, time, date, float, decimal, enum, set, blob, binary, json
Column options: nullable (bool), default (mixed), length (int), unsigned (bool), precision (int), scale (int), comment (string), after (string), first (bool), auto_increment (bool)
| Method | Description |
|---|---|
table(string $name): static |
Set the active table and reset column/index/FK state; required before any other call |
create(): void |
Execute CREATE TABLE with all defined columns, indexes, and FKs |
update(): void |
Apply all queued columns, indexes, and FKs to an existing table |
drop(): void |
Execute DROP TABLE IF EXISTS |
addColumns(): void |
Execute ALTER TABLE ... ADD COLUMN for columns only (use update() if you also need indexes or FKs) |
dropColumns(array $columns): void |
Execute ALTER TABLE ... DROP COLUMN for each named column |
rename(string $newName): void |
Execute RENAME TABLE |
modifyColumn(string $name, string $type, array $options = []): void |
Execute ALTER TABLE ... MODIFY COLUMN |
renameColumn(string $oldName, string $newName): void |
Execute ALTER TABLE ... RENAME COLUMN (MySQL 8.0+) |
hasTable(string $name): bool |
Return true if the table exists in the current database |
hasColumn(string $column): bool |
Return true if the column exists in the current table (requires prior table() call) |
dropIndex(string|array $indexOrColumns): void |
Drop an index by name, or pass a column array to look up the name from the database |
dropForeignKey(string|array $fkOrColumns): void |
Drop a FK by name, or pass a column array to look up the constraint name from the database |
statement(string $sql): void |
Run a raw SQL statement for anything the builder doesn't cover |
| Method | Description |
|---|---|
addColumn(string $name, string $type, array $options = []): static |
Queue a column definition; returns $this for chaining |
addIndex(array $columns, array $options = []): static |
Queue an index; options: unique (bool), name (string) |
addForeignKey(array $columns, string $refTable, array $refColumns, array $options = []): static |
Queue a FK constraint; options: delete, update (CASCADE|SET NULL|RESTRICT|NO ACTION), name |
If a migration fails partway through, the data saver puts every table it touched back the way it was, including data. Enabled automatically before each migration runs.
Two levels:
- Lightweight for non-destructive operations (
addColumns,addIndex,addForeignKey,modifyColumn,renameColumn,rename,dropIndex,dropForeignKey). Reads the table's current state from the database before the change. On failure, runs the opposite operation to undo it. - Heavy for destructive operations (
drop,dropColumns). Copies the table (structure + data) into a_bak_table before the operation. On failure, replaces the original from the copy. On success, drops the copy.
statement() (raw SQL) is not tracked. If you use raw SQL, you accept the risk.
| Method | Description |
|---|---|
enableSafetyNet(): void |
Enable and reset data saver state. Called by the runner before each migration |
disableSafetyNet(): void |
Disable the data saver |
restoreSafetyNet(): void |
Undo all recorded operations and restore any copied tables. Called by the runner on failure |
cleanupSafetyNet(): void |
Drop _bak_ tables after a successful migration |
| Method | Description |
|---|---|
startRecording(): void |
Start capturing operations for later reversal |
stopRecording(): array |
Stop capturing and return the recorded operations array |
reverseOperations(array $operations): void |
Undo each recorded operation, last to first |
Extend this class for all migration files. Filename must be YYYY-MM-DD-HHmmss_ClassName.php.
| Method | Description |
|---|---|
table(string $name): SchemaBuilder |
Get the schema builder for the named table |
up(): void |
Override for forward migration (paired with down()) |
down(): void |
Override for rollback (paired with up()) |
change(): void |
Override instead of up()/down() for automatically reversible migrations |
usesChange(): bool |
Returns true if the subclass overrides change() but not up(); used internally to choose execution path |
Concurrency lock backed by the migrations_lock table. Uses a compare-and-swap UPDATE, safe on MySQL row-level locking.
| Method | Return | Description |
|---|---|---|
acquire(): bool |
bool |
Attempt to acquire the lock; returns false if already held. Lock table must already exist (created by ensureTrackingTable() on first install). |
release(): void |
void |
Release the lock unconditionally |
isLocked(): bool |
bool |
Return true if the lock is currently held by any process |
getLockInfo(): ?array |
array{locked_by: string|null, locked_at: string|null}|null |
Return lock metadata if held, null if free |
ensureStore(): void |
void |
Create the lock table if it doesn't exist. Safe to call every time |
Table: migrations
| Column | Type | Description |
|---|---|---|
id |
INT UNSIGNED AUTO_INCREMENT |
Primary key |
version |
VARCHAR(32) |
Timestamp from filename (YYYY-MM-DD-HHmmss) |
class |
VARCHAR(255) |
Migration class name |
group |
VARCHAR(64) |
Always 'default' (reserved for multi-connection) |
package |
VARCHAR(255) |
Composer package name |
batch |
INT UNSIGNED |
Groups migrations executed together for rollback |
breakpoint |
TINYINT(1) |
Rollback stops before this migration when set |
reversal_ops |
TEXT NULL |
JSON-encoded operations for reversible migrations |
run_at |
DATETIME |
When the migration was applied |
Table: migrations_lock
| Column | Type | Description |
|---|---|---|
id |
INT (always 1) |
Single-row sentinel |
is_locked |
TINYINT(1) |
0 = free, 1 = held |
locked_by |
VARCHAR(64) |
PID of the holding process |
locked_at |
DATETIME |
When the lock was acquired |
Holds info about a backup copy of a table, created by the data saver before destructive operations.
| Method | Return | Description |
|---|---|---|
getTableName() |
string |
The original table name that was copied |
getBackupTableName() |
string |
The backup table name (e.g. _bak_users) |
getAutoIncrement() |
int |
The AUTO_INCREMENT value captured before copying. Restored when the backup replaces the original |
Table: seeds
| Column | Type | Description |
|---|---|---|
id |
INT UNSIGNED AUTO_INCREMENT |
Primary key |
package |
VARCHAR(255) UNIQUE |
Composer package name (one row per package) |
version |
VARCHAR(32) |
Last seeded version |
run_at |
DATETIME |
When the seed was last applied |