Skip to main content

rainier_database/
migrator.rs

1//! Schema migrations — the [`Migration`] contract, and the [`Migrator`] that
2//! runs and rolls them back.
3//!
4//! An ordered, idempotent list of named steps, tracked in a
5//! `rainier_migrations` table so each runs at most once — and, because every
6//! step declares how to undo itself, so a batch can be taken back off.
7//!
8//! Rainier ORM ships its own `migrate::Migrator`, and it is the better tool when
9//! you can reach it: it understands sharding fan-out and its `ddl::Migration`
10//! change sets are dialect-portable. But its `run` is `async` while holding
11//! `sea_query` values, so its future is `!Send` and it cannot be awaited from a
12//! service provider's `boot`. This migrator renders DDL **synchronously**
13//! (through Rainier ORM's own [`schema`](rainier_orm::schema) module, which is
14//! already sync) and then executes plain strings, so booting can run it.
15//!
16//! ```ignore
17//! Migrator::new()
18//!     .create_table::<User>("0001_create_users")
19//!     .create_table::<Post>("0002_create_posts")
20//!     .raw(
21//!         "0003_index_posts_author",
22//!         vec!["CREATE INDEX idx_posts_author ON posts (author_id)".into()],
23//!         vec!["DROP INDEX idx_posts_author".into()],
24//!     )
25//!     .run(&db)
26//!     .await?;
27//! ```
28
29use std::sync::Arc;
30
31use rainier_orm::{Blueprint, Dialect, Entity, Row, TableChanges};
32use rainier_support::{Error, Result};
33
34use crate::connection::Database;
35
36/// How a migration undoes itself.
37///
38/// An enum rather than a possibly-empty `Vec`, because "this cannot be undone"
39/// and "nobody wrote a down step" are different facts and only one of them is a
40/// bug. A rollback **refuses** on [`Irreversible`](Down::Irreversible) and
41/// prints the reason, instead of doing nothing and reporting success.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Down {
44    /// The statements that undo this migration, in the order they should run.
45    Statements(Vec<String>),
46    /// Deliberately not reversible, and why.
47    ///
48    /// A migration that drops a column or rewrites data cannot restore what it
49    /// destroyed. Saying so is more useful than a `down` that runs cleanly and
50    /// leaves the database subtly wrong.
51    Irreversible(String),
52}
53
54impl Down {
55    /// Undone by these statements.
56    pub fn statements(statements: impl IntoIterator<Item = String>) -> Self {
57        Self::Statements(statements.into_iter().collect())
58    }
59
60    /// Not reversible, for the stated reason.
61    pub fn irreversible(reason: impl Into<String>) -> Self {
62        Self::Irreversible(reason.into())
63    }
64
65    /// The statements, or an error naming the migration and why it has none.
66    pub fn sql(&self, migration: &str) -> Result<&[String]> {
67        match self {
68            Down::Statements(statements) => Ok(statements),
69            Down::Irreversible(reason) => {
70                Err(Error::internal(format!("`{migration}` cannot be rolled back: {reason}")))
71            }
72        }
73    }
74
75    /// Whether this migration can be rolled back.
76    pub fn is_reversible(&self) -> bool {
77        matches!(self, Down::Statements(_))
78    }
79}
80
81/// One migration: a name, what it does, and how to undo it.
82///
83/// **`down` is required.** A contract with an optional `down` is a contract
84/// whose `down` is usually missing — and by the time you need one, the
85/// migration that lacks it is months old and nobody remembers what it changed.
86/// Requiring it costs one line for a `create_table` (a `DROP`) and forces the
87/// question at the only moment it is easy to answer.
88///
89/// Where the honest answer is "you cannot", say so with
90/// [`Down::irreversible`] and the reason travels with the migration.
91///
92/// ```ignore
93/// struct BackfillSlugs;
94///
95/// impl Migration for BackfillSlugs {
96///     fn name(&self) -> &str { "0004_backfill_slugs" }
97///
98///     fn up(&self, _: Dialect) -> Vec<String> {
99///         vec!["UPDATE posts SET slug = lower(title) WHERE slug IS NULL".into()]
100///     }
101///
102///     fn down(&self, _: Dialect) -> Down {
103///         Down::irreversible("the original NULLs are not recoverable")
104///     }
105/// }
106/// ```
107///
108/// Both renderers are **synchronous**, which is what keeps
109/// [`Migrator::run`]'s future `Send`.
110pub trait Migration: Send + Sync + 'static {
111    /// The name recorded in the ledger.
112    ///
113    /// Permanent once applied: renaming one makes it run again.
114    fn name(&self) -> &str;
115
116    /// The statements that apply this migration to `dialect`.
117    ///
118    /// Returning an empty vector is a no-op, which is how a step skips a
119    /// backend that does not need it.
120    fn up(&self, dialect: Dialect) -> Vec<String>;
121
122    /// How to undo it.
123    fn down(&self, dialect: Dialect) -> Down;
124}
125
126/// A [`Migration`] built from closures — what the builder methods produce.
127///
128/// Implement [`Migration`] directly when a migration wants a name of its own
129/// and a place to live; use `Step` when it is two lists of statements.
130pub struct Step {
131    name: String,
132    up: Box<dyn Fn(Dialect) -> Vec<String> + Send + Sync>,
133    down: Box<dyn Fn(Dialect) -> Down + Send + Sync>,
134}
135
136impl Step {
137    /// A step from an `up` and a `down` renderer.
138    pub fn new(
139        name: impl Into<String>,
140        up: impl Fn(Dialect) -> Vec<String> + Send + Sync + 'static,
141        down: impl Fn(Dialect) -> Down + Send + Sync + 'static,
142    ) -> Self {
143        Self { name: name.into(), up: Box::new(up), down: Box::new(down) }
144    }
145
146    /// A step running fixed SQL either way, whatever the dialect.
147    pub fn raw(name: impl Into<String>, up: Vec<String>, down: Vec<String>) -> Self {
148        Self::new(name, move |_| up.clone(), move |_| Down::Statements(down.clone()))
149    }
150
151    /// A step that applies fixed SQL and cannot be undone, with the reason.
152    pub fn raw_irreversible(
153        name: impl Into<String>,
154        up: Vec<String>,
155        reason: impl Into<String>,
156    ) -> Self {
157        let reason = reason.into();
158        Self::new(name, move |_| up.clone(), move |_| Down::irreversible(reason.clone()))
159    }
160
161    /// A step creating `E`'s table and its indexes, from the derived metadata.
162    ///
163    /// Its `down` drops the table, which is the honest inverse — and also why
164    /// rolling back a `create_table` in production destroys data. That is a
165    /// property of the operation, not of this implementation.
166    pub fn create_table<E: Entity>(name: impl Into<String>) -> Self {
167        Self::new(
168            name,
169            |dialect| rainier_orm::schema::schema_ddl::<E>(dialect),
170            |_| Down::Statements(vec![format!("DROP TABLE IF EXISTS {}", E::table())]),
171        )
172    }
173
174    /// A step creating `table` from a [`Blueprint`].
175    ///
176    /// For a table no model describes: a pivot, a join table, something a
177    /// third-party tool reads. The description is written once and lowered to
178    /// whichever engine the executor reports, so there is no per-dialect SQL
179    /// to keep in step.
180    ///
181    /// ```ignore
182    /// Step::create("0007_create_post_tag", "post_tag", |table| {
183    ///     table.foreign_id("post_id").constrained_on("posts").cascade_on_delete();
184    ///     table.foreign_id("tag_id").constrained_on("tags").cascade_on_delete();
185    ///     table.primary(["post_id", "tag_id"]);
186    /// })
187    /// ```
188    ///
189    /// Its `down` drops the table — which is the honest inverse, and also why
190    /// rolling one back in production destroys data.
191    pub fn create(
192        name: impl Into<String>,
193        table: impl Into<String>,
194        build: impl FnOnce(&mut Blueprint),
195    ) -> Self {
196        let blueprint = Arc::new(Blueprint::create(table, build));
197        let reverse = Arc::clone(&blueprint);
198
199        Self::new(
200            name,
201            move |dialect| blueprint.to_sql(dialect),
202            move |dialect| Down::Statements(reverse.to_reverse_sql(dialect)),
203        )
204    }
205
206    /// A step changing a table that already exists.
207    ///
208    /// ```ignore
209    /// Step::table("0008_posts_add_subtitle", "posts", |table| {
210    ///     table.string("subtitle").nullable();
211    ///     table.index(["author_id", "published"]);
212    /// })
213    /// ```
214    ///
215    /// **The `down` is derived from what changed**, so it cannot drift from
216    /// the `up` the way a hand-written one does. Where a change genuinely
217    /// cannot be undone — a dropped column, a raw statement with no reverse —
218    /// the step reports itself irreversible and says which change made it so.
219    pub fn table(
220        name: impl Into<String>,
221        table: impl Into<String>,
222        build: impl FnOnce(&mut TableChanges),
223    ) -> Self {
224        let changes = Arc::new(TableChanges::to(table, build));
225        let reverse = Arc::clone(&changes);
226
227        Self::new(
228            name,
229            move |dialect| changes.to_sql(dialect),
230            move |dialect| match reverse.irreversible_because() {
231                Some(reason) => Down::irreversible(reason),
232                None => Down::Statements(reverse.to_reverse_sql(dialect)),
233            },
234        )
235    }
236
237    /// A step dropping a table.
238    ///
239    /// **Irreversible**, and deliberately so: nothing here knows the shape of
240    /// what it dropped, and a `down` that recreated an empty table would
241    /// report success having restored nothing.
242    pub fn drop_table(name: impl Into<String>, table: impl Into<String>) -> Self {
243        let table = table.into();
244        let reason = format!("`{table}` was dropped, along with everything in it");
245
246        Self::new(
247            name,
248            move |dialect| {
249                let mut stmt = rainier_orm::sea_query::Table::drop();
250                stmt.table(rainier_orm::sea_query::Alias::new(table.clone())).if_exists();
251                vec![dialect.build_schema(&stmt)]
252            },
253            move |_| Down::irreversible(reason.clone()),
254        )
255    }
256
257    /// A step renaming a table. Reverses by renaming it back.
258    ///
259    /// `ALTER TABLE … RENAME TO` on SQLite and Postgres, `RENAME TABLE` on
260    /// MySQL — one of the several places the engines simply disagree.
261    pub fn rename_table(
262        name: impl Into<String>,
263        from: impl Into<String>,
264        to: impl Into<String>,
265    ) -> Self {
266        let (from, to) = (from.into(), to.into());
267        let (back_from, back_to) = (to.clone(), from.clone());
268
269        Self::new(
270            name,
271            move |dialect| vec![rename_table_sql(dialect, &from, &to)],
272            move |dialect| Down::Statements(vec![rename_table_sql(dialect, &back_from, &back_to)]),
273        )
274    }
275}
276
277/// Rename a table, in whichever way the dialect spells it.
278fn rename_table_sql(dialect: Dialect, from: &str, to: &str) -> String {
279    let mut stmt = rainier_orm::sea_query::Table::rename();
280    stmt.table(
281        rainier_orm::sea_query::Alias::new(from.to_string()),
282        rainier_orm::sea_query::Alias::new(to.to_string()),
283    );
284    dialect.build_schema(&stmt)
285}
286
287impl Migration for Step {
288    fn name(&self) -> &str {
289        &self.name
290    }
291
292    fn up(&self, dialect: Dialect) -> Vec<String> {
293        (self.up)(dialect)
294    }
295
296    fn down(&self, dialect: Dialect) -> Down {
297        (self.down)(dialect)
298    }
299}
300
301impl std::fmt::Debug for Step {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.debug_struct("Step").field("name", &self.name).finish()
304    }
305}
306
307/// The table recording which migrations have run, and in which batch.
308const LEDGER: &str = "rainier_migrations";
309
310/// Runs an ordered list of [`Migration`]s, skipping any already applied.
311#[derive(Default)]
312pub struct Migrator {
313    migrations: Vec<Arc<dyn Migration>>,
314}
315
316impl Migrator {
317    /// An empty migrator.
318    pub fn new() -> Self {
319        Self::default()
320    }
321
322    /// Append a migration.
323    #[allow(clippy::should_implement_trait, reason = "a builder step, not arithmetic")]
324    pub fn add(mut self, migration: impl Migration) -> Self {
325        self.migrations.push(Arc::new(migration));
326        self
327    }
328
329    /// Append an already-shared migration.
330    pub fn add_arc(mut self, migration: Arc<dyn Migration>) -> Self {
331        self.migrations.push(migration);
332        self
333    }
334
335    /// Append a "create this entity's table" step, whose `down` drops it.
336    pub fn create_table<E: Entity>(self, name: impl Into<String>) -> Self {
337        self.add(Step::create_table::<E>(name))
338    }
339
340    /// Append a step from `up` and `down` rendering closures.
341    pub fn step(
342        self,
343        name: impl Into<String>,
344        up: impl Fn(Dialect) -> Vec<String> + Send + Sync + 'static,
345        down: impl Fn(Dialect) -> Down + Send + Sync + 'static,
346    ) -> Self {
347        self.add(Step::new(name, up, down))
348    }
349
350    /// Append a step running fixed SQL, with the SQL that undoes it.
351    pub fn raw(self, name: impl Into<String>, up: Vec<String>, down: Vec<String>) -> Self {
352        self.add(Step::raw(name, up, down))
353    }
354
355    /// Append a step that cannot be undone, with the reason.
356    pub fn raw_irreversible(
357        self,
358        name: impl Into<String>,
359        up: Vec<String>,
360        reason: impl Into<String>,
361    ) -> Self {
362        self.add(Step::raw_irreversible(name, up, reason))
363    }
364
365    /// Append every step of `other`, keeping its order.
366    ///
367    /// How a component that owns tables contributes them to the application's
368    /// one migrator — `DatabaseQueue::migrations()` being the case that ships.
369    /// Without this, a driver's tables could only be created by running a
370    /// second migrator, which the `migrate` command has no way to find.
371    pub fn merge(mut self, other: Migrator) -> Self {
372        self.migrations.extend(other.migrations);
373        self
374    }
375
376    /// Every registered step's name, in order.
377    pub fn names(&self) -> Vec<&str> {
378        self.migrations.iter().map(|m| m.name()).collect()
379    }
380
381    /// How many steps are registered.
382    pub fn len(&self) -> usize {
383        self.migrations.len()
384    }
385
386    /// Whether no steps are registered.
387    pub fn is_empty(&self) -> bool {
388        self.migrations.is_empty()
389    }
390
391    /// Which registered steps declare themselves irreversible on `dialect`.
392    ///
393    /// Worth printing before a deploy: it is exactly the list of things a
394    /// rollback will refuse to undo.
395    pub fn irreversible(&self, dialect: Dialect) -> Vec<&str> {
396        self.migrations
397            .iter()
398            .filter(|m| !m.down(dialect).is_reversible())
399            .map(|m| m.name())
400            .collect()
401    }
402
403    /// Apply every step that has not run yet, in order. Returns the names of
404    /// the steps that were applied this time.
405    ///
406    /// They share one **batch**, which is the unit
407    /// [`rollback`](Self::rollback) takes back off.
408    ///
409    /// A step's statements run before its ledger row is written, so a failure
410    /// part-way leaves it unrecorded and it is retried next boot. That means a
411    /// step must be **idempotent** where it can be — `CREATE TABLE IF NOT
412    /// EXISTS`, which is what Rainier ORM's generated DDL emits.
413    pub async fn run(&self, db: &Database) -> Result<Vec<String>> {
414        self.ensure_ledger(db).await?;
415        let ledger = self.ledger_rows(db).await?;
416        let batch = ledger.iter().map(|(_, batch)| *batch).max().unwrap_or(0) + 1;
417
418        let mut ran = Vec::new();
419        for migration in &self.migrations {
420            let name = migration.name();
421            if ledger.iter().any(|(applied, _)| applied == name) {
422                continue;
423            }
424
425            for sql in migration.up(db.dialect()) {
426                self.execute(db, name, &sql, "migration").await?;
427            }
428
429            self.record(db, name, batch).await?;
430            tracing::info!(migration = %name, batch, "migrated");
431            ran.push(name.to_string());
432        }
433
434        Ok(ran)
435    }
436
437    /// Undo the last `batches` batches, most recent first. Returns the names
438    /// that were rolled back.
439    ///
440    /// Within a batch, steps are undone in **reverse** declaration order,
441    /// because a later step may depend on an earlier one — dropping the table
442    /// a foreign key points at before dropping the key fails on every backend
443    /// that enforces them.
444    ///
445    /// Refuses **before running anything** if any step in range is
446    /// [irreversible](Down::Irreversible), or is in the ledger but no longer in
447    /// this migrator. A rollback that half-completes leaves the schema in a
448    /// state no migration describes, which is worse than one that never
449    /// started.
450    pub async fn rollback(&self, db: &Database, batches: u32) -> Result<Vec<String>> {
451        let targets = self.rollback_targets(db, batches).await?;
452        if targets.is_empty() {
453            return Ok(Vec::new());
454        }
455
456        for name in &targets {
457            self.find(name)?.down(db.dialect()).sql(name)?;
458        }
459
460        let mut rolled_back = Vec::new();
461        for name in &targets {
462            let down = self.find(name)?.down(db.dialect());
463            for sql in down.sql(name)? {
464                self.execute(db, name, sql, "rollback").await?;
465            }
466
467            self.forget(db, name).await?;
468            tracing::info!(migration = %name, "rolled back");
469            rolled_back.push(name.clone());
470        }
471
472        Ok(rolled_back)
473    }
474
475    /// The names the last `batches` batches applied, in the order a rollback
476    /// would undo them.
477    ///
478    /// What `migrate:rollback --pretend` prints.
479    pub async fn rollback_targets(&self, db: &Database, batches: u32) -> Result<Vec<String>> {
480        if batches == 0 {
481            return Ok(Vec::new());
482        }
483
484        let mut rows = self.ledger_rows(db).await?;
485        if rows.is_empty() {
486            return Ok(Vec::new());
487        }
488
489        // The `batches` highest batch numbers, whatever they happen to be —
490        // counting back from the largest rather than assuming they are
491        // contiguous, because a rollback leaves gaps.
492        let mut numbers: Vec<i64> = rows.iter().map(|(_, batch)| *batch).collect();
493        numbers.sort_unstable();
494        numbers.dedup();
495        let cutoff = numbers.iter().rev().take(batches as usize).copied().min().unwrap_or(i64::MAX);
496        rows.retain(|(_, batch)| *batch >= cutoff);
497
498        // Highest batch first, and within a batch the reverse of the order the
499        // steps were declared in, so dependants are dropped before what they
500        // depend on. A name the migrator no longer knows sorts last; the
501        // rollback then refuses on it by name rather than skipping it.
502        let declared = self.names();
503        rows.sort_by_key(|(name, batch)| {
504            let position = declared.iter().position(|n| n == name).unwrap_or(usize::MAX);
505            (std::cmp::Reverse(*batch), std::cmp::Reverse(position))
506        });
507
508        Ok(rows.into_iter().map(|(name, _)| name).collect())
509    }
510
511    /// Which migrations the ledger says have already run.
512    pub async fn applied(&self, db: &Database) -> Result<Vec<String>> {
513        Ok(self.ledger_rows(db).await?.into_iter().map(|(name, _)| name).collect())
514    }
515
516    /// Which registered migrations have not run yet, in order.
517    pub async fn pending(&self, db: &Database) -> Result<Vec<&str>> {
518        let applied = self.applied(db).await?;
519        Ok(self.names().into_iter().filter(|name| !applied.iter().any(|a| a == name)).collect())
520    }
521
522    fn find(&self, name: &str) -> Result<&Arc<dyn Migration>> {
523        self.migrations.iter().find(|m| m.name() == name).ok_or_else(|| {
524            Error::internal(format!(
525                "`{name}` is in the ledger but not in this migrator, so there is nothing to \
526                 run to undo it. It was probably deleted from the code while still applied."
527            ))
528        })
529    }
530
531    async fn execute(&self, db: &Database, name: &str, sql: &str, what: &str) -> Result<()> {
532        db.statement(sql).await.map(|_| ()).map_err(|e| {
533            Error::internal(format!(
534                "{what} `{name}` failed on `{}`: {e}",
535                rainier_support::str::limit(sql, 80, "…")
536            ))
537        })
538    }
539
540    /// Every ledger row, as `(name, batch)`.
541    async fn ledger_rows(&self, db: &Database) -> Result<Vec<(String, i64)>> {
542        self.ensure_ledger(db).await?;
543
544        let prepared = crate::statement::Prepared {
545            sql: format!("SELECT name, batch FROM {LEDGER}"),
546            params: Vec::new(),
547            route: rainier_orm::ShardRoute::Global,
548        };
549        let columns = vec![
550            crate::row::ColumnRequest::new("name", rainier_orm::ColumnType::Text),
551            crate::row::ColumnRequest::new("batch", rainier_orm::ColumnType::BigInt),
552        ];
553
554        let rows = db.fetch(prepared, columns).await?;
555        rows.iter()
556            .map(|row| {
557                let name = row
558                    .get_string("name")
559                    .map_err(Error::from)?
560                    .ok_or_else(|| Error::internal("a migration ledger row had a NULL name"))?;
561                // A ledger written before batches existed has no such column,
562                // and everything in it belongs to the same original batch.
563                let batch = row.get_i64("batch").map_err(Error::from)?.unwrap_or(1);
564                Ok((name, batch))
565            })
566            .collect()
567    }
568
569    async fn ensure_ledger(&self, db: &Database) -> Result<()> {
570        // `VARCHAR(255) PRIMARY KEY` renders acceptably on all three dialects,
571        // and `IF NOT EXISTS` makes this safe to run on every boot.
572        db.statement(&format!(
573            "CREATE TABLE IF NOT EXISTS {LEDGER} (\
574               name VARCHAR(255) NOT NULL PRIMARY KEY, \
575               batch BIGINT NOT NULL DEFAULT 1)"
576        ))
577        .await?;
578        Ok(())
579    }
580
581    async fn record(&self, db: &Database, name: &str, batch: i64) -> Result<()> {
582        let values = if db.dialect() == Dialect::Postgres { "($1, $2)" } else { "(?, ?)" };
583        let prepared = crate::statement::Prepared {
584            sql: format!("INSERT INTO {LEDGER} (name, batch) VALUES {values}"),
585            params: vec![name.into(), batch.into()],
586            route: rainier_orm::ShardRoute::Global,
587        };
588
589        db.execute(prepared).await?;
590        Ok(())
591    }
592
593    async fn forget(&self, db: &Database, name: &str) -> Result<()> {
594        let placeholder = if db.dialect() == Dialect::Postgres { "$1" } else { "?" };
595        let prepared = crate::statement::Prepared {
596            sql: format!("DELETE FROM {LEDGER} WHERE name = {placeholder}"),
597            params: vec![name.into()],
598            route: rainier_orm::ShardRoute::Global,
599        };
600
601        db.execute(prepared).await?;
602        Ok(())
603    }
604}
605
606impl std::fmt::Debug for Migrator {
607    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608        f.debug_struct("Migrator").field("migrations", &self.names()).finish()
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use crate::row::OwnedRow;
616    use crate::testing::{fake_database, MemoryConnection};
617
618    #[derive(rainier_orm::Entity, Clone, Debug)]
619    #[orm(table = "users")]
620    struct User {
621        #[orm(pk, auto_increment)]
622        id: u64,
623        #[orm(unique)]
624        email: String,
625    }
626
627    /// A ledger row as the fake connection would return one.
628    fn ledger(name: &str, batch: i64) -> OwnedRow {
629        OwnedRow::new().with("name", name).with("batch", batch)
630    }
631
632    #[test]
633    fn a_migrator_run_is_send() {
634        // The whole reason this exists rather than Rainier ORM's own migrator:
635        // a service provider's `boot` needs a `Send` future.
636        fn assert_send<T: Send>(_: T) {}
637
638        let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
639        let migrator = Migrator::new().create_table::<User>("0001_users");
640        assert_send(async move { migrator.run(&db).await });
641    }
642
643    #[test]
644    fn a_rollback_is_send_too() {
645        fn assert_send<T: Send>(_: T) {}
646
647        let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
648        let migrator = Migrator::new().create_table::<User>("0001_users");
649        assert_send(async move { migrator.rollback(&db, 1).await });
650    }
651
652    #[test]
653    fn create_table_renders_the_entitys_ddl() {
654        let step = Step::create_table::<User>("0001_users");
655        let statements = step.up(Dialect::Sqlite);
656
657        assert_eq!(step.name(), "0001_users");
658        assert!(statements[0].contains("users"), "{:?}", statements);
659        assert!(statements[0].contains("IF NOT EXISTS"), "{:?}", statements);
660    }
661
662    #[test]
663    fn create_table_undoes_itself_by_dropping_the_table() {
664        let step = Step::create_table::<User>("0001_users");
665
666        assert_eq!(
667            step.down(Dialect::Sqlite),
668            Down::Statements(vec!["DROP TABLE IF EXISTS users".to_string()])
669        );
670    }
671
672    #[test]
673    fn ddl_differs_per_dialect() {
674        let step = Step::create_table::<User>("0001_users");
675        let sqlite = step.up(Dialect::Sqlite).join("");
676        let mysql = step.up(Dialect::MySql).join("");
677        assert_ne!(sqlite, mysql, "each dialect should render its own DDL");
678    }
679
680    #[test]
681    fn an_irreversible_step_says_why_rather_than_doing_nothing() {
682        // The distinction the enum exists for: "cannot" is not "forgot".
683        let step = Step::raw_irreversible(
684            "0003_backfill",
685            vec!["UPDATE users SET email = lower(email)".into()],
686            "the original casing is not recoverable",
687        );
688
689        let down = step.down(Dialect::Sqlite);
690        assert!(!down.is_reversible());
691
692        let err = down.sql("0003_backfill").unwrap_err();
693        assert!(err.message().contains("cannot be rolled back"), "{}", err.message());
694        assert!(err.message().contains("not recoverable"), "{}", err.message());
695    }
696
697    #[test]
698    fn a_custom_type_can_implement_the_contract() {
699        struct Citext;
700
701        impl Migration for Citext {
702            fn name(&self) -> &str {
703                "0004_citext"
704            }
705
706            fn up(&self, dialect: Dialect) -> Vec<String> {
707                match dialect {
708                    Dialect::Postgres => vec!["CREATE EXTENSION IF NOT EXISTS citext".into()],
709                    _ => Vec::new(),
710                }
711            }
712
713            fn down(&self, dialect: Dialect) -> Down {
714                match dialect {
715                    Dialect::Postgres => Down::statements(["DROP EXTENSION citext".to_string()]),
716                    _ => Down::statements([]),
717                }
718            }
719        }
720
721        let migrator = Migrator::new().add(Citext);
722
723        assert_eq!(migrator.names(), vec!["0004_citext"]);
724        assert!(migrator.irreversible(Dialect::Postgres).is_empty());
725    }
726
727    #[test]
728    fn irreversible_steps_can_be_listed_before_a_deploy() {
729        let migrator = Migrator::new().create_table::<User>("0001_users").raw_irreversible(
730            "0002_drop_legacy",
731            vec!["ALTER TABLE users DROP COLUMN legacy".into()],
732            "the column's data is gone",
733        );
734
735        assert_eq!(migrator.irreversible(Dialect::Sqlite), vec!["0002_drop_legacy"]);
736    }
737
738    #[tokio::test]
739    async fn runs_pending_migrations_in_order() {
740        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
741
742        let ran = Migrator::new()
743            .raw("0001_a", vec!["CREATE TABLE a (id INT)".into()], vec!["DROP TABLE a".into()])
744            .raw("0002_b", vec!["CREATE TABLE b (id INT)".into()], vec!["DROP TABLE b".into()])
745            .run(&db)
746            .await
747            .unwrap();
748
749        assert_eq!(ran, vec!["0001_a", "0002_b"]);
750
751        let statements = connection.statements();
752        assert!(statements.iter().any(|s| s.contains("CREATE TABLE a")));
753        assert!(statements.iter().any(|s| s.contains("CREATE TABLE b")));
754        assert!(statements.iter().any(|s| s.contains("rainier_migrations")));
755    }
756
757    #[tokio::test]
758    async fn an_already_applied_migration_is_skipped() {
759        // The ledger read returns `0001_a` as already applied. Queued answers
760        // are consumed in order: the first `fetch` is the ledger query.
761        let connection = MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]);
762        let (db, handle) = fake_database(connection);
763
764        let ran = Migrator::new()
765            .raw("0001_a", vec!["CREATE TABLE a (id INT)".into()], vec!["DROP TABLE a".into()])
766            .raw("0002_b", vec!["CREATE TABLE b (id INT)".into()], vec!["DROP TABLE b".into()])
767            .run(&db)
768            .await
769            .unwrap();
770
771        assert_eq!(ran, vec!["0002_b"]);
772        assert!(
773            !handle.statements().iter().any(|s| s.contains("CREATE TABLE a")),
774            "the applied migration must not run again"
775        );
776    }
777
778    #[tokio::test]
779    async fn a_new_run_gets_the_next_batch_number() {
780        let (db, connection) =
781            fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 7)]));
782
783        Migrator::new()
784            .raw("0001_a", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
785            .raw("0002_b", vec!["SELECT 2".into()], vec!["SELECT 2".into()])
786            .run(&db)
787            .await
788            .unwrap();
789
790        let inserts: Vec<_> = connection
791            .recorded()
792            .into_iter()
793            .filter(|r| r.sql.starts_with("INSERT INTO rainier_migrations"))
794            .collect();
795
796        assert_eq!(inserts.len(), 1, "only the pending step should be recorded");
797        assert_eq!(
798            inserts[0].params[1],
799            8_i64.into(),
800            "one past the highest batch in the ledger, not one past its row count"
801        );
802    }
803
804    #[tokio::test]
805    async fn a_failing_step_names_itself_and_its_statement() {
806        let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite).failing("syntax error"));
807
808        let err = Migrator::new()
809            .raw("0001_broken", vec!["CREATE TABLE (".into()], vec!["SELECT 1".into()])
810            .run(&db)
811            .await
812            .unwrap_err();
813
814        // The ledger creation is the first statement to fail here, which is
815        // itself worth surfacing rather than hiding.
816        assert!(err.message().contains("syntax error"), "{}", err.message());
817    }
818
819    #[tokio::test]
820    async fn the_ledger_is_created_before_anything_is_read() {
821        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
822        Migrator::new().run(&db).await.unwrap();
823
824        let statements = connection.statements();
825        assert!(statements[0].contains("CREATE TABLE IF NOT EXISTS rainier_migrations"));
826        assert!(statements[0].contains("batch"), "{}", statements[0]);
827    }
828
829    #[test]
830    fn the_migrator_lists_its_steps() {
831        let migrator = Migrator::new().create_table::<User>("0001_users").raw(
832            "0002_seed",
833            vec!["INSERT INTO users DEFAULT VALUES".into()],
834            vec!["DELETE FROM users".into()],
835        );
836
837        assert_eq!(migrator.names(), vec!["0001_users", "0002_seed"]);
838        assert_eq!(migrator.len(), 2);
839        assert!(!migrator.is_empty());
840    }
841
842    #[test]
843    fn merging_appends_the_other_migrators_steps_in_order() {
844        let component = Migrator::new()
845            .raw("component_0001", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
846            .raw("component_0002", vec!["SELECT 2".into()], vec!["SELECT 2".into()]);
847
848        let migrator = Migrator::new()
849            .raw("app_0001", vec!["SELECT 3".into()], vec!["SELECT 3".into()])
850            .merge(component)
851            .raw("app_0002", vec!["SELECT 4".into()], vec!["SELECT 4".into()]);
852
853        assert_eq!(
854            migrator.names(),
855            vec!["app_0001", "component_0001", "component_0002", "app_0002"],
856            "merged steps keep their order, at the point they were merged in"
857        );
858    }
859
860    #[tokio::test]
861    async fn postgres_gets_numbered_placeholders() {
862        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Postgres));
863        Migrator::new()
864            .raw("0001_a", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
865            .run(&db)
866            .await
867            .unwrap();
868
869        let insert = connection
870            .statements()
871            .into_iter()
872            .find(|s| s.starts_with("INSERT INTO rainier_migrations"))
873            .expect("the ledger row should have been written");
874        assert!(insert.contains("$1") && insert.contains("$2"), "{insert}");
875    }
876
877    #[tokio::test]
878    async fn rolling_back_an_empty_ledger_is_not_an_error() {
879        let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
880        let migrator = Migrator::new().create_table::<User>("0001_users");
881
882        assert!(migrator.rollback(&db, 1).await.unwrap().is_empty());
883    }
884
885    #[tokio::test]
886    async fn rolling_back_zero_batches_does_nothing() {
887        let (db, connection) =
888            fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]));
889
890        let migrator =
891            Migrator::new().raw("0001_a", vec!["SELECT 1".into()], vec!["DROP TABLE a".into()]);
892
893        assert!(migrator.rollback(&db, 0).await.unwrap().is_empty());
894        assert!(
895            !connection.statements().iter().any(|s| s.contains("DROP TABLE a")),
896            "nothing should have been undone"
897        );
898    }
899
900    #[tokio::test]
901    async fn a_rollback_undoes_the_last_batch_in_reverse() {
902        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite).returning([
903            ledger("0001_a", 1),
904            ledger("0002_b", 2),
905            ledger("0003_c", 2),
906        ]));
907
908        let migrator = Migrator::new()
909            .raw("0001_a", vec!["SELECT 1".into()], vec!["DROP TABLE a".into()])
910            .raw("0002_b", vec!["SELECT 2".into()], vec!["DROP TABLE b".into()])
911            .raw("0003_c", vec!["SELECT 3".into()], vec!["DROP TABLE c".into()]);
912
913        let rolled_back = migrator.rollback(&db, 1).await.unwrap();
914
915        assert_eq!(
916            rolled_back,
917            vec!["0003_c", "0002_b"],
918            "the newest batch only, and dependants first"
919        );
920
921        let statements = connection.statements();
922        assert!(statements.iter().any(|s| s == "DROP TABLE c"));
923        assert!(statements.iter().any(|s| s == "DROP TABLE b"));
924        assert!(!statements.iter().any(|s| s == "DROP TABLE a"), "batch 1 was not in range");
925        assert!(statements.iter().any(|s| s.starts_with("DELETE FROM rainier_migrations")));
926    }
927
928    #[tokio::test]
929    async fn a_rollback_can_take_several_batches() {
930        let (db, _) = fake_database(
931            MemoryConnection::new(Dialect::Sqlite)
932                .returning([ledger("0001_a", 1), ledger("0002_b", 2)]),
933        );
934
935        let migrator = Migrator::new()
936            .raw("0001_a", vec!["SELECT 1".into()], vec!["DROP TABLE a".into()])
937            .raw("0002_b", vec!["SELECT 2".into()], vec!["DROP TABLE b".into()]);
938
939        assert_eq!(migrator.rollback(&db, 2).await.unwrap(), vec!["0002_b", "0001_a"]);
940    }
941
942    #[tokio::test]
943    async fn an_irreversible_step_stops_the_whole_rollback_before_it_starts() {
944        // `0002_b` is reversible and would be undone first. It must not run,
945        // because `0001_a` in the same batch cannot be undone at all — a
946        // half-rolled-back batch matches no migration's idea of the schema.
947        let (db, connection) = fake_database(
948            MemoryConnection::new(Dialect::Sqlite)
949                .returning([ledger("0001_a", 1), ledger("0002_b", 1)]),
950        );
951
952        let migrator = Migrator::new()
953            .raw_irreversible("0001_a", vec!["SELECT 1".into()], "the rows are gone")
954            .raw("0002_b", vec!["SELECT 2".into()], vec!["DROP TABLE b".into()]);
955
956        let err = migrator.rollback(&db, 1).await.unwrap_err();
957
958        assert!(err.message().contains("0001_a"), "{}", err.message());
959        assert!(err.message().contains("the rows are gone"), "{}", err.message());
960        assert!(
961            !connection.statements().iter().any(|s| s == "DROP TABLE b"),
962            "no step should have run"
963        );
964    }
965
966    #[tokio::test]
967    async fn rolling_back_a_migration_that_was_deleted_from_the_code_says_so() {
968        let (db, _) =
969            fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]));
970
971        let err = Migrator::new().rollback(&db, 1).await.unwrap_err();
972
973        assert!(err.message().contains("0001_a"), "{}", err.message());
974        assert!(err.message().contains("not in this migrator"), "{}", err.message());
975    }
976
977    #[tokio::test]
978    async fn pending_is_what_has_not_run() {
979        let (db, _) =
980            fake_database(MemoryConnection::new(Dialect::Sqlite).returning([ledger("0001_a", 1)]));
981
982        let migrator = Migrator::new()
983            .raw("0001_a", vec!["SELECT 1".into()], vec!["SELECT 1".into()])
984            .raw("0002_b", vec!["SELECT 2".into()], vec!["SELECT 2".into()]);
985
986        assert_eq!(migrator.pending(&db).await.unwrap(), vec!["0002_b"]);
987    }
988}