Skip to main content
rbs ships a built-in SQL migration engine: versioned and repeatable .sql files live in your package, a migration_database declaration in the build file names the database they belong to, and rbs migrate applies and tracks them. It supports PostgreSQL, MySQL, and SQLite, with checksummed history, a database-side lock so concurrent migrators cannot interleave, and a dry-run mode.

Declaring a database

migration_database registers configuration — it is not a build target. It produces nothing under rbs build, and you address it by name (rbs migrate apply app_db), from any directory in the workspace: the command loads every package, so the declaration is found wherever it lives.

Migration files

Two kinds of file live in migrations_dir:
  • VersionedV{unix_timestamp}__{description}.sql. Runs exactly once, in version order (e.g. V1709141200__create_users_table.sql).
  • RepeatableR__{description}.sql. Re-runs whenever its checksum changes — the natural home for views, functions, and grants.
Always create files with the CLI rather than hand-naming them, so the version stamp is correct:
Placeholders from the declaration substitute into the SQL as ${key}:
An unresolved ${...} token fails the migration and points back at the placeholders map on the rule.

Selecting the database

The connection string comes from the environment variable named by database_url_env, read when you invoke rbs migrate:
The engine is inferred from the URL scheme — no engine attribute exists: SQLite makes a handy local fast path: export DATABASE_URL=file:./dev.db runs the same migration files against a local file.

Per-environment databases

The simplest per-environment story is exporting a different URL in each context — your shell, CI, a deploy pipeline. Because migrations against a cluster database are usually run over a port-forward or from inside the network, this composes naturally with a Kubernetes deploy:
The global -e flag (or RBS_ENV) selects one of your declared environments before workspace files execute, and the env module is available in every build file — so a package can register a different configuration per environment, for example a different variable name:

Commands

apply

Applies all pending versioned migrations, in order, plus any repeatable migrations whose checksum changed. Behavior worth knowing:
  • Each migration runs in its own transaction. On failure the migration is recorded as failed and execution stops.
  • A migration whose leading comments contain -- rbs:no-transaction runs its statements individually outside any transaction — for statements like CREATE INDEX CONCURRENTLY that refuse one.
  • Concurrent applies are safe. A database-side lock serializes migrators, so two CI jobs applying at once cannot interleave.
  • A pending migration versioned lower than one already applied is rejected — the classic stale-branch-merged-late case. Pass --out-of-order to apply it anyway, or re-create it with a fresh timestamp.
  • --dry-run prints the SQL that would run without touching the database (and skips the lock).

validate

Checks that reality matches the files on disk, exiting non-zero on any issue — put it in CI:
  • files modified after being applied (checksum mismatch)
  • files deleted after being applied
  • previously failed migrations that need attention

baseline

Adopting rbs migrations on a database that already has its schema? Baseline marks all versioned migrations up to and including the given version as applied without executing them:

repair

The recovery command, for after you have fixed a broken migration or deliberately edited an already-applied file. It removes history entries for failed migrations (so they can be retried) and realigns stored checksums with the current files on disk.

A typical workflow

And in CI, before deploying: