Migrations
Numbered SQL migrations driven by the Nibiru CLI.
Migrations are flat SQL files in application/settings/config/database/, executed in numeric order. The CLI tracks which files have run and skips them on subsequent invocations.
File naming
Section titled “File naming”NNN-<slug>.sqlNNN: zero-padded three-digit number for sort order.<slug>: kebab-case description (add-account-email,create-acl-data).
Example progression:
001-acl.sql002-account.sql003-api_registry.sql004-timeanddate.sql005-user.sql006-user_to_account.sql007-timeanddate_to_account.sql008-user_to_acl.sql009-account_to_api_registry.sql010-timeanddate_to_user.sql011-acl-data.sql012-add-unique-key-user.sql013-add-account-email.sqlRunning migrations
Section titled “Running migrations”./nibiru -mi local # APPLICATION_ENV=developmentAPPLICATION_ENV=staging ./nibiru -mi stagingAPPLICATION_ENV=production ./nibiru -mi productionThe {env} argument and APPLICATION_ENV should agree. Migrations target the database configured in settings.<env>.ini under [DATABASE].
What the runner does
Section titled “What the runner does”For each *.sql file in numeric order:
- Looks up its filename in the
_migrationstable. - If absent, opens a transaction (where the driver supports DDL transactions), executes the file, and inserts a record on success.
- If a statement fails, rolls back and exits non-zero.
The _migrations table is created automatically on first run.
Idempotent SQL
Section titled “Idempotent SQL”Always write migrations that can re-run without errors after a partial failure:
CREATE TABLE IF NOT EXISTS api_registry ( api_registry_id INT(11) NOT NULL AUTO_INCREMENT, api_registry_name VARCHAR(255) NOT NULL, PRIMARY KEY (api_registry_id), UNIQUE KEY api_registry_name_uk (api_registry_name)) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;For PostgreSQL:
CREATE TABLE IF NOT EXISTS api_registry ( api_registry_id SERIAL PRIMARY KEY, api_registry_name TEXT NOT NULL UNIQUE);For ALTER statements, prefer IF NOT EXISTS clauses on supported engines:
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS user_email VARCHAR(255);If your engine doesn’t support IF NOT EXISTS on alter, wrap the statement in a guard query that no-ops when the change already exists.
Reset commands
Section titled “Reset commands”./nibiru -mi-reset local # forget all applied migrations./nibiru -mi-reset-file 005-user.sql local # forget a single fileThe single-file form is useful when you’ve fixed a bug in a previously-applied migration and want to re-run only that file.
Branch hygiene
Section titled “Branch hygiene”Two engineers working on parallel branches can both add 015-… and collide. Conventions that help:
- Reserve numbers in pull-request titles before writing the SQL.
- Use a generous gap for hotfixes (e.g., reserve
099,199,299for emergency cherry-picks). - Prefer additive migrations (new tables, new columns) over destructive ones (drops). They merge more cleanly.
Squashing
Section titled “Squashing”For long-lived projects, periodically collapse old migrations into a single seed file representing the current schema. Move the originals to archive/ so the audit trail still exists, and create a new 000-baseline-2026.sql that creates everything at once. Update the migrations runner with a manual INSERT INTO _migrations to mark old files as applied.
Schema-first models
Section titled “Schema-first models”When [GENERATOR] database = true, models are regenerated from the live schema after each migration. So a typical deploy is:
./nibiru -mi production# Generator regenerates models on the next request../nibiru -cache-clearFor zero-downtime deploys: turn off the generator (database = false) and check in the regenerated models alongside the migrations they depend on.