Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

Migrations

Elyra's php artisan migrate. Migrations are SQL files applied in order, tracked in a _elyra_migrations table, and grouped into batches so rollback can undo the most recent migrate as a unit.

File layout

Migrations live in the directory set by [database].migrations (default migrations/), named <version>_<name>.sql (the "up"), with an optional <version>_<name>.down.sql (the "down", for rollback):

migrations/
├── 0001_create_todos.sql
├── 0001_create_todos.down.sql
├── 1751800000_add_users.sql
└── 1751800000_add_users.down.sql

version is a numeric, sortable prefix. make:migration uses a unix timestamp.

-- 0001_create_todos.sql
CREATE TABLE IF NOT EXISTS todos (
    id    INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    done  INTEGER NOT NULL DEFAULT 0
);
-- 0001_create_todos.down.sql
DROP TABLE todos;

CLI

rata make:migration create_todos   # scaffold up + down files
rata migrate                       # apply all pending (as one new batch)
rata migrate:status                # list applied/pending
rata migrate:rollback              # undo the most recent batch (runs .down.sql)

rata connects directly using [database] from elyra.toml (or DATABASE_URL) — no app binary required, as long as the project has no Rust migrations (then it runs the app's migrate mode).

Programmatic use

The framework can run migrations at runtime too (e.g. auto-migrate on boot):

let db = elyra::Database::connect(url).await?;
db.migrator("migrations").run().await?;             // Vec of applied versions
db.migrator("migrations").status().await?;          // Vec<MigrationStatus>
db.migrator("migrations").rollback().await?;         // Vec of rolled-back versions

The schema builder

Writing per-driver DDL by hand defeats the point of one Database across SQLite/MySQL/Postgres. Schema renders it for you:

use elyra::db::{Driver, Schema};

let statements = Schema::create("users", |t| {
    t.id();                                  // per driver: AUTOINCREMENT / AUTO_INCREMENT / BIGSERIAL
    t.string("email").unique();
    t.string("name").nullable();
    t.integer("age").default_value("0");
    t.boolean("active").default_value("1");  // INTEGER 0/1, matching #[derive(Model)]
    t.foreign_id("team_id", "teams").on_delete_cascade();
    t.nullable_foreign_id("owner_id", "users"); // an optional parent
    t.timestamps();                          // created_at + updated_at
    t.soft_deletes();                        // nullable deleted_at
    t.index("email");
})
.to_sql(Driver::Sqlite);

Also available: Schema::table(..) for ALTER (add_string / add_integer / drop_column / rename_column / index / unique_index / drop_index), Schema::drop, drop_if_exists, rename, and column helpers text, float, json, blob, big_integer, timestamp, string_len, primary(&[..]).

Run it directly when you need to:

Schema::create("users", |t| { t.id(); }).execute(&db).await?;

Rust migrations

RustMigration pairs the schema builder with the same history table and batch semantics as the .sql files, so you can mix both:

use elyra::db::{Driver, RustMigration, Schema};

pub struct CreateUsers;

impl RustMigration for CreateUsers {
    fn version(&self) -> &str { "20260801120000" }
    fn name(&self) -> &str { "create_users_table" }

    fn up(&self, driver: Driver) -> Vec<String> {
        Schema::create("users", |t| {
            t.id();
            t.string("email").unique();
            t.timestamps();
        })
        .to_sql(driver)
    }

    fn down(&self, driver: Driver) -> Vec<String> {
        Schema::drop_if_exists("users").to_sql(driver)
    }
}

Register them:

App::new()
    .database("sqlite://app.db?mode=rwc")
    .migrations(vec![Box::new(CreateUsers)])
    .run()

Then rata migrate, rata migrate:status and rata migrate:rollback work as for SQL files. When src/ has a Rust migration, rata runs the app in its migrate mode, which knows them. That mode handles the SQL files in the same pass, so the two kinds mix freely:

  • migrate applies everything pending, in version order, as one batch.
  • rollback runs each migration's own down: the Rust one's down, or the SQL one's .down.sql.
  • A batch holding a migration that's neither, such as a Rust one that's no longer registered, isn't rolled back. Elyra won't forget a migration whose tables are still there.

The mode works without rata too, with no window:

ELYRA_MIGRATE=up     cargo run    # apply pending migrations
ELYRA_MIGRATE=down   cargo run    # roll back the most recent batch
ELYRA_MIGRATE=status cargo run    # list them

In Rust: Migrator::run_all, rollback_all and status_all take the registered Rust migrations alongside the directory.

rata make:migration create_users_table --rust scaffolds the file above under src/migrations/ and prints the wiring step.

Seeders

use elyra::seeder::{BoxSeed, Seeder};
use elyra::db::Database;

struct DemoData;

impl Seeder for DemoData {
    fn name(&self) -> &str { "DemoData" }

    fn run<'a>(&'a self, db: &'a Database) -> BoxSeed<'a> {
        Box::pin(async move {
            elyra::db::sqlx::query("INSERT INTO tags (name) VALUES ('demo')")
                .execute(db.pool())
                .await?;
            Ok(())
        })
    }
}

App::new().database(url).seeder(DemoData).run()
ELYRA_SEED=1 cargo run

Portability notes

  • The tracking table uses portable types (VARCHAR, INTEGER, BIGINT).
  • Migration files run via sqlx::raw_sql, so multiple ;-separated statements in one file are supported.
  • Migrations run per-statement without a wrapping transaction (so MySQL's non-transactional DDL behaves) — write idempotent-friendly SQL.
  • Column-type tip: for boolean-ish columns use INTEGER (0/1). The Any driver can't read SQLite's native BOOLEAN type; models map bool fields to INTEGER.

Related