spawn_db/commands/migration/
mod.rs1mod adopt;
2mod apply;
3mod build;
4mod new;
5mod pin;
6mod status;
7
8pub use adopt::AdoptMigration;
9pub use apply::ApplyMigration;
10pub use build::BuildMigration;
11pub use new::NewMigration;
12pub use pin::PinMigration;
13pub use status::MigrationStatus;
14
15pub const DEFAULT_NAMESPACE: &str = "default";
16
17use crate::config::Config;
18use crate::engine::{MigrationDbInfo, MigrationHistoryStatus};
19use crate::store::list_migration_fs_status;
20use anyhow::Result;
21use dialoguer::Confirm;
22use std::collections::{HashMap, HashSet};
23
24#[derive(Debug, Clone)]
26pub struct MigrationStatusRow {
27 pub migration_name: String,
28 pub exists_in_filesystem: bool,
29 pub is_pinned: bool,
30 pub exists_in_db: bool,
31 pub last_status: Option<MigrationHistoryStatus>,
32 pub last_activity: Option<String>,
33 pub checksum: Option<String>,
34}
35
36pub async fn get_combined_migration_status(
40 config: &Config,
41 namespace: Option<&str>,
42) -> Result<Vec<MigrationStatusRow>> {
43 let engine = config.new_engine().await?;
44
45 let fs_status = list_migration_fs_status(config.operator(), &config.pather(), None).await?;
47
48 let db_migrations_list = engine.get_migrations_from_db(namespace).await?;
50
51 let db_migrations: HashMap<String, MigrationDbInfo> = db_migrations_list
53 .into_iter()
54 .map(|info| (info.migration_name.clone(), info))
55 .collect();
56
57 let all_migration_names: HashSet<String> = fs_status
59 .keys()
60 .chain(db_migrations.keys())
61 .cloned()
62 .collect();
63
64 let mut results: Vec<MigrationStatusRow> = all_migration_names
65 .into_iter()
66 .map(|name| {
67 let fs = fs_status.get(&name);
68 let db_info = db_migrations.get(&name);
69
70 MigrationStatusRow {
71 migration_name: name.clone(),
72 exists_in_filesystem: fs.map_or(false, |s| s.has_up_sql),
73 is_pinned: fs.map_or(false, |s| s.has_lock_toml),
74 exists_in_db: db_info.is_some(),
75 last_status: db_info.and_then(|info| info.last_status),
76 last_activity: db_info.and_then(|info| info.last_activity.clone()),
77 checksum: db_info.and_then(|info| info.checksum.clone()),
78 }
79 })
80 .collect();
81
82 results.sort_by(|a, b| a.migration_name.cmp(&b.migration_name));
84
85 Ok(results)
86}
87
88pub async fn get_pending_and_confirm(
92 config: &Config,
93 action: &str,
94 yes: bool,
95) -> Result<Option<Vec<String>>> {
96 let status_rows = get_combined_migration_status(config, Some(DEFAULT_NAMESPACE)).await?;
97
98 let pending: Vec<String> = status_rows
99 .into_iter()
100 .filter(|row| row.last_status.is_none() && row.exists_in_filesystem)
101 .map(|row| row.migration_name)
102 .collect();
103
104 if pending.is_empty() {
105 println!("No pending migrations to {}.", action);
106 return Ok(None);
107 }
108
109 let db_config = config.db_config()?;
110 let target = config.database.as_deref().unwrap_or("unknown");
111 let env = &db_config.environment;
112
113 println!();
114 println!("TARGET: {}", target);
115 if env.starts_with("prod") {
116 println!("ENVIRONMENT: {} \u{26a0}\u{fe0f}", env);
117 } else {
118 println!("ENVIRONMENT: {}", env);
119 }
120 println!();
121 println!(
122 "The following {} migration{} will be {}:",
123 pending.len(),
124 if pending.len() == 1 { "" } else { "s" },
125 if action == "apply" {
126 "applied"
127 } else {
128 "adopted"
129 },
130 );
131 for (i, name) in pending.iter().enumerate() {
132 println!(" {}. {}", i + 1, name);
133 }
134 println!();
135
136 if !yes {
137 let prompt = format!("Do you want to {} these migrations?", action);
138 let confirmed = Confirm::new()
139 .with_prompt(prompt)
140 .default(false)
141 .interact()?;
142
143 if !confirmed {
144 println!("Aborted.");
145 return Ok(None);
146 }
147 }
148
149 println!();
150 Ok(Some(pending))
151}