1use std::sync::Arc;
30
31use rainier_orm::{Blueprint, Dialect, Entity, Row, TableChanges};
32use rainier_support::{Error, Result};
33
34use crate::connection::Database;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Down {
44 Statements(Vec<String>),
46 Irreversible(String),
52}
53
54impl Down {
55 pub fn statements(statements: impl IntoIterator<Item = String>) -> Self {
57 Self::Statements(statements.into_iter().collect())
58 }
59
60 pub fn irreversible(reason: impl Into<String>) -> Self {
62 Self::Irreversible(reason.into())
63 }
64
65 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 pub fn is_reversible(&self) -> bool {
77 matches!(self, Down::Statements(_))
78 }
79}
80
81pub trait Migration: Send + Sync + 'static {
111 fn name(&self) -> &str;
115
116 fn up(&self, dialect: Dialect) -> Vec<String>;
121
122 fn down(&self, dialect: Dialect) -> Down;
124}
125
126pub 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 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 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 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 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 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 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 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 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
277fn 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
307const LEDGER: &str = "rainier_migrations";
309
310#[derive(Default)]
312pub struct Migrator {
313 migrations: Vec<Arc<dyn Migration>>,
314}
315
316impl Migrator {
317 pub fn new() -> Self {
319 Self::default()
320 }
321
322 #[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 pub fn add_arc(mut self, migration: Arc<dyn Migration>) -> Self {
331 self.migrations.push(migration);
332 self
333 }
334
335 pub fn create_table<E: Entity>(self, name: impl Into<String>) -> Self {
337 self.add(Step::create_table::<E>(name))
338 }
339
340 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 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 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 pub fn merge(mut self, other: Migrator) -> Self {
372 self.migrations.extend(other.migrations);
373 self
374 }
375
376 pub fn names(&self) -> Vec<&str> {
378 self.migrations.iter().map(|m| m.name()).collect()
379 }
380
381 pub fn len(&self) -> usize {
383 self.migrations.len()
384 }
385
386 pub fn is_empty(&self) -> bool {
388 self.migrations.is_empty()
389 }
390
391 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}