1use std::collections::HashSet;
15use std::path::Path;
16
17use crate::core::{inventory, ModelEntry, ModelSchema};
18use crate::sql::sqlx;
19#[cfg(feature = "postgres")]
20use crate::sql::sqlx::Row;
21#[cfg(feature = "postgres")]
24use crate::sql::sqlx::PgPool;
25
26use super::diff::render_changes_split;
27use super::file::{self, Migration, Operation};
28use super::invert::invert;
29use super::progress::{emit, MigrationEvent, MigrationObserver, Outcome};
30use super::snapshot::SchemaSnapshot;
31use super::{ddl, MigrateError};
32
33pub const LEDGER_TABLE: &str = "__rustango_migrations__";
37
38pub const SYSTEM_LEDGER_TABLE: &str = "__rustango_system_migrations__";
43
44#[derive(Debug, Clone, Copy)]
62pub struct Builder {
63 ledger: &'static str,
64}
65
66impl Default for Builder {
67 fn default() -> Self {
68 Self {
69 ledger: LEDGER_TABLE,
70 }
71 }
72}
73
74impl Builder {
75 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[must_use]
91 pub fn ledger(mut self, name: &'static str) -> Self {
92 validate_ledger_name(name);
93 self.ledger = name;
94 self
95 }
96
97 #[must_use]
99 pub fn ledger_name(&self) -> &'static str {
100 self.ledger
101 }
102
103 #[cfg(feature = "postgres")]
111 pub async fn migrate(&self, pool: &PgPool, dir: &Path) -> Result<Vec<Migration>, MigrateError> {
112 migrate_with_ledger(pool, dir, self.ledger, None).await
113 }
114
115 #[cfg(feature = "postgres")]
120 pub async fn migrate_to(
121 &self,
122 pool: &PgPool,
123 dir: &Path,
124 target: &str,
125 ) -> Result<Vec<Migration>, MigrateError> {
126 migrate_to_with_ledger(pool, dir, target, self.ledger).await
127 }
128
129 #[cfg(feature = "postgres")]
134 pub async fn migrate_embedded(
135 &self,
136 pool: &PgPool,
137 embedded: &[(&str, &str)],
138 ) -> Result<Vec<Migration>, MigrateError> {
139 migrate_embedded_with_ledger(pool, embedded, self.ledger).await
140 }
141
142 #[cfg(feature = "postgres")]
147 pub async fn migrate_dry_run(
148 &self,
149 pool: &PgPool,
150 dir: &Path,
151 ) -> Result<Vec<MigrationPreview>, MigrateError> {
152 migrate_dry_run_with_ledger(pool, dir, self.ledger).await
153 }
154
155 #[cfg(feature = "postgres")]
160 pub async fn downgrade(
161 &self,
162 pool: &PgPool,
163 dir: &Path,
164 steps: usize,
165 ) -> Result<Vec<Migration>, MigrateError> {
166 downgrade_with_ledger(pool, dir, steps, self.ledger).await
167 }
168
169 #[cfg(feature = "postgres")]
174 pub async fn unapply(
175 &self,
176 pool: &PgPool,
177 dir: &Path,
178 name: &str,
179 ) -> Result<Migration, MigrateError> {
180 unapply_with_ledger(pool, dir, name, self.ledger).await
181 }
182
183 #[cfg(feature = "postgres")]
188 pub async fn unapply_force(
189 &self,
190 pool: &PgPool,
191 dir: &Path,
192 name: &str,
193 ) -> Result<Migration, MigrateError> {
194 unapply_force_with_ledger(pool, dir, name, self.ledger).await
195 }
196
197 #[cfg(feature = "postgres")]
202 pub async fn applied_set(&self, pool: &PgPool) -> Result<HashSet<String>, MigrateError> {
203 applied_set_for(pool, self.ledger).await
204 }
205
206 #[cfg(feature = "postgres")]
211 pub async fn ensure_ledger(&self, pool: &PgPool) -> Result<(), MigrateError> {
212 ensure_ledger_for(pool, self.ledger).await
213 }
214}
215
216fn validate_ledger_name(name: &str) {
217 let bytes = name.as_bytes();
220 let valid = !bytes.is_empty()
221 && bytes.len() <= 63
222 && (bytes[0].is_ascii_alphabetic() || bytes[0] == b'_')
223 && bytes
224 .iter()
225 .all(|b| b.is_ascii_alphanumeric() || *b == b'_');
226 assert!(
227 valid,
228 "Builder::ledger({name:?}) is not a valid SQL identifier — \
229 must match [A-Za-z_][A-Za-z0-9_]* and be ≤ 63 bytes"
230 );
231}
232
233#[cfg(any(feature = "postgres", feature = "mysql"))]
242const MIGRATE_LOCK_KEY: i64 = 0x5255_5354_4d49_4754;
243
244#[must_use]
247pub fn registered_models() -> Vec<&'static ModelSchema> {
248 inventory::iter::<ModelEntry>
249 .into_iter()
250 .map(|e| e.schema)
251 .collect()
252}
253
254fn bootstrap_models() -> Vec<&'static ModelSchema> {
264 registered_models()
265 .into_iter()
266 .filter(|m| m.managed)
267 .collect()
268}
269
270#[cfg(feature = "postgres")]
278pub async fn apply_all(pool: &PgPool) -> Result<(), MigrateError> {
279 #[cfg(feature = "signals")]
282 use crate::signals::migrate::{
283 send_post_migrate, send_pre_migrate, PostMigrateContext, PreMigrateContext,
284 };
285 #[cfg(feature = "signals")]
286 send_pre_migrate(PreMigrateContext {
287 source: "apply_all",
288 })
289 .await;
290 let models = bootstrap_models();
291
292 for model in &models {
293 let sql = ddl::create_table_sql(model);
294 sqlx::query(&sql).execute(pool).await?;
295 }
296 for model in &models {
297 for sql in ddl::create_constraints_sql(model) {
298 sqlx::query(&sql).execute(pool).await?;
299 }
300 }
301 #[cfg(feature = "signals")]
304 send_post_migrate(PostMigrateContext {
305 source: "apply_all",
306 applied: Vec::new(),
307 })
308 .await;
309 Ok(())
310}
311
312#[cfg(feature = "postgres")]
319pub async fn drop_all(pool: &PgPool) -> Result<(), MigrateError> {
320 for model in bootstrap_models() {
321 let sql = ddl::drop_table_sql(model, true, true);
322 sqlx::query(&sql).execute(pool).await?;
323 }
324 Ok(())
325}
326
327pub async fn apply_all_pool(pool: &crate::sql::Pool) -> Result<(), MigrateError> {
342 #[cfg(feature = "signals")]
343 use crate::signals::migrate::{
344 send_post_migrate, send_pre_migrate, PostMigrateContext, PreMigrateContext,
345 };
346 #[cfg(feature = "signals")]
347 send_pre_migrate(PreMigrateContext {
348 source: "apply_all_pool",
349 })
350 .await;
351 let dialect = pool.dialect();
352 let models = bootstrap_models();
353 for model in &models {
354 let sql = ddl::create_table_sql_with_dialect(dialect, model);
355 crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
356 }
357 for model in &models {
358 for sql in ddl::create_constraints_sql_with_dialect(dialect, model) {
359 crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
360 }
361 }
362 for model in &models {
366 for sql in ddl::column_comment_statements_with_dialect(dialect, model) {
367 crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
368 }
369 }
370 for model in &models {
374 for sql in ddl::table_comment_statements_with_dialect(dialect, model) {
375 crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
376 }
377 }
378 #[cfg(feature = "signals")]
382 send_post_migrate(PostMigrateContext {
383 source: "apply_all_pool",
384 applied: Vec::new(),
385 })
386 .await;
387 Ok(())
388}
389
390pub async fn drop_all_pool(pool: &crate::sql::Pool) -> Result<(), MigrateError> {
402 let dialect = pool.dialect();
403 let cascade = dialect.name() == "postgres";
405 let models = bootstrap_models();
406 for model in &models {
412 for sql in ddl::drop_constraints_sql_with_dialect(dialect, model) {
413 let _ = crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await;
414 }
415 }
416 for model in &models {
418 let sql =
419 ddl::drop_table_sql_with_dialect(dialect, model, true, cascade);
420 crate::sql::raw_execute_pool(pool, &sql, ::std::vec::Vec::new()).await?;
421 }
422 Ok(())
423}
424
425#[cfg(feature = "postgres")]
442pub async fn migrate(pool: &PgPool, dir: &Path) -> Result<Vec<Migration>, MigrateError> {
443 Builder::default().migrate(pool, dir).await
444}
445
446#[cfg(feature = "postgres")]
460pub async fn migrate_with_progress(
461 pool: &PgPool,
462 dir: &Path,
463 observer: &dyn MigrationObserver,
464) -> Result<Vec<Migration>, MigrateError> {
465 migrate_with_ledger(pool, dir, LEDGER_TABLE, Some(observer)).await
466}
467
468#[cfg(feature = "postgres")]
469async fn migrate_with_ledger(
470 pool: &PgPool,
471 dir: &Path,
472 ledger: &str,
473 observer: Option<&dyn MigrationObserver>,
474) -> Result<Vec<Migration>, MigrateError> {
475 #[cfg(feature = "signals")]
476 use crate::signals::migrate::{
477 send_post_migrate, send_pre_migrate, PostMigrateContext, PreMigrateContext,
478 };
479 #[cfg(feature = "signals")]
480 send_pre_migrate(PreMigrateContext { source: "migrate" }).await;
481 ensure_ledger_for(pool, ledger).await?;
482 let newly = with_migrate_lock(pool, async {
483 let all = file::list_dir(dir)?;
484 let applied = applied_set_for(pool, ledger).await?;
485 let pending = pending_migrations(all, &applied);
486
487 let total = pending.len();
488 emit(observer, || MigrationEvent::Planned { total });
489
490 let mut newly = Vec::with_capacity(total);
491 let enum_pool = crate::sql::Pool::Postgres(pool.clone());
497 for (i, mig) in pending.into_iter().enumerate() {
498 let index = i + 1;
499 emit(observer, || MigrationEvent::Started {
500 name: mig.name.clone(),
501 index,
502 total,
503 });
504 let began = std::time::Instant::now();
505
506 let step = async {
507 Ok::<_, MigrateError>(match reconcile(&enum_pool, &mig, &applied, false).await? {
508 ReconcileAction::Fake => {
509 fake_apply_pool(&enum_pool, &mig, ledger).await?;
510 Outcome::Faked
511 }
512 ReconcileAction::Run | ReconcileAction::RunPartial(_) => {
516 apply_one(pool, &mig, ledger).await?;
517 Outcome::Ran
518 }
519 })
520 }
521 .await;
522
523 let outcome = match step {
524 Ok(outcome) => outcome,
525 Err(e) => {
526 emit(observer, || MigrationEvent::Failed {
527 name: mig.name.clone(),
528 index,
529 total,
530 error: e.to_string(),
531 });
532 return Err(e);
533 }
534 };
535
536 emit(observer, || MigrationEvent::Finished {
537 name: mig.name.clone(),
538 index,
539 total,
540 outcome,
541 elapsed: began.elapsed(),
542 });
543 newly.push(mig);
544 }
545 Ok(newly)
546 })
547 .await?;
548 #[cfg(feature = "signals")]
552 send_post_migrate(PostMigrateContext {
553 source: "migrate",
554 applied: newly.iter().map(|m| m.name.clone()).collect(),
555 })
556 .await;
557 Ok(newly)
558}
559
560#[cfg(feature = "postgres")]
570async fn with_migrate_lock<F, R>(pool: &PgPool, body: F) -> Result<R, MigrateError>
571where
572 F: std::future::Future<Output = Result<R, MigrateError>>,
573{
574 use crate::sql::{Dialect as _, Postgres};
575 let dialect = Postgres;
580 let mut lock_conn = pool.acquire().await?;
581 if let Some(acquire_sql) = dialect.acquire_session_lock_sql() {
582 sqlx::query(&acquire_sql)
583 .bind(MIGRATE_LOCK_KEY)
584 .execute(&mut *lock_conn)
585 .await?;
586 }
587 let result = body.await;
588 if let Some(release_sql) = dialect.release_session_lock_sql() {
593 let _ = sqlx::query(&release_sql)
594 .bind(MIGRATE_LOCK_KEY)
595 .execute(&mut *lock_conn)
596 .await;
597 }
598 result
599}
600
601#[cfg(feature = "postgres")]
609pub async fn applied_set(pool: &PgPool) -> Result<HashSet<String>, MigrateError> {
610 applied_set_for(pool, LEDGER_TABLE).await
611}
612
613#[cfg(feature = "postgres")]
614async fn applied_set_for(pool: &PgPool, ledger: &str) -> Result<HashSet<String>, MigrateError> {
615 let rows = sqlx::query(&format!("SELECT name FROM {ledger}"))
616 .fetch_all(pool)
617 .await?;
618 let mut out = HashSet::with_capacity(rows.len());
619 for row in rows {
620 out.insert(row.try_get::<String, _>("name")?);
621 }
622 Ok(out)
623}
624
625#[cfg(feature = "postgres")]
638pub async fn ensure_ledger(pool: &PgPool) -> Result<(), MigrateError> {
639 ensure_ledger_for(pool, LEDGER_TABLE).await
640}
641
642#[cfg(feature = "postgres")]
643async fn ensure_ledger_for(pool: &PgPool, ledger: &str) -> Result<(), MigrateError> {
644 use crate::sql::{Dialect as _, Postgres};
645 const LOCK_KEY: i64 = 0x5255_5354;
647 let dialect = Postgres;
648 let mut tx = pool.begin().await?;
649 if let Some(xact_lock_sql) = dialect.acquire_xact_lock_sql() {
652 sqlx::query(&xact_lock_sql)
653 .bind(LOCK_KEY)
654 .execute(&mut *tx)
655 .await?;
656 }
657 let create_sql = format!(
658 "CREATE TABLE IF NOT EXISTS {ledger} (\
659 name TEXT PRIMARY KEY, \
660 applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())"
661 );
662 sqlx::query(&create_sql).execute(&mut *tx).await?;
663 tx.commit().await?;
664 Ok(())
665}
666
667#[derive(Debug, Clone, PartialEq, Eq)]
676pub struct MigrationPreview {
677 pub name: String,
678 pub atomic: bool,
681 pub statements: Vec<String>,
682}
683
684#[cfg(feature = "postgres")]
695pub async fn migrate_dry_run(
696 pool: &PgPool,
697 dir: &Path,
698) -> Result<Vec<MigrationPreview>, MigrateError> {
699 Builder::default().migrate_dry_run(pool, dir).await
700}
701
702#[cfg(feature = "postgres")]
703async fn migrate_dry_run_with_ledger(
704 pool: &PgPool,
705 dir: &Path,
706 ledger: &str,
707) -> Result<Vec<MigrationPreview>, MigrateError> {
708 ensure_ledger_for(pool, ledger).await?;
709 let all = file::list_dir(dir)?;
710 let applied = applied_set_for(pool, ledger).await?;
711 let pending: Vec<Migration> = all
712 .into_iter()
713 .filter(|m| !applied.contains(&m.name))
714 .collect();
715
716 let mut out = Vec::with_capacity(pending.len());
717 for mig in pending {
718 out.push(preview_migration(&mig, ledger)?);
719 }
720 Ok(out)
721}
722
723pub fn sqlmigrate_one(dir: &Path, name: &str) -> Result<MigrationPreview, MigrateError> {
733 let all = file::list_dir(dir)?;
734 let mig = all.into_iter().find(|m| m.name == name).ok_or_else(|| {
735 MigrateError::Validation(format!("migration `{name}` not found in {}", dir.display()))
736 })?;
737 preview_migration(&mig, LEDGER_TABLE)
738}
739
740async fn invoke_migration_callback(
745 op: &crate::migrate::file::CallbackOp,
746 pool: crate::sql::Pool,
747) -> Result<(), MigrateError> {
748 let cb = crate::migrate::callbacks::find(&op.name).ok_or_else(|| {
749 MigrateError::Validation(format!(
750 "migration callback `{}` is not registered — \
751 call `rustango::register_migration_callback!(\"{0}\", …)` \
752 at startup",
753 op.name,
754 ))
755 })?;
756 (cb.forward)(pool).await
757}
758
759fn preview_migration(mig: &Migration, ledger: &str) -> Result<MigrationPreview, MigrateError> {
763 let mut statements = Vec::new();
764 let mut deferred_fks: Vec<String> = Vec::new();
765 if mig.atomic {
766 statements.push("BEGIN".to_string());
767 }
768 for op in &mig.forward {
769 match op {
770 Operation::Schema(change) => {
771 let batch = render_changes_split(std::slice::from_ref(change), &mig.snapshot)
772 .map_err(MigrateError::Validation)?;
773 statements.extend(batch.immediate);
774 deferred_fks.extend(batch.deferred_fks);
775 }
776 Operation::Data(d) => {
777 statements.push(d.sql.clone());
778 }
779 Operation::Callback(c) => {
780 statements.push(format!("-- RunPython: {}", c.name));
785 }
786 }
787 }
788 statements.extend(deferred_fks);
789 statements.push(format!(
790 "INSERT INTO {ledger} (name) VALUES ('{}')",
791 mig.name.replace('\'', "''")
792 ));
793 if mig.atomic {
794 statements.push("COMMIT".to_string());
795 }
796 Ok(MigrationPreview {
797 name: mig.name.clone(),
798 atomic: mig.atomic,
799 statements,
800 })
801}
802
803#[cfg(feature = "postgres")]
804async fn apply_atomic(pool: &PgPool, mig: &Migration, ledger: &str) -> Result<(), MigrateError> {
805 tracing::info!(migration = %mig.name, "applying (atomic)");
806 let mut tx = pool.begin().await?;
807 let mut deferred_fks: Vec<String> = Vec::new();
808 for op in &mig.forward {
809 match op {
810 Operation::Schema(change) => {
811 let batch = render_changes_split(std::slice::from_ref(change), &mig.snapshot)
812 .map_err(MigrateError::Validation)?;
813 for stmt in batch.immediate {
814 sqlx::query(&stmt).execute(&mut *tx).await?;
815 }
816 deferred_fks.extend(batch.deferred_fks);
817 }
818 Operation::Data(d) => {
819 sqlx::query(&d.sql).execute(&mut *tx).await?;
820 }
821 Operation::Callback(c) => {
822 invoke_migration_callback(c, pool.clone().into()).await?;
829 }
830 }
831 }
832 for stmt in deferred_fks {
833 sqlx::query(&stmt).execute(&mut *tx).await?;
834 }
835 sqlx::query(&format!("INSERT INTO {ledger} (name) VALUES ($1)"))
836 .bind(&mig.name)
837 .execute(&mut *tx)
838 .await?;
839 tx.commit().await?;
840 Ok(())
841}
842
843#[cfg(feature = "postgres")]
867pub async fn migrate_to(
868 pool: &PgPool,
869 dir: &Path,
870 target: &str,
871) -> Result<Vec<Migration>, MigrateError> {
872 Builder::default().migrate_to(pool, dir, target).await
873}
874
875#[cfg(feature = "postgres")]
876async fn migrate_to_with_ledger(
877 pool: &PgPool,
878 dir: &Path,
879 target: &str,
880 ledger: &str,
881) -> Result<Vec<Migration>, MigrateError> {
882 ensure_ledger_for(pool, ledger).await?;
883 with_migrate_lock(pool, async {
884 let all = file::list_dir(dir)?;
885 let applied = applied_set_for(pool, ledger).await?;
886
887 if target == "zero" {
888 return unapply_all_in_order(pool, dir, &all, &applied, ledger).await;
889 }
890
891 if !all.iter().any(|m| m.name == target) {
892 return Err(MigrateError::Validation(format!(
893 "target migration `{target}` not found in {}",
894 dir.display()
895 )));
896 }
897
898 let head = all
899 .iter()
900 .rev()
901 .find(|m| applied.contains(&m.name))
902 .map(|m| m.name.clone());
903
904 let mut touched = Vec::new();
905 match head {
906 None => {
907 for mig in all.into_iter().filter(|m| m.name.as_str() <= target) {
909 apply_one(pool, &mig, ledger).await?;
910 touched.push(mig);
911 }
912 }
913 Some(h) => {
914 use std::cmp::Ordering;
915 match target.cmp(h.as_str()) {
916 Ordering::Equal => {}
917 Ordering::Greater => {
918 for mig in all.into_iter().filter(|m| {
919 m.name.as_str() > h.as_str()
920 && m.name.as_str() <= target
921 && !applied.contains(&m.name)
922 }) {
923 apply_one(pool, &mig, ledger).await?;
924 touched.push(mig);
925 }
926 }
927 Ordering::Less => {
928 let mut to_unapply: Vec<Migration> = all
929 .into_iter()
930 .filter(|m| {
931 m.name.as_str() > target
932 && m.name.as_str() <= h.as_str()
933 && applied.contains(&m.name)
934 })
935 .collect();
936 to_unapply.reverse();
937 for mig in to_unapply {
938 unapply_locked(pool, dir, &mig.name, ledger).await?;
939 touched.push(mig);
940 }
941 }
942 }
943 }
944 }
945 Ok(touched)
946 })
947 .await
948}
949
950#[cfg(feature = "postgres")]
969pub async fn migrate_embedded(
970 pool: &PgPool,
971 embedded: &[(&str, &str)],
972) -> Result<Vec<Migration>, MigrateError> {
973 Builder::default().migrate_embedded(pool, embedded).await
974}
975
976#[cfg(feature = "postgres")]
977async fn migrate_embedded_with_ledger(
978 pool: &PgPool,
979 embedded: &[(&str, &str)],
980 ledger: &str,
981) -> Result<Vec<Migration>, MigrateError> {
982 ensure_ledger_for(pool, ledger).await?;
983 with_migrate_lock(pool, async {
984 let mut all: Vec<Migration> = Vec::with_capacity(embedded.len());
985 for (name, json) in embedded {
986 let mig = file::parse(json)?;
987 if mig.name != *name {
988 return Err(MigrateError::Validation(format!(
989 "embedded entry key `{name}` doesn't match migration `name` field `{}`",
990 mig.name,
991 )));
992 }
993 all.push(mig);
994 }
995 all.sort_by(|a, b| a.name.cmp(&b.name));
996 file::validate_chain(&all, "embedded slice")?;
997
998 let applied = applied_set_for(pool, ledger).await?;
999 let pending: Vec<Migration> = all
1000 .into_iter()
1001 .filter(|m| !applied.contains(&m.name))
1002 .collect();
1003
1004 let mut newly = Vec::with_capacity(pending.len());
1005 for mig in pending {
1006 apply_one(pool, &mig, ledger).await?;
1007 newly.push(mig);
1008 }
1009 Ok(newly)
1010 })
1011 .await
1012}
1013
1014#[cfg(feature = "postgres")]
1024pub async fn downgrade(
1025 pool: &PgPool,
1026 dir: &Path,
1027 steps: usize,
1028) -> Result<Vec<Migration>, MigrateError> {
1029 Builder::default().downgrade(pool, dir, steps).await
1030}
1031
1032#[cfg(feature = "postgres")]
1033async fn downgrade_with_ledger(
1034 pool: &PgPool,
1035 dir: &Path,
1036 steps: usize,
1037 ledger: &str,
1038) -> Result<Vec<Migration>, MigrateError> {
1039 if steps == 0 {
1040 return Ok(Vec::new());
1041 }
1042 ensure_ledger_for(pool, ledger).await?;
1043 with_migrate_lock(pool, async {
1044 let all = file::list_dir(dir)?;
1045 let applied = applied_set_for(pool, ledger).await?;
1046
1047 let applied_in_order: Vec<Migration> = all
1048 .into_iter()
1049 .filter(|m| applied.contains(&m.name))
1050 .collect();
1051 if applied_in_order.is_empty() {
1052 return Ok(Vec::new());
1053 }
1054
1055 let n = steps.min(applied_in_order.len());
1056 let to_unapply: Vec<Migration> = applied_in_order.into_iter().rev().take(n).collect();
1057
1058 let mut touched = Vec::with_capacity(to_unapply.len());
1059 for mig in to_unapply {
1060 unapply_locked(pool, dir, &mig.name, ledger).await?;
1061 touched.push(mig);
1062 }
1063 Ok(touched)
1064 })
1065 .await
1066}
1067
1068#[cfg(feature = "postgres")]
1069async fn apply_one(pool: &PgPool, mig: &Migration, ledger: &str) -> Result<(), MigrateError> {
1070 if mig.atomic {
1071 apply_atomic(pool, mig, ledger).await
1072 } else {
1073 apply_loose(pool, mig, ledger).await
1074 }
1075}
1076
1077#[cfg(feature = "postgres")]
1078async fn unapply_all_in_order(
1079 pool: &PgPool,
1080 dir: &Path,
1081 all: &[Migration],
1082 applied: &HashSet<String>,
1083 ledger: &str,
1084) -> Result<Vec<Migration>, MigrateError> {
1085 let mut to_unapply: Vec<Migration> = all
1086 .iter()
1087 .filter(|m| applied.contains(&m.name))
1088 .cloned()
1089 .collect();
1090 to_unapply.reverse();
1091 let mut touched = Vec::with_capacity(to_unapply.len());
1092 for mig in to_unapply {
1093 unapply_locked(pool, dir, &mig.name, ledger).await?;
1097 touched.push(mig);
1098 }
1099 Ok(touched)
1100}
1101
1102#[cfg(feature = "postgres")]
1130pub async fn unapply(pool: &PgPool, dir: &Path, name: &str) -> Result<Migration, MigrateError> {
1131 Builder::default().unapply(pool, dir, name).await
1132}
1133
1134#[cfg(feature = "postgres")]
1135async fn unapply_with_ledger(
1136 pool: &PgPool,
1137 dir: &Path,
1138 name: &str,
1139 ledger: &str,
1140) -> Result<Migration, MigrateError> {
1141 ensure_ledger_for(pool, ledger).await?;
1142 with_migrate_lock(pool, async {
1143 check_is_head(pool, dir, name, ledger).await?;
1144 unapply_locked(pool, dir, name, ledger).await
1145 })
1146 .await
1147}
1148
1149#[cfg(feature = "postgres")]
1160pub async fn unapply_force(
1161 pool: &PgPool,
1162 dir: &Path,
1163 name: &str,
1164) -> Result<Migration, MigrateError> {
1165 Builder::default().unapply_force(pool, dir, name).await
1166}
1167
1168#[cfg(feature = "postgres")]
1169async fn unapply_force_with_ledger(
1170 pool: &PgPool,
1171 dir: &Path,
1172 name: &str,
1173 ledger: &str,
1174) -> Result<Migration, MigrateError> {
1175 ensure_ledger_for(pool, ledger).await?;
1176 with_migrate_lock(pool, unapply_locked(pool, dir, name, ledger)).await
1177}
1178
1179#[cfg(feature = "postgres")]
1184async fn check_is_head(
1185 pool: &PgPool,
1186 dir: &Path,
1187 name: &str,
1188 ledger: &str,
1189) -> Result<(), MigrateError> {
1190 let applied = applied_set_for(pool, ledger).await?;
1191 if !applied.contains(name) {
1192 return Ok(());
1193 }
1194 let all = file::list_dir(dir)?;
1195 let head = all
1196 .iter()
1197 .rev()
1198 .find(|m| applied.contains(&m.name))
1199 .map(|m| m.name.as_str());
1200 match head {
1201 Some(h) if h == name => Ok(()),
1202 Some(h) => Err(MigrateError::Validation(format!(
1203 "refusing to unapply `{name}` out of order: current head is `{h}`. \
1204 Use `downgrade(pool, dir, n)` / `migrate_to(pool, dir, target)` for \
1205 ordered rollback, or `unapply_force` to bypass.",
1206 ))),
1207 None => Ok(()),
1208 }
1209}
1210
1211#[cfg(feature = "postgres")]
1217async fn unapply_locked(
1218 pool: &PgPool,
1219 dir: &Path,
1220 name: &str,
1221 ledger: &str,
1222) -> Result<Migration, MigrateError> {
1223 let all = file::list_dir(dir)?;
1224 let target = all
1225 .iter()
1226 .find(|m| m.name == name)
1227 .cloned()
1228 .ok_or_else(|| {
1229 MigrateError::Validation(format!("migration `{name}` not found in {}", dir.display()))
1230 })?;
1231
1232 let prev_snapshot = match &target.prev {
1233 None => SchemaSnapshot {
1234 tables: vec![],
1235 m2m_tables: vec![],
1236 indexes: vec![],
1237 checks: vec![],
1238 excludes: vec![],
1239 },
1240 Some(prev_name) => all
1241 .iter()
1242 .find(|m| &m.name == prev_name)
1243 .map(|m| m.snapshot.clone())
1244 .ok_or_else(|| {
1245 MigrateError::Validation(format!(
1246 "migration `{name}` declares prev=`{prev_name}` but that file is missing in {}",
1247 dir.display()
1248 ))
1249 })?,
1250 };
1251
1252 let inverted = invert(&target.forward, &prev_snapshot)?;
1253
1254 if target.atomic {
1255 unapply_atomic(pool, &target, &inverted, &prev_snapshot, ledger).await?;
1256 } else {
1257 unapply_loose(pool, &target, &inverted, &prev_snapshot, ledger).await?;
1258 }
1259
1260 Ok(target)
1261}
1262
1263#[cfg(feature = "postgres")]
1264async fn unapply_atomic(
1265 pool: &PgPool,
1266 target: &Migration,
1267 inverted: &[Operation],
1268 snapshot: &SchemaSnapshot,
1269 ledger: &str,
1270) -> Result<(), MigrateError> {
1271 tracing::info!(migration = %target.name, "unapplying (atomic)");
1272 let mut tx = pool.begin().await?;
1273 let mut deferred_fks: Vec<String> = Vec::new();
1274 for op in inverted {
1275 match op {
1276 Operation::Schema(change) => {
1277 let batch = render_changes_split(std::slice::from_ref(change), snapshot)
1278 .map_err(MigrateError::Validation)?;
1279 for stmt in batch.immediate {
1280 sqlx::query(&stmt).execute(&mut *tx).await?;
1281 }
1282 deferred_fks.extend(batch.deferred_fks);
1283 }
1284 Operation::Data(d) => {
1285 sqlx::query(&d.sql).execute(&mut *tx).await?;
1286 }
1287 Operation::Callback(c) => {
1288 invoke_migration_callback(c, pool.clone().into()).await?;
1291 }
1292 }
1293 }
1294 for stmt in deferred_fks {
1295 sqlx::query(&stmt).execute(&mut *tx).await?;
1296 }
1297 sqlx::query(&format!("DELETE FROM {ledger} WHERE name = $1"))
1298 .bind(&target.name)
1299 .execute(&mut *tx)
1300 .await?;
1301 tx.commit().await?;
1302 Ok(())
1303}
1304
1305#[cfg(feature = "postgres")]
1306async fn unapply_loose(
1307 pool: &PgPool,
1308 target: &Migration,
1309 inverted: &[Operation],
1310 snapshot: &SchemaSnapshot,
1311 ledger: &str,
1312) -> Result<(), MigrateError> {
1313 tracing::info!(migration = %target.name, "unapplying (non-atomic)");
1314 let mut deferred_fks: Vec<String> = Vec::new();
1315 for op in inverted {
1316 match op {
1317 Operation::Schema(change) => {
1318 let batch = render_changes_split(std::slice::from_ref(change), snapshot)
1319 .map_err(MigrateError::Validation)?;
1320 for stmt in batch.immediate {
1321 sqlx::query(&stmt).execute(pool).await?;
1322 }
1323 deferred_fks.extend(batch.deferred_fks);
1324 }
1325 Operation::Data(d) => {
1326 sqlx::query(&d.sql).execute(pool).await?;
1327 }
1328 Operation::Callback(c) => {
1329 invoke_migration_callback(c, pool.clone().into()).await?;
1332 }
1333 }
1334 }
1335 for stmt in deferred_fks {
1336 sqlx::query(&stmt).execute(pool).await?;
1337 }
1338 sqlx::query(&format!("DELETE FROM {ledger} WHERE name = $1"))
1339 .bind(&target.name)
1340 .execute(pool)
1341 .await?;
1342 Ok(())
1343}
1344
1345#[cfg(feature = "postgres")]
1346async fn apply_loose(pool: &PgPool, mig: &Migration, ledger: &str) -> Result<(), MigrateError> {
1347 tracing::info!(migration = %mig.name, "applying (non-atomic)");
1348 let mut deferred_fks: Vec<String> = Vec::new();
1349 for op in &mig.forward {
1350 match op {
1351 Operation::Schema(change) => {
1352 let batch = render_changes_split(std::slice::from_ref(change), &mig.snapshot)
1353 .map_err(MigrateError::Validation)?;
1354 for stmt in batch.immediate {
1355 sqlx::query(&stmt).execute(pool).await?;
1356 }
1357 deferred_fks.extend(batch.deferred_fks);
1358 }
1359 Operation::Data(d) => {
1360 sqlx::query(&d.sql).execute(pool).await?;
1361 }
1362 Operation::Callback(c) => {
1363 invoke_migration_callback(c, pool.clone().into()).await?;
1366 }
1367 }
1368 }
1369 for stmt in deferred_fks {
1370 sqlx::query(&stmt).execute(pool).await?;
1371 }
1372 sqlx::query(&format!("INSERT INTO {ledger} (name) VALUES ($1)"))
1373 .bind(&mig.name)
1374 .execute(pool)
1375 .await?;
1376 Ok(())
1377}
1378
1379pub async fn ensure_ledger_pool(pool: &crate::sql::Pool) -> Result<(), MigrateError> {
1409 ensure_ledger_pool_with_ledger(pool, LEDGER_TABLE).await
1410}
1411
1412pub async fn ensure_ledger_pool_with_ledger(
1416 pool: &crate::sql::Pool,
1417 ledger: &str,
1418) -> Result<(), MigrateError> {
1419 let dialect_name = pool.dialect().name();
1420 let timestamp_col = match dialect_name {
1421 "postgres" => "TIMESTAMPTZ NOT NULL DEFAULT NOW()",
1422 "mysql" => "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)",
1423 "sqlite" => "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1427 other => {
1432 return Err(MigrateError::Validation(format!(
1433 "ensure_ledger_pool: unrecognized dialect `{other}`"
1434 )));
1435 }
1436 };
1437 let create_sql = format!(
1438 "CREATE TABLE IF NOT EXISTS {ledger} (\
1439 name VARCHAR(255) PRIMARY KEY, \
1440 applied_at {timestamp_col})"
1441 );
1442 crate::sql::raw_execute_pool(pool, &create_sql, ::std::vec::Vec::new()).await?;
1443 Ok(())
1444}
1445
1446pub async fn applied_set_pool(pool: &crate::sql::Pool) -> Result<HashSet<String>, MigrateError> {
1453 applied_set_pool_with_ledger(pool, LEDGER_TABLE).await
1454}
1455
1456pub async fn applied_set_pool_with_ledger(
1460 pool: &crate::sql::Pool,
1461 ledger: &str,
1462) -> Result<HashSet<String>, MigrateError> {
1463 let sql = format!("SELECT name FROM {ledger}");
1464 let rows: Vec<(String,)> = crate::sql::raw_query_pool(&sql, Vec::new(), pool).await?;
1471 Ok(rows.into_iter().map(|(name,)| name).collect())
1472}
1473
1474pub async fn migrate_pool(
1492 pool: &crate::sql::Pool,
1493 dir: &Path,
1494) -> Result<Vec<Migration>, MigrateError> {
1495 migrate_pool_with_ledger(pool, dir, LEDGER_TABLE).await
1496}
1497
1498pub async fn migrate_pool_with_progress(
1514 pool: &crate::sql::Pool,
1515 dir: &Path,
1516 observer: &dyn MigrationObserver,
1517) -> Result<Vec<Migration>, MigrateError> {
1518 migrate_pool_with_ledger_opts(pool, dir, LEDGER_TABLE, false, Some(observer)).await
1519}
1520
1521pub async fn migrate_pool_with_ledger_fake_initial_with_progress(
1528 pool: &crate::sql::Pool,
1529 dir: &Path,
1530 ledger: &str,
1531 observer: &dyn MigrationObserver,
1532) -> Result<Vec<Migration>, MigrateError> {
1533 migrate_pool_with_ledger_opts(pool, dir, ledger, true, Some(observer)).await
1534}
1535
1536pub async fn migrate_pool_with_ledger(
1546 pool: &crate::sql::Pool,
1547 dir: &Path,
1548 ledger: &str,
1549) -> Result<Vec<Migration>, MigrateError> {
1550 migrate_pool_with_ledger_opts(pool, dir, ledger, false, None).await
1551}
1552
1553pub async fn migrate_pool_with_ledger_fake_initial(
1578 pool: &crate::sql::Pool,
1579 dir: &Path,
1580 ledger: &str,
1581) -> Result<Vec<Migration>, MigrateError> {
1582 migrate_pool_with_ledger_opts(pool, dir, ledger, true, None).await
1583}
1584
1585async fn migrate_pool_with_ledger_opts(
1586 pool: &crate::sql::Pool,
1587 dir: &Path,
1588 ledger: &str,
1589 fake_initial: bool,
1590 observer: Option<&dyn MigrationObserver>,
1591) -> Result<Vec<Migration>, MigrateError> {
1592 ensure_ledger_pool_with_ledger(pool, ledger).await?;
1593 with_migrate_lock_pool(pool, async {
1594 let all = file::list_dir(dir)?;
1595 let applied = applied_set_pool_with_ledger(pool, ledger).await?;
1596 let pending = pending_migrations(all, &applied);
1597
1598 let total = pending.len();
1599 emit(observer, || MigrationEvent::Planned { total });
1600
1601 let mut newly = Vec::with_capacity(total);
1602 for (i, mig) in pending.into_iter().enumerate() {
1603 let index = i + 1;
1604 emit(observer, || MigrationEvent::Started {
1605 name: mig.name.clone(),
1606 index,
1607 total,
1608 });
1609 let began = std::time::Instant::now();
1610
1611 let outcome = reconcile_and_apply(pool, &mig, &applied, ledger, fake_initial).await;
1616 let outcome = match outcome {
1617 Ok(outcome) => outcome,
1618 Err(e) => {
1619 emit(observer, || MigrationEvent::Failed {
1620 name: mig.name.clone(),
1621 index,
1622 total,
1623 error: e.to_string(),
1624 });
1625 return Err(e);
1626 }
1627 };
1628
1629 emit(observer, || MigrationEvent::Finished {
1630 name: mig.name.clone(),
1631 index,
1632 total,
1633 outcome,
1634 elapsed: began.elapsed(),
1635 });
1636 newly.push(mig);
1637 }
1638 Ok(newly)
1639 })
1640 .await
1641}
1642
1643async fn reconcile_and_apply(
1650 pool: &crate::sql::Pool,
1651 mig: &Migration,
1652 applied: &HashSet<String>,
1653 ledger: &str,
1654 fake_initial: bool,
1655) -> Result<Outcome, MigrateError> {
1656 match reconcile(pool, mig, applied, fake_initial).await? {
1657 ReconcileAction::Fake => {
1658 fake_apply_pool(pool, mig, ledger).await?;
1659 Ok(Outcome::Faked)
1660 }
1661 other => {
1662 let (effective, outcome) = match other {
1663 ReconcileAction::RunPartial(existing) => (
1664 std::borrow::Cow::Owned(without_tables(mig, &existing)),
1665 Outcome::RanPartial { skipped: existing },
1666 ),
1667 _ => (std::borrow::Cow::Borrowed(mig), Outcome::Ran),
1668 };
1669 if effective.atomic {
1670 apply_atomic_pool(pool, &effective, ledger).await?;
1671 } else {
1672 apply_nonatomic_pool(pool, &effective, ledger).await?;
1673 }
1674 Ok(outcome)
1675 }
1676 }
1677}
1678
1679#[derive(Debug, Clone, PartialEq, Eq)]
1684enum ReconcileAction {
1685 Run,
1687 Fake,
1690 RunPartial(Vec<String>),
1694}
1695
1696async fn reconcile(
1722 pool: &crate::sql::Pool,
1723 mig: &Migration,
1724 applied: &HashSet<String>,
1725 fake_initial: bool,
1726) -> Result<ReconcileAction, MigrateError> {
1727 if !mig.replaces.is_empty() {
1728 let present = mig.replaces.iter().filter(|n| applied.contains(*n)).count();
1729 if present == mig.replaces.len() {
1730 tracing::info!(
1731 migration = %mig.name,
1732 replaces = ?mig.replaces,
1733 "reconciling squash — every replaced migration is already applied; \
1734 recording it and tombstoning them without running DDL"
1735 );
1736 return Ok(ReconcileAction::Fake);
1737 }
1738 if present > 0 {
1739 let missing: Vec<&str> = mig
1740 .replaces
1741 .iter()
1742 .filter(|n| !applied.contains(*n))
1743 .map(String::as_str)
1744 .collect();
1745 return Err(MigrateError::Validation(format!(
1746 "cannot reconcile squash `{}`: it replaces {} migration(s) but only {} are \
1747 recorded as applied (missing: {}). The database is in a partial state — \
1748 resolve it by hand (see `migrate --fake <name>`) rather than guessing.",
1749 mig.name,
1750 mig.replaces.len(),
1751 present,
1752 missing.join(", "),
1753 )));
1754 }
1755 let targets = create_table_targets(mig);
1759 if targets.is_empty() {
1760 return Ok(ReconcileAction::Run);
1761 }
1762 let existing = count_existing_tables(pool, &targets).await;
1763 if existing == targets.len() {
1764 tracing::info!(
1765 migration = %mig.name,
1766 tables = %targets.join(", "),
1767 "reconciling squash — its tables already exist (cross-ledger \
1768 fake-initial); recording it without running DDL"
1769 );
1770 return Ok(ReconcileAction::Fake);
1771 }
1772 if existing > 0 {
1773 return Err(MigrateError::Validation(format!(
1774 "cannot reconcile squash `{}`: {} of its {} tables already exist but the rest \
1775 do not. The database is in a partial state — resolve it by hand (see \
1776 `migrate --fake <name>`) rather than guessing.",
1777 mig.name,
1778 existing,
1779 targets.len(),
1780 )));
1781 }
1782 return Ok(ReconcileAction::Run);
1783 }
1784
1785 if fake_initial {
1788 if let Some(tables) = create_only_tables(mig) {
1789 let existing: Vec<String> = {
1790 let mut present = Vec::new();
1791 for t in &tables {
1792 if count_existing_tables(pool, std::slice::from_ref(t)).await == 1 {
1793 present.push(t.clone());
1794 }
1795 }
1796 present
1797 };
1798 if existing.len() == tables.len() {
1799 tracing::warn!(
1800 migration = %mig.name,
1801 tables = %tables.join(", "),
1802 "fake-initial: tables already exist (pre-migration \
1803 `ensure_table` era) — recorded as applied without \
1804 running its CREATE TABLE; existing data left intact"
1805 );
1806 return Ok(ReconcileAction::Fake);
1807 }
1808 if !existing.is_empty() {
1809 tracing::warn!(
1817 migration = %mig.name,
1818 existing = %existing.join(", "),
1819 "fake-initial: some framework tables already exist — creating \
1820 only the missing ones and leaving the existing untouched"
1821 );
1822 return Ok(ReconcileAction::RunPartial(existing));
1823 }
1824 }
1825 }
1826 Ok(ReconcileAction::Run)
1827}
1828
1829pub(crate) fn without_tables(mig: &Migration, existing: &[String]) -> Migration {
1837 use super::diff::SchemaChange as SC;
1838 let skip = |t: &String| existing.iter().any(|e| e == t);
1839 let forward = mig
1840 .forward
1841 .iter()
1842 .filter(|op| match op {
1843 Operation::Schema(SC::CreateTable(t)) => !skip(t),
1844 Operation::Schema(SC::CreateIndex { table, .. }) => !skip(table),
1845 Operation::Schema(SC::CreateM2MTable { through, .. }) => !skip(through),
1846 _ => true,
1847 })
1848 .cloned()
1849 .collect();
1850 Migration {
1851 forward,
1852 ..mig.clone()
1853 }
1854}
1855
1856fn pending_migrations(all: Vec<Migration>, applied: &HashSet<String>) -> Vec<Migration> {
1866 let superseded: HashSet<&str> = all
1867 .iter()
1868 .filter(|m| applied.contains(&m.name))
1869 .flat_map(|m| m.replaces.iter().map(String::as_str))
1870 .collect();
1871 all.iter()
1872 .filter(|m| !applied.contains(&m.name) && !superseded.contains(m.name.as_str()))
1873 .cloned()
1874 .collect()
1875}
1876
1877fn create_table_targets(mig: &Migration) -> Vec<String> {
1883 mig.forward
1884 .iter()
1885 .filter_map(|op| match op {
1886 Operation::Schema(super::diff::SchemaChange::CreateTable(t)) => Some(t.clone()),
1887 _ => None,
1888 })
1889 .collect()
1890}
1891
1892fn create_only_tables(mig: &Migration) -> Option<Vec<String>> {
1914 use super::diff::SchemaChange as SC;
1915 let mut tables: Vec<String> = Vec::new();
1916 let mut targeted: Vec<&str> = Vec::new();
1917 for op in &mig.forward {
1918 match op {
1919 Operation::Schema(SC::CreateTable(t)) => tables.push(t.clone()),
1920 Operation::Schema(SC::CreateIndex { table, .. }) => targeted.push(table.as_str()),
1921 Operation::Schema(SC::CreateM2MTable { through, .. }) => tables.push(through.clone()),
1922 _ => return None,
1923 }
1924 }
1925 if tables.is_empty() {
1926 return None;
1927 }
1928 if targeted
1930 .iter()
1931 .any(|t| !tables.iter().any(|created| created == t))
1932 {
1933 return None;
1934 }
1935 Some(tables)
1936}
1937
1938async fn count_existing_tables(pool: &crate::sql::Pool, tables: &[String]) -> usize {
1944 let mut n = 0;
1945 for t in tables {
1946 if table_exists_here(pool, t).await {
1947 n += 1;
1948 }
1949 }
1950 n
1951}
1952
1953async fn table_exists_here(pool: &crate::sql::Pool, table: &str) -> bool {
1966 match pool {
1967 #[cfg(feature = "postgres")]
1968 crate::sql::Pool::Postgres(pg) => sqlx::query_scalar::<_, i64>(
1969 "SELECT COUNT(*) FROM information_schema.tables \
1970 WHERE table_schema = current_schema() AND table_name = $1",
1971 )
1972 .bind(table)
1973 .fetch_one(pg)
1974 .await
1975 .map(|n| n > 0)
1976 .unwrap_or(false),
1977 #[cfg(feature = "mysql")]
1978 crate::sql::Pool::Mysql(my) => sqlx::query_scalar::<_, i64>(
1979 "SELECT COUNT(*) FROM information_schema.tables \
1980 WHERE table_schema = DATABASE() AND table_name = ?",
1981 )
1982 .bind(table)
1983 .fetch_one(my)
1984 .await
1985 .map(|n| n > 0)
1986 .unwrap_or(false),
1987 #[cfg(feature = "sqlite")]
1988 crate::sql::Pool::Sqlite(sq) => sqlx::query_scalar::<_, i64>(
1989 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?",
1990 )
1991 .bind(table)
1992 .fetch_one(sq)
1993 .await
1994 .map(|n| n > 0)
1995 .unwrap_or(false),
1996 }
1997}
1998
1999async fn fake_apply_pool(
2007 pool: &crate::sql::Pool,
2008 mig: &Migration,
2009 ledger: &str,
2010) -> Result<(), MigrateError> {
2011 let (insert, delete) = {
2015 let dialect = pool.dialect();
2016 let placeholder = dialect.placeholder(1);
2017 let table = dialect.quote_ident(ledger);
2018 let name_col = dialect.quote_ident("name");
2019 let conflict_tail = dialect.insert_on_conflict_skip(&[&name_col]);
2020 (
2021 format!("INSERT INTO {table} ({name_col}) VALUES ({placeholder}) {conflict_tail}"),
2022 format!("DELETE FROM {table} WHERE {name_col} = {placeholder}"),
2023 )
2024 };
2025 crate::sql::raw_execute_pool(
2026 pool,
2027 &insert,
2028 vec![crate::core::SqlValue::String(mig.name.clone())],
2029 )
2030 .await
2031 .map_err(|e| {
2032 MigrateError::Validation(format!(
2033 "fake-apply: ledger insert for `{}` failed: {e}",
2034 mig.name
2035 ))
2036 })?;
2037
2038 if mig.replaces.is_empty() {
2039 return Ok(());
2040 }
2041 for replaced in &mig.replaces {
2042 crate::sql::raw_execute_pool(
2043 pool,
2044 &delete,
2045 vec![crate::core::SqlValue::String(replaced.clone())],
2046 )
2047 .await
2048 .map_err(|e| {
2049 MigrateError::Validation(format!(
2050 "fake-apply: tombstoning `{replaced}` (replaced by `{}`) failed: {e}",
2051 mig.name
2052 ))
2053 })?;
2054 }
2055 Ok(())
2056}
2057
2058async fn with_migrate_lock_pool<F, R>(pool: &crate::sql::Pool, body: F) -> Result<R, MigrateError>
2075where
2076 F: std::future::Future<Output = Result<R, MigrateError>>,
2077{
2078 match pool {
2079 #[cfg(feature = "postgres")]
2080 crate::sql::Pool::Postgres(pg) => {
2081 let mut lock_conn = pg.acquire().await?;
2082 sqlx::query("SELECT pg_advisory_lock($1)")
2083 .bind(MIGRATE_LOCK_KEY)
2084 .execute(&mut *lock_conn)
2085 .await?;
2086 let result = body.await;
2087 let _ = sqlx::query("SELECT pg_advisory_unlock($1)")
2090 .bind(MIGRATE_LOCK_KEY)
2091 .execute(&mut *lock_conn)
2092 .await;
2093 result
2094 }
2095 #[cfg(feature = "mysql")]
2096 crate::sql::Pool::Mysql(my) => {
2097 let lock_name = format!("rustango_migrate_{:x}", MIGRATE_LOCK_KEY);
2098 let mut lock_conn = my.acquire().await?;
2099 sqlx::query("SELECT GET_LOCK(?, -1)")
2100 .bind(&lock_name)
2101 .execute(&mut *lock_conn)
2102 .await?;
2103 let result = body.await;
2104 let _ = sqlx::query("SELECT RELEASE_LOCK(?)")
2107 .bind(&lock_name)
2108 .execute(&mut *lock_conn)
2109 .await;
2110 result
2111 }
2112 #[cfg(feature = "sqlite")]
2113 crate::sql::Pool::Sqlite(_) => {
2114 body.await
2121 }
2122 }
2123}
2124
2125async fn apply_atomic_pool(
2133 pool: &crate::sql::Pool,
2134 mig: &Migration,
2135 ledger: &str,
2136) -> Result<(), MigrateError> {
2137 tracing::info!(migration = %mig.name, "applying (atomic, _pool)");
2138 let dialect = pool.dialect();
2139 match pool {
2140 #[cfg(feature = "postgres")]
2141 crate::sql::Pool::Postgres(pg) => {
2142 let mut tx = pg.begin().await?;
2143 let mut deferred_fks: Vec<String> = Vec::new();
2144 for op in &mig.forward {
2145 match op {
2146 Operation::Schema(change) => {
2147 let batch = super::diff::render_changes_split_with_dialect(
2148 std::slice::from_ref(change),
2149 &mig.snapshot,
2150 dialect,
2151 )
2152 .map_err(MigrateError::Validation)?;
2153 for stmt in batch.immediate {
2154 sqlx::query(&stmt).execute(&mut *tx).await?;
2155 }
2156 deferred_fks.extend(batch.deferred_fks);
2157 }
2158 Operation::Data(d) => {
2159 sqlx::query(&d.sql).execute(&mut *tx).await?;
2160 }
2161 Operation::Callback(c) => {
2162 invoke_migration_callback(c, pool.clone().into()).await?;
2164 }
2165 }
2166 }
2167 for stmt in deferred_fks {
2168 sqlx::query(&stmt).execute(&mut *tx).await?;
2169 }
2170 sqlx::query(&format!("INSERT INTO {ledger} (name) VALUES ($1)"))
2171 .bind(&mig.name)
2172 .execute(&mut *tx)
2173 .await?;
2174 tx.commit().await?;
2175 }
2176 #[cfg(feature = "mysql")]
2177 crate::sql::Pool::Mysql(my) => {
2178 tracing::warn!(
2203 migration = %mig.name,
2204 "MySQL silently auto-commits on every DDL statement (CREATE/ALTER/DROP/INDEX); \
2205 the atomic-migration wrapper only protects RunSQL/RunPython operations \
2206 between DDL ops. A failure mid-DDL leaves the migration partially applied \
2207 and requires manual recovery. See migrate/runner.rs::apply_atomic_pool for \
2208 details. Tracked in #559."
2209 );
2210 let mut tx = my.begin().await?;
2211 let mut deferred_fks: Vec<String> = Vec::new();
2212 for op in &mig.forward {
2213 match op {
2214 Operation::Schema(change) => {
2215 let batch = super::diff::render_changes_split_with_dialect(
2216 std::slice::from_ref(change),
2217 &mig.snapshot,
2218 dialect,
2219 )
2220 .map_err(MigrateError::Validation)?;
2221 for stmt in batch.immediate {
2222 sqlx::query(&stmt).execute(&mut *tx).await?;
2223 }
2224 deferred_fks.extend(batch.deferred_fks);
2225 }
2226 Operation::Data(d) => {
2227 sqlx::query(&d.sql).execute(&mut *tx).await?;
2228 }
2229 Operation::Callback(c) => {
2230 invoke_migration_callback(c, pool.clone().into()).await?;
2232 }
2233 }
2234 }
2235 for stmt in deferred_fks {
2236 sqlx::query(&stmt).execute(&mut *tx).await?;
2237 }
2238 sqlx::query(&format!("INSERT INTO {ledger} (name) VALUES (?)"))
2239 .bind(&mig.name)
2240 .execute(&mut *tx)
2241 .await?;
2242 tx.commit().await?;
2243 }
2244 #[cfg(feature = "sqlite")]
2245 crate::sql::Pool::Sqlite(sq) => {
2246 let mut tx = sq.begin().await?;
2249 let mut deferred_fks: Vec<String> = Vec::new();
2250 for op in &mig.forward {
2251 match op {
2252 Operation::Schema(change) => {
2253 let batch = super::diff::render_changes_split_with_dialect(
2254 std::slice::from_ref(change),
2255 &mig.snapshot,
2256 dialect,
2257 )
2258 .map_err(MigrateError::Validation)?;
2259 for stmt in batch.immediate {
2260 sqlx::query(&stmt).execute(&mut *tx).await?;
2261 }
2262 deferred_fks.extend(batch.deferred_fks);
2263 }
2264 Operation::Data(d) => {
2265 sqlx::query(&d.sql).execute(&mut *tx).await?;
2266 }
2267 Operation::Callback(c) => {
2268 invoke_migration_callback(c, pool.clone().into()).await?;
2270 }
2271 }
2272 }
2273 for stmt in deferred_fks {
2274 sqlx::query(&stmt).execute(&mut *tx).await?;
2275 }
2276 sqlx::query(&format!("INSERT INTO {ledger} (name) VALUES (?)"))
2277 .bind(&mig.name)
2278 .execute(&mut *tx)
2279 .await?;
2280 tx.commit().await?;
2281 }
2282 }
2283 Ok(())
2284}
2285
2286async fn apply_nonatomic_pool(
2290 pool: &crate::sql::Pool,
2291 mig: &Migration,
2292 ledger: &str,
2293) -> Result<(), MigrateError> {
2294 tracing::info!(migration = %mig.name, "applying (non-atomic, _pool)");
2295 let mut deferred_fks: Vec<String> = Vec::new();
2296 for op in &mig.forward {
2297 match op {
2298 Operation::Schema(change) => {
2299 let batch = super::diff::render_changes_split_with_dialect(
2300 std::slice::from_ref(change),
2301 &mig.snapshot,
2302 pool.dialect(),
2303 )
2304 .map_err(MigrateError::Validation)?;
2305 for stmt in batch.immediate {
2306 crate::sql::raw_execute_pool(pool, &stmt, ::std::vec::Vec::new()).await?;
2307 }
2308 deferred_fks.extend(batch.deferred_fks);
2309 }
2310 Operation::Data(d) => {
2311 crate::sql::raw_execute_pool(pool, &d.sql, ::std::vec::Vec::new()).await?;
2312 }
2313 Operation::Callback(c) => {
2314 invoke_migration_callback(c, pool.clone()).await?;
2316 }
2317 }
2318 }
2319 for stmt in deferred_fks {
2320 crate::sql::raw_execute_pool(pool, &stmt, ::std::vec::Vec::new()).await?;
2321 }
2322 let placeholder = pool.dialect().placeholder(1);
2323 let insert_sql = format!("INSERT INTO {ledger} (name) VALUES ({placeholder})");
2324 crate::sql::raw_execute_pool(
2325 pool,
2326 &insert_sql,
2327 ::std::vec![crate::core::SqlValue::String(mig.name.clone())],
2328 )
2329 .await?;
2330 Ok(())
2331}
2332
2333pub async fn migrate_to_pool(
2352 pool: &crate::sql::Pool,
2353 dir: &Path,
2354 target: &str,
2355) -> Result<Vec<Migration>, MigrateError> {
2356 migrate_to_pool_with_ledger(pool, dir, target, LEDGER_TABLE).await
2357}
2358
2359pub async fn migrate_to_pool_with_ledger(
2365 pool: &crate::sql::Pool,
2366 dir: &Path,
2367 target: &str,
2368 ledger: &str,
2369) -> Result<Vec<Migration>, MigrateError> {
2370 ensure_ledger_pool_with_ledger(pool, ledger).await?;
2371 with_migrate_lock_pool(pool, async {
2372 let all = file::list_dir(dir)?;
2373 let applied = applied_set_pool_with_ledger(pool, ledger).await?;
2374
2375 if target == "zero" {
2376 return unapply_all_in_order_pool(pool, dir, &all, &applied, ledger).await;
2377 }
2378
2379 if !all.iter().any(|m| m.name == target) {
2380 return Err(MigrateError::Validation(format!(
2381 "target migration `{target}` not found in {}",
2382 dir.display()
2383 )));
2384 }
2385
2386 let head = all
2387 .iter()
2388 .rev()
2389 .find(|m| applied.contains(&m.name))
2390 .map(|m| m.name.clone());
2391
2392 let mut touched = Vec::new();
2393 match head {
2394 None => {
2395 for mig in all.into_iter().filter(|m| m.name.as_str() <= target) {
2396 apply_one_pool(pool, &mig, ledger).await?;
2397 touched.push(mig);
2398 }
2399 }
2400 Some(h) => {
2401 use std::cmp::Ordering;
2402 match target.cmp(h.as_str()) {
2403 Ordering::Equal => {}
2404 Ordering::Greater => {
2405 for mig in all.into_iter().filter(|m| {
2406 m.name.as_str() > h.as_str()
2407 && m.name.as_str() <= target
2408 && !applied.contains(&m.name)
2409 }) {
2410 apply_one_pool(pool, &mig, ledger).await?;
2411 touched.push(mig);
2412 }
2413 }
2414 Ordering::Less => {
2415 let mut to_unapply: Vec<Migration> = all
2416 .into_iter()
2417 .filter(|m| {
2418 m.name.as_str() > target
2419 && m.name.as_str() <= h.as_str()
2420 && applied.contains(&m.name)
2421 })
2422 .collect();
2423 to_unapply.reverse();
2424 for mig in to_unapply {
2425 unapply_locked_pool(pool, dir, &mig.name, ledger).await?;
2426 touched.push(mig);
2427 }
2428 }
2429 }
2430 }
2431 }
2432 Ok(touched)
2433 })
2434 .await
2435}
2436
2437pub async fn downgrade_pool(
2442 pool: &crate::sql::Pool,
2443 dir: &Path,
2444 steps: usize,
2445) -> Result<Vec<Migration>, MigrateError> {
2446 downgrade_pool_with_ledger(pool, dir, steps, LEDGER_TABLE).await
2447}
2448
2449pub async fn downgrade_pool_with_ledger(
2455 pool: &crate::sql::Pool,
2456 dir: &Path,
2457 steps: usize,
2458 ledger: &str,
2459) -> Result<Vec<Migration>, MigrateError> {
2460 if steps == 0 {
2461 return Ok(Vec::new());
2462 }
2463 ensure_ledger_pool_with_ledger(pool, ledger).await?;
2464 with_migrate_lock_pool(pool, async {
2465 let all = file::list_dir(dir)?;
2466 let applied = applied_set_pool_with_ledger(pool, ledger).await?;
2467
2468 let applied_in_order: Vec<Migration> = all
2469 .into_iter()
2470 .filter(|m| applied.contains(&m.name))
2471 .collect();
2472 if applied_in_order.is_empty() {
2473 return Ok(Vec::new());
2474 }
2475
2476 let n = steps.min(applied_in_order.len());
2477 let to_unapply: Vec<Migration> = applied_in_order.into_iter().rev().take(n).collect();
2478
2479 let mut touched = Vec::with_capacity(to_unapply.len());
2480 for mig in to_unapply {
2481 unapply_locked_pool(pool, dir, &mig.name, ledger).await?;
2482 touched.push(mig);
2483 }
2484 Ok(touched)
2485 })
2486 .await
2487}
2488
2489pub async fn unapply_pool(
2497 pool: &crate::sql::Pool,
2498 dir: &Path,
2499 name: &str,
2500) -> Result<Migration, MigrateError> {
2501 unapply_pool_with_ledger(pool, dir, name, LEDGER_TABLE).await
2502}
2503
2504pub async fn unapply_pool_with_ledger(
2510 pool: &crate::sql::Pool,
2511 dir: &Path,
2512 name: &str,
2513 ledger: &str,
2514) -> Result<Migration, MigrateError> {
2515 ensure_ledger_pool_with_ledger(pool, ledger).await?;
2516 with_migrate_lock_pool(pool, async {
2517 check_is_head_pool(pool, dir, name, ledger).await?;
2518 unapply_locked_pool(pool, dir, name, ledger).await
2519 })
2520 .await
2521}
2522
2523pub async fn unapply_force_pool(
2529 pool: &crate::sql::Pool,
2530 dir: &Path,
2531 name: &str,
2532) -> Result<Migration, MigrateError> {
2533 unapply_force_pool_with_ledger(pool, dir, name, LEDGER_TABLE).await
2534}
2535
2536async fn unapply_force_pool_with_ledger(
2537 pool: &crate::sql::Pool,
2538 dir: &Path,
2539 name: &str,
2540 ledger: &str,
2541) -> Result<Migration, MigrateError> {
2542 ensure_ledger_pool_with_ledger(pool, ledger).await?;
2543 with_migrate_lock_pool(pool, unapply_locked_pool(pool, dir, name, ledger)).await
2544}
2545
2546pub async fn migrate_dry_run_pool(
2552 pool: &crate::sql::Pool,
2553 dir: &Path,
2554) -> Result<Vec<MigrationPreview>, MigrateError> {
2555 migrate_dry_run_pool_with_ledger(pool, dir, LEDGER_TABLE).await
2556}
2557
2558pub async fn migrate_dry_run_pool_with_ledger(
2564 pool: &crate::sql::Pool,
2565 dir: &Path,
2566 ledger: &str,
2567) -> Result<Vec<MigrationPreview>, MigrateError> {
2568 ensure_ledger_pool_with_ledger(pool, ledger).await?;
2569 let all = file::list_dir(dir)?;
2570 let applied = applied_set_pool_with_ledger(pool, ledger).await?;
2571 let pending: Vec<Migration> = all
2572 .into_iter()
2573 .filter(|m| !applied.contains(&m.name))
2574 .collect();
2575 let mut out = Vec::with_capacity(pending.len());
2576 for mig in pending {
2577 out.push(preview_migration(&mig, ledger)?);
2578 }
2579 Ok(out)
2580}
2581
2582async fn apply_one_pool(
2585 pool: &crate::sql::Pool,
2586 mig: &Migration,
2587 ledger: &str,
2588) -> Result<(), MigrateError> {
2589 if mig.atomic {
2590 apply_atomic_pool(pool, mig, ledger).await
2591 } else {
2592 apply_nonatomic_pool(pool, mig, ledger).await
2593 }
2594}
2595
2596async fn unapply_all_in_order_pool(
2597 pool: &crate::sql::Pool,
2598 dir: &Path,
2599 all: &[Migration],
2600 applied: &HashSet<String>,
2601 ledger: &str,
2602) -> Result<Vec<Migration>, MigrateError> {
2603 let mut to_unapply: Vec<Migration> = all
2604 .iter()
2605 .filter(|m| applied.contains(&m.name))
2606 .cloned()
2607 .collect();
2608 to_unapply.reverse();
2609 let mut touched = Vec::with_capacity(to_unapply.len());
2610 for mig in to_unapply {
2611 unapply_locked_pool(pool, dir, &mig.name, ledger).await?;
2612 touched.push(mig);
2613 }
2614 Ok(touched)
2615}
2616
2617async fn unapply_locked_pool(
2620 pool: &crate::sql::Pool,
2621 dir: &Path,
2622 name: &str,
2623 ledger: &str,
2624) -> Result<Migration, MigrateError> {
2625 let all = file::list_dir(dir)?;
2626 let target = all
2627 .iter()
2628 .find(|m| m.name == name)
2629 .cloned()
2630 .ok_or_else(|| {
2631 MigrateError::Validation(format!("migration `{name}` not found in {}", dir.display()))
2632 })?;
2633
2634 let prev_snapshot = match &target.prev {
2635 None => SchemaSnapshot {
2636 tables: vec![],
2637 m2m_tables: vec![],
2638 indexes: vec![],
2639 checks: vec![],
2640 excludes: vec![],
2641 },
2642 Some(prev_name) => all
2643 .iter()
2644 .find(|m| &m.name == prev_name)
2645 .map(|m| m.snapshot.clone())
2646 .ok_or_else(|| {
2647 MigrateError::Validation(format!(
2648 "migration `{name}` declares prev=`{prev_name}` but that file is missing in {}",
2649 dir.display()
2650 ))
2651 })?,
2652 };
2653
2654 let inverted = invert(&target.forward, &prev_snapshot)?;
2655
2656 if target.atomic {
2657 unapply_atomic_pool(pool, &target, &inverted, &prev_snapshot, ledger).await?;
2658 } else {
2659 unapply_nonatomic_pool(pool, &target, &inverted, &prev_snapshot, ledger).await?;
2660 }
2661
2662 Ok(target)
2663}
2664
2665async fn check_is_head_pool(
2666 pool: &crate::sql::Pool,
2667 dir: &Path,
2668 name: &str,
2669 ledger: &str,
2670) -> Result<(), MigrateError> {
2671 let applied = applied_set_pool_with_ledger(pool, ledger).await?;
2672 if !applied.contains(name) {
2673 return Ok(());
2674 }
2675 let all = file::list_dir(dir)?;
2676 let head = all
2677 .iter()
2678 .rev()
2679 .find(|m| applied.contains(&m.name))
2680 .map(|m| m.name.as_str());
2681 match head {
2682 Some(h) if h == name => Ok(()),
2683 Some(h) => Err(MigrateError::Validation(format!(
2684 "refusing to unapply `{name}` out of order: current head is `{h}`. \
2685 Use `downgrade_pool(pool, dir, n)` / `migrate_to_pool(pool, dir, target)` for \
2686 ordered rollback, or `unapply_force_pool` to bypass.",
2687 ))),
2688 None => Ok(()),
2689 }
2690}
2691
2692async fn unapply_atomic_pool(
2693 pool: &crate::sql::Pool,
2694 target: &Migration,
2695 inverted: &[Operation],
2696 snapshot: &SchemaSnapshot,
2697 ledger: &str,
2698) -> Result<(), MigrateError> {
2699 tracing::info!(migration = %target.name, "unapplying (atomic, _pool)");
2700 match pool {
2701 #[cfg(feature = "postgres")]
2702 crate::sql::Pool::Postgres(pg) => {
2703 let mut tx = pg.begin().await?;
2704 let mut deferred_fks: Vec<String> = Vec::new();
2705 for op in inverted {
2706 match op {
2707 Operation::Schema(change) => {
2708 let batch = super::diff::render_changes_split_with_dialect(
2709 std::slice::from_ref(change),
2710 snapshot,
2711 pool.dialect(),
2712 )
2713 .map_err(MigrateError::Validation)?;
2714 for stmt in batch.immediate {
2715 sqlx::query(&stmt).execute(&mut *tx).await?;
2716 }
2717 deferred_fks.extend(batch.deferred_fks);
2718 }
2719 Operation::Data(d) => {
2720 sqlx::query(&d.sql).execute(&mut *tx).await?;
2721 }
2722 Operation::Callback(c) => {
2723 invoke_migration_callback(c, pool.clone().into()).await?;
2725 }
2726 }
2727 }
2728 for stmt in deferred_fks {
2729 sqlx::query(&stmt).execute(&mut *tx).await?;
2730 }
2731 sqlx::query(&format!("DELETE FROM {ledger} WHERE name = $1"))
2732 .bind(&target.name)
2733 .execute(&mut *tx)
2734 .await?;
2735 tx.commit().await?;
2736 }
2737 #[cfg(feature = "mysql")]
2738 crate::sql::Pool::Mysql(my) => {
2739 tracing::warn!(
2744 migration = %target.name,
2745 "MySQL silently auto-commits on every DDL statement; the atomic-unapply \
2746 wrapper only protects RunSQL/RunPython between DDL ops. A failure mid-unapply \
2747 leaves the schema half-reverted and requires manual recovery."
2748 );
2749 let mut tx = my.begin().await?;
2750 let mut deferred_fks: Vec<String> = Vec::new();
2751 for op in inverted {
2752 match op {
2753 Operation::Schema(change) => {
2754 let batch = super::diff::render_changes_split_with_dialect(
2755 std::slice::from_ref(change),
2756 snapshot,
2757 pool.dialect(),
2758 )
2759 .map_err(MigrateError::Validation)?;
2760 for stmt in batch.immediate {
2761 sqlx::query(&stmt).execute(&mut *tx).await?;
2762 }
2763 deferred_fks.extend(batch.deferred_fks);
2764 }
2765 Operation::Data(d) => {
2766 sqlx::query(&d.sql).execute(&mut *tx).await?;
2767 }
2768 Operation::Callback(c) => {
2769 invoke_migration_callback(c, pool.clone().into()).await?;
2771 }
2772 }
2773 }
2774 for stmt in deferred_fks {
2775 sqlx::query(&stmt).execute(&mut *tx).await?;
2776 }
2777 sqlx::query(&format!("DELETE FROM {ledger} WHERE name = ?"))
2778 .bind(&target.name)
2779 .execute(&mut *tx)
2780 .await?;
2781 tx.commit().await?;
2782 }
2783 #[cfg(feature = "sqlite")]
2784 crate::sql::Pool::Sqlite(sq) => {
2785 let mut tx = sq.begin().await?;
2786 let mut deferred_fks: Vec<String> = Vec::new();
2787 for op in inverted {
2788 match op {
2789 Operation::Schema(change) => {
2790 let batch = super::diff::render_changes_split_with_dialect(
2791 std::slice::from_ref(change),
2792 snapshot,
2793 pool.dialect(),
2794 )
2795 .map_err(MigrateError::Validation)?;
2796 for stmt in batch.immediate {
2797 sqlx::query(&stmt).execute(&mut *tx).await?;
2798 }
2799 deferred_fks.extend(batch.deferred_fks);
2800 }
2801 Operation::Data(d) => {
2802 sqlx::query(&d.sql).execute(&mut *tx).await?;
2803 }
2804 Operation::Callback(c) => {
2805 invoke_migration_callback(c, pool.clone().into()).await?;
2807 }
2808 }
2809 }
2810 for stmt in deferred_fks {
2811 sqlx::query(&stmt).execute(&mut *tx).await?;
2812 }
2813 sqlx::query(&format!("DELETE FROM {ledger} WHERE name = ?"))
2814 .bind(&target.name)
2815 .execute(&mut *tx)
2816 .await?;
2817 tx.commit().await?;
2818 }
2819 }
2820 Ok(())
2821}
2822
2823pub async fn migrate_embedded_pool(
2840 pool: &crate::sql::Pool,
2841 embedded: &[(&str, &str)],
2842) -> Result<Vec<Migration>, MigrateError> {
2843 migrate_embedded_pool_with_ledger(pool, embedded, LEDGER_TABLE).await
2844}
2845
2846async fn migrate_embedded_pool_with_ledger(
2847 pool: &crate::sql::Pool,
2848 embedded: &[(&str, &str)],
2849 ledger: &str,
2850) -> Result<Vec<Migration>, MigrateError> {
2851 ensure_ledger_pool_with_ledger(pool, ledger).await?;
2852 with_migrate_lock_pool(pool, async {
2853 let mut all: Vec<Migration> = Vec::with_capacity(embedded.len());
2854 for (name, json) in embedded {
2855 let mig = file::parse(json)?;
2856 if mig.name != *name {
2857 return Err(MigrateError::Validation(format!(
2858 "embedded entry key `{name}` doesn't match migration `name` field `{}`",
2859 mig.name,
2860 )));
2861 }
2862 all.push(mig);
2863 }
2864 all.sort_by(|a, b| a.name.cmp(&b.name));
2865 file::validate_chain(&all, "embedded slice")?;
2866
2867 let applied = applied_set_pool_with_ledger(pool, ledger).await?;
2868 let pending: Vec<Migration> = all
2869 .into_iter()
2870 .filter(|m| !applied.contains(&m.name))
2871 .collect();
2872
2873 let mut newly = Vec::with_capacity(pending.len());
2874 for mig in pending {
2875 apply_one_pool(pool, &mig, ledger).await?;
2876 newly.push(mig);
2877 }
2878 Ok(newly)
2879 })
2880 .await
2881}
2882
2883async fn unapply_nonatomic_pool(
2884 pool: &crate::sql::Pool,
2885 target: &Migration,
2886 inverted: &[Operation],
2887 snapshot: &SchemaSnapshot,
2888 ledger: &str,
2889) -> Result<(), MigrateError> {
2890 tracing::info!(migration = %target.name, "unapplying (non-atomic, _pool)");
2891 let mut deferred_fks: Vec<String> = Vec::new();
2892 for op in inverted {
2893 match op {
2894 Operation::Schema(change) => {
2895 let batch = super::diff::render_changes_split_with_dialect(
2896 std::slice::from_ref(change),
2897 snapshot,
2898 pool.dialect(),
2899 )
2900 .map_err(MigrateError::Validation)?;
2901 for stmt in batch.immediate {
2902 crate::sql::raw_execute_pool(pool, &stmt, ::std::vec::Vec::new()).await?;
2903 }
2904 deferred_fks.extend(batch.deferred_fks);
2905 }
2906 Operation::Data(d) => {
2907 crate::sql::raw_execute_pool(pool, &d.sql, ::std::vec::Vec::new()).await?;
2908 }
2909 Operation::Callback(c) => {
2910 invoke_migration_callback(c, pool.clone()).await?;
2912 }
2913 }
2914 }
2915 for stmt in deferred_fks {
2916 crate::sql::raw_execute_pool(pool, &stmt, ::std::vec::Vec::new()).await?;
2917 }
2918 let placeholder = pool.dialect().placeholder(1);
2919 let delete_sql = format!("DELETE FROM {ledger} WHERE name = {placeholder}");
2920 crate::sql::raw_execute_pool(
2921 pool,
2922 &delete_sql,
2923 ::std::vec![crate::core::SqlValue::String(target.name.clone())],
2924 )
2925 .await?;
2926 Ok(())
2927}