Skip to content

Database Migrations

SQL migrations live in zmsbackend/migrations/. The migration runner is bin/migrate in the zmsbackend module:

bash
zmsbackend/bin/migrate

For a full local database reset (base schema, test data, migrations, cron jobs), see Local Database and Cache Operations.

Running migrations locally

DDEV

Check which migrations are pending (dry run):

bash
ddev exec "cd zmsbackend && bin/migrate"

Apply all pending migrations:

bash
ddev exec "cd zmsbackend && bin/migrate --update"

Apply only expand-phase migrations (see below):

bash
ddev exec "cd zmsbackend && bin/migrate --update --phase=expand"

Apply only contract-phase migrations:

bash
ddev exec "cd zmsbackend && bin/migrate --update --phase=contract"

Podman

Check which migrations are pending (dry run):

bash
podman exec -it zms-web bash -lc "cd zmsbackend && bin/migrate"

Apply all pending migrations:

bash
podman exec -it zms-web bash -lc "cd zmsbackend && bin/migrate --update"

Apply only expand-phase migrations:

bash
podman exec -it zms-web bash -lc "cd zmsbackend && bin/migrate --update --phase=expand"

Apply only contract-phase migrations:

bash
podman exec -it zms-web bash -lc "cd zmsbackend && bin/migrate --update --phase=contract"

Locally you usually run --update without --phase to apply everything at once. The phase flags are mainly for testing the same order used in deployment.

Expand and contract

Breaking schema changes (renames, drops) are split into two steps so old and new application code can run at the same time during rollout:

  1. Expand — add the new schema (new column, view, backfill, sync triggers). Old code keeps working.
  2. Deploy code — release that uses the new names.
  3. Contract — remove the old schema (drop column, drop view, drop triggers). Only safe once nothing uses the old names.

Additive changes (ADD COLUMN, new tables) only need an expand step — no contract file.

For the full deployment pipeline and MariaDB patterns, see Zero-downtime deployments and database migrations.

Filename prefixes

All migration files go in zmsbackend/migrations/. The filename tells the runner which phase applies:

Change typeFilename pattern--phaseExample
Additive only (ADD COLUMN, new table, indexes, seeds)no prefixexpand20260702-add-standort-custom-text-field3.sql
Expand step of a rename or dual-column transition*-expand-*expand20260702-expand-standort-add-scope-id.sql
Contract step (drop column, drop view, drop trigger)*-contract-*contract20260702-contract-drop-standort-view.sql

Quick rules:

  • ADD COLUMN → unprefixed (or *-expand-* if paired with a later contract). Never *-contract-*.
  • DROP COLUMN / DROP TABLE / DROP VIEW*-contract-* only. Never unprefixed.
  • RENAME COLUMN / RENAME TABLE → never unprefixed. Use an *-expand-* + *-contract-* pair in the same release.

Without --phase, bin/migrate runs every pending file (legacy behaviour). With --phase=expand, it skips *-contract-* files. With --phase=contract, it runs only *-contract-* files.