pub struct MigrationEngine { /* private fields */ }Expand description
Migration engine bound to an open connection.
Implementations§
Source§impl MigrationEngine
impl MigrationEngine
pub fn new( conn: Box<dyn Connection>, migrations_dir: PathBuf, dialect: Dialect, ) -> Self
Sourcepub fn ensure_migration_table(&mut self) -> Result<(), SqlError>
pub fn ensure_migration_table(&mut self) -> Result<(), SqlError>
Ensure the ferrule_migrations tracking table exists.
The DDL is dialect-specific because the canonical column types, the timestamp default, and the “create if absent” idiom differ across backends:
- SQLite / Postgres —
TEXTkeys are valid andCREATE TABLE IF NOT EXISTSis supported. - MySQL —
TEXTcannot be aPRIMARY KEYwithout a prefix length, so the keyed columns useVARCHAR. - MSSQL —
CREATE TABLE IF NOT EXISTSis not valid T-SQL,TEXTcannot key a table, and theTIMESTAMPtype is a rowversion (not a datetime); we guard creation withIF OBJECT_ID(...) IS NULLand useDATETIME2. - Oracle — has no
TEXTtype andIF NOT EXISTSis unsupported pre-23c, so creation runs inside a PL/SQL block that swallows ORA-00955 (name already used).
Sourcepub fn pending_migrations(&mut self) -> Result<Vec<MigrationFile>, SqlError>
pub fn pending_migrations(&mut self) -> Result<Vec<MigrationFile>, SqlError>
Return the list of migrations that have not yet been applied, sorted lexicographically by version.
Sourcepub fn apply_up(&mut self, file: &MigrationFile) -> Result<(), SqlError>
pub fn apply_up(&mut self, file: &MigrationFile) -> Result<(), SqlError>
Apply a single migration (.up.sql).
The migration script and the ferrule_migrations tracking-row
INSERT are committed as a single unit on backends with
transactional DDL (SQLite, Postgres, MSSQL): both succeed or both
roll back, so a mid-script failure can never leave the migration
recorded-but-partial or applied-but-untracked. On MySQL and Oracle,
DDL implicitly commits, so the two steps run best-effort and a
failure in the middle can leave the schema partially applied — see
apply_atomic for the per-dialect details.
Sourcepub fn apply_down(&mut self, file: &MigrationFile) -> Result<(), SqlError>
pub fn apply_down(&mut self, file: &MigrationFile) -> Result<(), SqlError>
Rollback a single migration (.down.sql).
The rollback script and the ferrule_migrations tracking-row
DELETE are committed together on backends with transactional DDL
(SQLite, Postgres, MSSQL): the row is removed only if the entire
down script succeeds, so a mid-script failure can never leave the
schema half-rolled-back while the row still marks the migration
applied. On MySQL and Oracle, DDL implicitly commits, so the two
steps run best-effort — see apply_atomic.
Sourcepub fn last_applied(
&mut self,
n: usize,
) -> Result<Vec<AppliedMigration>, SqlError>
pub fn last_applied( &mut self, n: usize, ) -> Result<Vec<AppliedMigration>, SqlError>
Read the last N applied versions from the tracking table, ordered by most-recent first.
The ordering uses version DESC as a deterministic tiebreak after
applied_at DESC: applied_at has second granularity, and
migrate up can apply a whole batch inside a single second, so
without the tiebreak down could roll back an arbitrary member of
that batch rather than the genuinely newest one.
The row-limit clause is dialect-specific: SQLite, Postgres, and
MySQL accept LIMIT n; MSSQL uses SELECT TOP n; Oracle (12c+)
uses FETCH FIRST n ROWS ONLY.
Sourcepub fn all_applied(&mut self) -> Result<Vec<AppliedMigration>, SqlError>
pub fn all_applied(&mut self) -> Result<Vec<AppliedMigration>, SqlError>
Read every applied migration from the tracking table, ordered
most-recent first (applied_at DESC, version DESC).
Unlike MigrationEngine::last_applied this applies no row cap, so
migrate history and migrate verify see the full set rather than a
silently truncated window. The ordering is identical to
last_applied and needs no dialect-specific limit clause, so the
same query runs on all backends.
Sourcepub fn verify_checksum(&mut self, version: &str) -> Result<(), SqlError>
pub fn verify_checksum(&mut self, version: &str) -> Result<(), SqlError>
Verify that a single migration’s .up.sql file on disk still matches
the checksum recorded in the database. Returns Ok(()) if clean,
Err on drift.
The on-disk file is resolved by reusing MigrationEngine::scan_dir
(Direction::Up) and matching the derived version exactly, the
same way apply_up and pending_migrations identify files. An
earlier name.starts_with(version) prefix match could bind the
wrong file (e.g. version 2026 matching 20260602_x.up.sql),
reporting spurious drift or masking real drift.
pub fn applied_versions(&mut self) -> Result<HashSet<String>, SqlError>
Sourcepub fn scan_dir(
&self,
direction: Direction,
) -> Result<Vec<MigrationFile>, SqlError>
pub fn scan_dir( &self, direction: Direction, ) -> Result<Vec<MigrationFile>, SqlError>
Scan the migrations directory for files in the given direction,
returning them sorted lexicographically by version.
The version is the substring before the first _ in the file stem
(20260602120000_add_users.up.sql -> 20260602120000). Because
two distinct files can collapse onto the same version under that
rule, this detects the collision up front and returns a
SqlError::QueryFailed naming the conflicting files. Surfacing
it here — before any apply_up runs — prevents a duplicate version
from being discovered only when the second tracking-row INSERT
hits the primary-key constraint mid-run, which would leave one
migration’s DDL applied but untracked.
Sourcepub fn on_disk_checksums(&self) -> Result<HashMap<String, String>, SqlError>
pub fn on_disk_checksums(&self) -> Result<HashMap<String, String>, SqlError>
Build a version -> file-checksum map by scanning the up migrations
directory exactly once.
The checksum is the SHA-256 hex digest of the .up.sql file’s
contents, identical to what MigrationEngine::apply_up records in
the tracking table. This lets verify compare every applied
migration against on-disk content using the checksums it already has
from the applied-list query — one directory scan and zero extra
SELECTs, instead of re-reading the directory and re-querying the
database once per applied migration.
Sourcepub fn verify_applied(
&self,
applied: &[AppliedMigration],
) -> Result<Vec<ChecksumDrift>, SqlError>
pub fn verify_applied( &self, applied: &[AppliedMigration], ) -> Result<Vec<ChecksumDrift>, SqlError>
Verify that every supplied applied migration still matches its
.up.sql file on disk, using checksums already held in hand.
applied is the list returned by MigrationEngine::last_applied
(or any caller-built list of applied versions + recorded checksums).
The directory is scanned once via
MigrationEngine::on_disk_checksums; no per-migration queries are
issued. The returned vector lists every drifted migration with a
human-readable reason — empty means all clean.