soma-schema
Plain SQL database migrations for Rust — with full-file drift detection, manifest-defined ordering, and run-scoped advisory locking.
soma-schema is a standalone Postgres migration tool that ships as both a library crate and a CLI binary. You write plain SQL files with UP and DOWN sections, maintain a migration-order.yaml manifest that defines apply and rollback order explicitly, and soma-schema handles the rest: advisory locking so concurrent runners can't collide, SHA-256 checksums over the entire file so any post-deploy edit is caught, and atomic apply+track so a crash can't leave a half-applied state. It has no ORM dependency and no opinions about your application framework.
Why soma-schema
Most Rust migration tools order migrations by filename sort and hash only the UP SQL. That works until it doesn't: a filename collision, a rollback that re-creates a table FK dependencies expect to exist, or someone editing a deployed DOWN section and not noticing the drift. soma-schema addresses each of these directly.
-
Manifest-defined order.
migration-order.yamllists every migration explicitly. Apply order is that list top-to-bottom; rollback order is the exact reverse of manifest position — not filename sort. Deterministic FK-safe rollback without naming conventions. (sqlx-migrate, refinery, and diesel_migrations all order by filename.) -
Full-file checksum drift detection. The SHA-256 checksum covers the entire file — UP and DOWN together. Editing the DOWN section of a deployed migration is caught as
ChecksumDriftthe next time any command runs. (sqlx hashes UP-only; refinery hashes metadata differently; diesel has no drift detection.) -
Apply and track in one transaction. The migration SQL and its tracking-table row commit atomically. A crash between those two operations is not possible; you will never see a migration that ran but has no record, or a record for a migration that never ran.
-
Run-scoped advisory lock. A single Postgres advisory lock is acquired once at the start of
up,down, orstatusand held via a RAII guard until the call returns — even on panic. Concurrent runners block rather than collide.
Install
One command installs the CLI and wires up your project for migrations:
&&
This creates:
migrations/— directory withmigration-order.yaml, idempotent bootstrap SQL in00_setup/, and a runnable example migration in01_migrated/1/already listed in the manifest.AGENTS.md— agent-rules file at the repo root so any AI coding agent follows the soma-schema conventions from the first prompt.
init flags:
| Flag | Default | Description |
|---|---|---|
[DIR] |
migrations |
Where to scaffold the migrations directory |
--rules <agents|claude|cursor|windsurf|all|none> |
agents |
Which agent-rules file(s) to write or append to |
--skill |
off | Also install the /soma-schema Claude skill to ~/.claude/skills/soma-schema/SKILL.md |
--explore |
off | Open the visual migration explorer after scaffolding |
If the target file already exists, the soma-schema rules section is appended idempotently — existing content is never overwritten.
As a library
[]
= "0.3"
Or:
As a CLI only
The binary requires the cli feature, which is on by default.
AI-native: your agent writes the migrations
Traditional migration tools (Flyway, Liquibase, sqlx-migrate) were built for humans typing commands. soma-schema ships the agent contract so your AI follows the conventions from the start. Agents are non-deterministic — the runner enforces the invariants (checksum drift, FK-safe ordering, never editing an applied file) to catch the mistakes they still make.
soma-schema init writes AGENTS.md for you. You can also paste the block manually into an existing CLAUDE.md or .cursor/rules/ file (works with any agent — Claude, Cursor, Copilot, etc.). See the canonical rules text in AGENTS.md.
60-second quickstart
# Install and scaffold — one command.
&&
# Set your connection URL.
# Apply everything pending (including the runnable example migration).
# Check what's applied and what's pending.
# Open the visual explorer.
# Roll back the last migration.
# Roll back the last 3 migrations.
DATABASE_URL can also be set as an environment variable; --database-url overrides it.
CLI flags
| Flag | Env var | Default | Description |
|---|---|---|---|
--database-url |
DATABASE_URL |
— | Postgres connection URL |
--migrations |
— | migrations |
Path to migrations root |
--schema |
— | (connection default) | Target schema |
--table |
— | 00_schema_migrations |
Tracking table name |
Visual explorer
soma-schema can build a self-contained HTML explorer from your migrations directory — no database connection needed.
This writes an HTML file and opens it in your browser. The page shows a schema ERD, a version-grouped migration timeline, and seed-data tables.
| Flag | Default | Description |
|---|---|---|
--format html|json |
html |
HTML page or raw JSON |
--out <path> |
temp file (html) / stdout (json) | Write to a specific path |
--no-open |
off | Skip opening the browser |
Migration file format
Each .sql file holds both directions separated by a delimiter line:
-- UP section (everything before the separator)
(id UUID PRIMARY KEY DEFAULT gen_random_uuid);
-- DOWN ==
IF EXISTS myschema.widgets;
The separator is exactly -- DOWN == (trimmed). The DOWN section is optional — soma-schema will error if you attempt to roll back a migration that lacks one.
Seeds are ordinary migration files whose UP SQL is idempotent (ON CONFLICT DO NOTHING).
Migration layout
migrations/
migration-order.yaml # authoritative ordered manifest
00_setup/ # idempotent bootstrap (schema, extensions, grants)
01_schema.sql # runs before every up(); untracked
01_migrated/ # deployed, treat as immutable
1/ # version 1 (integer folder name, sorted numerically)
20260101_01_init.sql
20260101_02_seed.sql
02_inprogress/ # staging area for in-flight work (optional)
2/
20260201_01_add_widgets.sql
migration-order.yaml
This file is the single source of truth for what migrations exist and in what order:
manifest_version: 1
versions:
- version: 1
description: "Initial schema"
migrations:
- file: "20260101_01_init.sql"
created: "2026-01-01"
author: "you"
why: "Create core tables"
- file: "20260101_02_seed.sql"
created: "2026-01-01"
why: "Seed reference data"
Rules:
- Every
.sqlfile under a version folder must be listed here. A file on disk but missing from the manifest →OrphanMigrationerror. - Every manifest entry must resolve to a real file →
MissingFileerror. fileis a bare filename (no path separators, must end in.sql).00_setupfiles are not listed here and are never orphan-flagged.- Versions are sorted numerically (
10comes after2).
Tracking table
soma-schema creates a table (default 00_schema_migrations) in the configured schema:
| Column | Type | Description |
|---|---|---|
version |
INTEGER | Version-folder number |
file |
VARCHAR(255) | Migration filename |
name |
VARCHAR(255) | Filename without .sql |
checksum |
TEXT | SHA-256 of the full file |
description |
TEXT | why from migration-order.yaml |
batch |
INTEGER | Increments once per up() run that applies at least one migration |
applied_at |
TIMESTAMPTZ | When it was deployed |
applied_by |
TEXT | DB role that deployed it |
execution_ms |
INTEGER | How long the migration took |
revert deletes the row; the migration becomes pending again. status shows applied rows plus pending migrations from the manifest.
Library usage
use ;
use PgPoolOptions;
async
Key types re-exported at the crate root:
| Type | Description |
|---|---|
Migrator |
Owns the migrations root; drives up, down, status, scaffold |
PostgresDriver |
Postgres implementation of MigrationDriver |
PostgresConfig |
Driver config: schema, table, advisory_lock_key |
MigrationDriver |
Trait to implement for additional database backends |
MigrationStatus |
Return type of status(): applied + pending lists |
AppliedMigration |
A row from the tracking table |
PendingMigration |
A manifest entry not yet applied |
Error, Result |
Crate error type and alias |
How it compares
| Tool | Lang | Format | Ordering | Checksum | Locking | Lib+CLI | License |
|---|---|---|---|---|---|---|---|
| soma-schema | Rust | Plain SQL | YAML manifest | Full-file (UP+DOWN) | Advisory, full-run | Both | Apache-2.0 |
| sqlx migrate | Rust | Plain SQL | Filename lexical | UP-only | Advisory (Pg) | Both | MIT/Apache |
| refinery | Rust | Plain SQL | Filename lexical | Metadata hash | None | Lib only | MIT |
| diesel_migrations | Rust | Plain SQL | Filename sort | None | None | Lib (ORM-tied) | MIT/Apache |
| dbmate | Go | Plain SQL | Filename sort | None | Advisory | CLI only | MIT |
| Flyway | JVM | Plain SQL | Version prefix | Full-file | Advisory | Both | Apache + commercial |
| Atlas | Go | HCL/SQL DSL | State-based diff | Schema hash | Advisory | Both | Apache/BSL |
Full analysis with per-tool comparison and "when NOT to choose soma-schema" guidance: docs/competitor-analysis.md.
Multi-DB design
The MigrationDriver trait in src/driver.rs is object-safe and database-agnostic. Six async operations — acquire lock, run setup, ensure tracking table, list applied, apply, revert — define the contract. PostgresDriver is the only bundled implementation today. Implementing a new backend means implementing MigrationDriver; no other code changes.
The manifest ordering, full-file checksum detection, run-scoped locking, and atomic apply+track all live above the driver and are reused by every backend.
Database support tiers
| Tier | Backends |
|---|---|
| Stable | PostgreSQL — full advisory lock, atomic apply+track, full-file drift detection |
| Committed (next) | SQLite — same UP/DOWN format, BEGIN IMMEDIATE lock; the SQLite-now → Postgres-later path |
| Planned | MySQL / MariaDB (GET_LOCK); CockroachDB (validate + document through the Postgres driver) |
| Exploratory | SurrealDB (SurrealQL migrations); MongoDB (idempotent change ops — honest caveat: atomic apply+track is best-effort without multi-doc transactions); DuckDB |
| Community-welcome | Any backend via MigrationDriver in src/driver.rs — open an issue to coordinate |
Planned features (database-agnostic)
--dry-run (print SQL without executing) · up --steps N · status --json for CI · --lock-timeout (non-blocking acquire) · verify (checksum recheck without applying) · repair / baseline (adopt an existing database) · new / generate (scaffold migration + manifest entry) · migration squash · structured timing in status output.
Full roadmap, driver contribution guide, and feature details: ROADMAP.md.
Tests
Unit tests run without a database:
Integration tests require a real Postgres instance:
TEST_DATABASE_URL="postgresql://user:pass@host:5432/db"
Every integration test generates a unique throwaway schema (_sdm_test_<uuid>), runs inside it, and drops it on teardown — even on panic. Tests never touch public or any pre-existing schema.
Contributing
Issues and pull requests are welcome at github.com/chaitugsk07/soma-schema.
License
Licensed under Apache-2.0. See LICENSE.