Database migrations
Database migrations let us keep schema changes in version control and apply them in a predictable order, so development machines and production servers are ensured to be running the same schema.
[!NOTE] In WebEngine, the same migrator is surfaced through
gt migrate, so the file layout and flags on this page apply directly to a WebEngine project. The WebEngine overview is at https://www.php.gt/webengine/database/.
Storing migration files
The default migration directory is:
query/_migration/
Migration files must:
- end with
.sqlor.php. - start with a numeric prefix.
- use a continuous numerical sequence with no gaps.
- keep the same contents once they have been run.
Examples:
0001-create-user.sql0002-add-created-at.sql0003-index-user-email.sql
The numeric prefix must be at the start of the filename. A file like a002-second.sql is ignored rather than treated as migration 2.
What the migrator stores
The migrator keeps its state in a database table, _migration by default.
For each migration it records:
- the migration number.
- a hash of the file contents.
- when it was applied.
That hash is what makes integrity checks possible. If we edit a migration after it has already run, the migrator will stop and tell us the history has changed.
Running migrations in code
use Gt\Database\Connection\Settings;
use Gt\Database\Migration\Migrator;
$settings = new Settings(
"query",
Settings::DRIVER_SQLITE,
"app.sqlite"
);
$migrator = new Migrator($settings, "query/_migration");
$migrator->createMigrationTable();
$files = $migrator->getMigrationFileList();
$migrator->checkFileListOrder($files);
$current = $migrator->getMigrationCount();
$migrator->checkIntegrity($files, $current);
$migrator->performMigration($files, $current);
That flow is safe to rerun. Only pending migrations will be applied.
CLI usage
The package ships with bin/migrate, and WebEngine exposes the same behaviour through gt migrate.
php bin/migrate execute
php bin/migrate execute --force
php bin/migrate execute --reset
php bin/migrate execute --reset 3
What the flags do:
- no flag: apply pending canonical migrations
--force: drop the schema and run again from migration1--reset: rerun only the latest canonical migration--reset 3: rerun from migration4onwards
For SQLite, --force resets the database by removing the database file and rebuilding it from the migration set.
Development migrations
For feature branches, there is also a dev migration flow:
query/_migration/dev/
These files are tracked separately in the _migration_dev table, and are intended for branch-local schema changes while work is still in progress.
Use:
migrate --dev
migrate --dev-merge
--devruns canonical migrations first, then branch-local dev migrations.--dev-mergepromotes applied dev migrations into canonical files inquery/_migration/, automatically renaming to the numerical sequence.
This keeps feature branch work flexible, while still ending up with one canonical migration history in the main branch.
[!TIP] A sensible workflow is to let each developer keep a local sequence in
query/_migration/dev/, then have the feature author rungt migrate --dev-mergebefore the work is merged.
Configuration keys
The CLI and migrators read these settings:
database.query_pathdatabase.migration_pathdatabase.migration_tabledatabase.dev_migration_pathdatabase.dev_migration_tabledatabase.driverdatabase.schemadatabase.hostdatabase.portdatabase.usernamedatabase.password
Example: https://github.com/PhpGt/Database/blob/master/example/05-database-migrations.php
To see these features in runnable scripts, move on to Examples.