Makemigrations Internals
The runique makemigrations command is a sophisticated tool that bridges the gap between your Rust entities (model!{}) and the database schema. Unlike standard ORM generators, it is designed to preserve architectural intent and framework-specific extensions.
The Generated Data Pipeline
The generation process follows a three-pass architecture:
Pass 1: AST Extraction (parse_schema_from_source)
Runique uses a lightweight custom parser (based on syn and regular expressions for performance) to read your src/entities/*.rs files.
- Static Analysis: It doesn't compile your code. It reads the source files directly to extract the structure of
model!{}blocks. - Normalizer: It converts high-level DSL types (e.g.,
datetime,uuid) into internalFieldDefstructures. - Intelligence: This is where the Automatic Field Mapping happens (mapping field names like
emailto specialized form behaviors).
Pass 2: Diffing & Snapshotting
Runique maintains a hidden state in migration/src/snapshots/.
- Current State: The parser builds a virtual schema of your current code.
- Previous State: It loads the last snapshot from the filesystem.
- Diffing Engine: It compares the two states to find:
- New tables / Deleted tables.
- Added columns / Removed columns.
- Column rename: via the explicit
[renamed_from: "old"]hint, the diff emits aRENAME COLUMNinstead of aDROP+ADD(no data loss). Without the hint, the non-interactive tool cannot guess intent. - Modified constraints (e.g., changing
nullabletorequired). - Enum values: additions, removals and renames (by position). A rename is treated as one operation, excluded from the add/remove lists.
Pass 3: SeaQuery Generation
The diff is converted into a sequence of SeaQuery statements (TableCreate, TableAlter).
- Ordering: It ensures that dependencies (Foreign Keys) are handled in the correct order (topological sort of new tables).
- Framework Tables: It automatically injects the
eihwaz_usersandeihwaz_groupesmigrations if they are missing or need extension viaextend!{}. - Rust Code Output: It writes a new
.rsfile inmigration/src/and updates theMigratortrait.
Engine-specific generation
The target engine is detected (DB_URL/DATABASE_URL/DB_ENGINE) and the output is adapted:
- Foreign keys: grouped into a
create_relationsmigration (ALTER β¦ ADD CONSTRAINT) on PostgreSQL/MySQL/MariaDB; declared inline in theCREATE TABLEon SQLite (which cannot add FKs to an existing table). - Enums:
CREATE TYPE β¦ AS ENUMon PostgreSQL; nativeVARCHAR/ENUMelsewhere. An enum value rename becomesALTER TYPE β¦ RENAME VALUEon PostgreSQL (atomic) and a plain dataUPDATEon other engines. updated_at: PostgreSQL trigger;ON UPDATE CURRENT_TIMESTAMPon MySQL/MariaDB.
Generated files are therefore engine-specific: to switch engines, regenerate from scratch with the right DB_ENGINE.
Atomic commit & destructive guard
The passes above only compute a plan in memory β nothing is written until the full plan (model!{} changes plus extend!{} changes) is assembled and validated:
- Destructive guard:
DROP COLUMN, column type changes,nullable β not null, dropped foreign keys and newly addedON DELETE CASCADEconstraints are blocked unlessmakemigrations --forceis passed. The guard covers bothmodel!{}andextend!{}changes. - Single commit: directory creation, file writes,
lib.rsregistration andAdminTableMigrationpositioning all run under one rollback. On any write error, generated files are removed and pre-existing snapshots andlib.rsare restored to their previous content.
Why customized snapshots?
Runique doesn't rely solely on the database state (which can be desynchronized). By keeping snapshots of the DSL state, the framework ensures that your Admin forms always match your model declarations, even if you haven't applied the migrations yet.
extend!{} logic
When you use extend! { table: "eihwaz_users", ... }, the parser:
- Identifies the target framework table.
- Stores the extension in a specific snapshot folder.
- Generates an
ALTER TABLEinstead of aCREATE TABLEduring the nextmakemigrationsrun.
Concrete examples
Renaming a column without data loss
Renaming a field directly produces a DROP + ADD β lost data. The renamed_from hint signals intent to the non-interactive tool:
model! {
Employe,
table: "employes",
fields: {
// before: job_title: text,
title: text [renamed_from: "job_title"],
}
}
makemigrations then emits ALTER TABLE employes RENAME COLUMN job_title TO title (PostgreSQL, MySQL/MariaDB, SQLite). The attribute is a migration-only directive: no effect on the generated entity or form. Safeguard: if the old column still exists in the snapshot (stale hint), no rename is emitted.
Extending a framework table with extend!{}
To add columns to eihwaz_users (or eihwaz_groupes) without touching the framework:
use runique::prelude::*;
extend! {
table: "eihwaz_users",
fields: {
bio: textarea,
avatar: image [upload_to: "avatars/"],
website: url,
is_verified: bool [default: false],
}
}
On the next makemigrations, these fields become an ALTER TABLE eihwaz_users ADD COLUMN β¦ (never a CREATE TABLE). extend!{} fields accept the same types and options as model!{}, including renamed_from.
Generating and applying
# Detect the diff and write the migration files
runique makemigrations
# Destructive changes (DROP COLUMN, nullable β not null,
# type change, FK removal) are blocked by default.
# To allow them explicitly:
runique makemigrations --force
# Custom paths (defaults: src/entities and migration/src)
runique makemigrations --entities src/entities --migrations migration/src
# Apply the generated migrations
sea-orm-cli migrate up
β Architecture | Models β