Vortos
Migrations

Running Migrations

Run pending migrations safely with preflight checks, advisory locking, dry runs, and production flags.

Running Migrations

Run all pending migrations:

php vortos migrate

Production deployment command:

php vortos migrate --force --no-interaction

Preview without executing:

php vortos migrate --dry-run --force

Preflight

Before executing SQL, Vortos checks pending module migrations against the live schema.

If a pending migration owns objects that already exist, migration stops:

Schema drift detected. No migrations were executed.

→ App\Migrations\Version20260505114121 (Authorization/001_authorization_rbac.php)
  Drift: compatible existing schema
  existing table: role_permissions
  existing table: user_roles

If the existing schema is correct, run:
  php vortos migrate:adopt VERSION --verify

This prevents duplicate table errors and avoids hiding drift behind IF NOT EXISTS.

Post-Deploy Verification

After deployment, run migrate:verify to confirm the live schema matches what the migrations say it should:

php vortos migrate:verify
  ✔ App\Migrations\Version20260505114121
  ✔ App\Migrations\Version20260507083200

All 2 executed migration(s) verified — schema is clean.

Exit code 0 means every executed framework migration's tables and columns are present. Exit code 1 means drift — the output lists exactly what is missing:

  ✘ App\Migrations\Version20260505114121  [partial]
      missing table:  role_permissions

Add --json for machine-readable output in CI pipelines. Only framework module migrations are verified — user-authored migrations are skipped.

PostgreSQL Schema Bootstrap

When your DATABASE_URL uses a pgsql:// or postgres:// scheme, vortos:migrate automatically runs:

CREATE SCHEMA IF NOT EXISTS vortos

before executing any migrations. You do not need to create the schema manually or add it to your deployment scripts.

vortos:migrate:fresh drops the vortos schema completely (including all its tables) before re-running all migrations from scratch. This gives a guaranteed clean slate in development and CI.

Opting out

Add ?vortos_prefix=true to your DSN to use underscore prefix mode (vortos_table_name) instead of schema mode. No schema is created in prefix mode. See the DBAL docs for details.

Locking

On PostgreSQL, vortos:migrate uses an advisory lock so two deployment runners cannot migrate at the same time.

php vortos migrate --lock-timeout=60

If the lock cannot be acquired:

Another migration process is already running.

--no-lock exists for unusual local/debug workflows. Do not use it in production.


ORM and Framework Migrations Together

vortos:migrate runs all pending migrations in a single command, regardless of their source:

  • Framework module tables come from SQL stubs published via vortos:migrate:publish — the generated classes now carry a #[DeployPhase(MigrationPhase::Expand)] attribute (or Contract when the SQL is destructive), so they pass vortos:migrate:analyze out of the box
  • ORM entity tables come from diffs generated via vortos:orm:diff

Both land in the same migrations/ directory and are run together by vortos:migrate. There is no separate ORM migration runner — one command covers everything.

Standard workflow when using Doctrine ORM:

# 1. Publish any new framework module migrations
php vortos migrate:publish

# 2. Generate an ORM diff for entity changes
php vortos vortos:orm:diff

# 3. Run everything — both framework tables and ORM entity tables
php vortos migrate

Review the generated migration file in migrations/ before running step 3. The diff command generates plain SQL — it does not execute anything.

One migrations/ directory

Framework table migrations and ORM entity migrations are indistinguishable to vortos:migrate — it runs them in timestamp order. Keep generated migration files in version control alongside hand-written ones.


Creating Migrations

vortos:migrate:make

Generates an empty Doctrine migration class ready to be filled in:

php bin/console vortos:migrate:make CreateOrdersTable

Creates migrations/VersionYYYYMMDDHHIISS.php with empty up() and down() stubs.

--aggregate flag

For DBAL aggregate tables, pass --aggregate with the table name. The migration is pre-filled with id and lock_version — the two columns every aggregate table requires:

php bin/console vortos:migrate:make orders --aggregate

Generated up():

CREATE TABLE IF NOT EXISTS orders (
    id           VARCHAR(36) NOT NULL,
    lock_version INTEGER     NOT NULL DEFAULT 0,

    PRIMARY KEY (id)
)

Add your domain columns below lock_version, then run vortos:migrate.

DBAL only

The --aggregate flag is for DBAL-backed aggregates only. ORM users generate migrations via vortos:orm:diff — Doctrine reads lock_version from the #[ORM\Version] annotation automatically.

On this page