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