RocketBoost Orbit logo Orbit rocketboost.rs

Migrations

A migration changes the schema. Orbit's migrations are folders of SQL. The migrator applies them, records each one in a migrations table with its batch number, and rolls back the last batch on request.

Layout

Each migration is a folder named <timestamp>_<name>, with an up.sql that applies it and a down.sql that reverts it:

migrations/
  20261108000000_create_users/
    up.sql
    down.sql
  • The timestamp is 14 digits, YYYYMMDDHHMMSS, followed by an underscore and a name.
  • No two migrations may have the same name, or the same timestamp.
  • Migrations run in name order, which is timestamp order.
  • When reading a folder, Orbit skips hidden entries, whose names start with a dot.

Run, check and roll back

Read the folder with Migrator::from_dir, then call the migrator with the database:

use orbit_orm::{Database, Migrator};

let db = Database::connect(&url).await?;
let migrator = Migrator::from_dir("migrations")?;

migrator.run(&db).await?;                  // applies every pending migration as one batch
let status = migrator.status(&db).await?;  // each migration as Applied (with its batch) or Pending
migrator.rollback(&db).await?;             // reverts the last batch, in reverse order
  • run applies every pending migration, in name order, as one new batch. Each migration runs in its own transaction. The run stops at the first failure, which is returned as MigrationFailed with the migration's name. The migrations before it stay applied. It returns the names it applied.
  • status lists each migration of this migrator as State::Applied { batch } or State::Pending. Applied migrations that have no folder here are not listed.
  • rollback reverts the last batch. It runs each down.sql in reverse name order, each in its own transaction. Before it reverts anything, it checks that every migration in the batch has a folder here. It returns the names it reverted, or nothing when no migration has run.

The migrations table is created the first time a migrator reads or changes it.

Orbit has no command-line tool. Call the migrator from your own code, for example from a binary you write.

DDL is not rolled back

MySQL does not roll back DDL, such as CREATE TABLE. A migration that fails can leave the DDL it ran before the failure in place. Only its data changes are rolled back.

One process at a time

run, rollback and import_diesel_history hold a MySQL lock named orbit_migrations for the whole run. A second process waits up to 10 seconds. If the lock is still held, the call fails with MigrationsLocked. Change the wait with with_lock_timeout:

use std::time::Duration;

let migrator = Migrator::from_dir("migrations")?
    .with_lock_timeout(Duration::from_secs(30));

status does not take the lock.

Embedded migrations

The migrations! macro embeds the files in the binary at build time. Its first argument is the folder, relative to the crate root. Then list the migration names by hand. A missing file is a build error. The macro returns Result<Migrator, Error>:

let migrator = orbit_orm::migrations!("migrations", "20261108000000_create_users")?;

A migrator can also be built from code, with Migration::new(name, up, down) and Migrator::new(vec).

Importing a Diesel history

To move a project from Diesel, record the migrations that Diesel has already applied. import_diesel_history reads Diesel's __diesel_schema_migrations table and records each migration with a matching folder that is not yet recorded. It records them in one new batch, in name order. It runs no up.sql, and it does not change Diesel's table. A second call imports nothing. When Diesel's table does not exist, it returns nothing.

let imported = migrator.import_diesel_history(&db).await?;
let unmatched = migrator.unmatched_diesel_versions(&db).await?;

unmatched_diesel_versions lists the versions in Diesel's table that have no folder in this migrator, in order. The import leaves them out, so you decide what to do with them.