1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::Serialize;
7
8#[cfg(feature = "postgres")]
9use tokio_postgres::Client;
10
11use crate::config::WaypointConfig;
12use crate::db::DbClient;
13use crate::error::Result;
14use crate::history::{self, AppliedMigration};
15use crate::migration::{MigrationKind, MigrationVersion, ResolvedMigration, scan_migrations};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub enum MigrationState {
20 Pending,
22 Applied,
24 Failed,
26 Missing,
28 Outdated,
30 OutOfOrder,
32 BelowBaseline,
34 Ignored,
36 Baseline,
38 Undone,
40}
41
42impl std::fmt::Display for MigrationState {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 MigrationState::Pending => write!(f, "Pending"),
46 MigrationState::Applied => write!(f, "Applied"),
47 MigrationState::Failed => write!(f, "Failed"),
48 MigrationState::Missing => write!(f, "Missing"),
49 MigrationState::Outdated => write!(f, "Outdated"),
50 MigrationState::OutOfOrder => write!(f, "Out of Order"),
51 MigrationState::BelowBaseline => write!(f, "Below Baseline"),
52 MigrationState::Ignored => write!(f, "Ignored"),
53 MigrationState::Baseline => write!(f, "Baseline"),
54 MigrationState::Undone => write!(f, "Undone"),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize)]
61pub struct MigrationInfo {
62 pub version: Option<String>,
64 pub description: String,
66 pub migration_type: String,
68 pub script: String,
70 pub state: MigrationState,
72 pub installed_on: Option<DateTime<Utc>>,
74 pub execution_time: Option<i32>,
76 pub checksum: Option<i32>,
78}
79
80#[cfg(feature = "postgres")]
82pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<Vec<MigrationInfo>> {
83 let schema = &config.migrations.schema;
84 let table = &config.migrations.table;
85
86 if !history::history_table_exists(client, schema, table).await? {
87 let resolved = scan_migrations(&config.migrations.locations)?;
88 return Ok(pending_only(resolved));
89 }
90 let applied = history::get_applied_migrations(client, schema, table).await?;
91 let resolved = scan_migrations(&config.migrations.locations)?;
92 Ok(merge(applied, resolved))
93}
94
95pub async fn execute_db(client: &DbClient, config: &WaypointConfig) -> Result<Vec<MigrationInfo>> {
97 let schema = client.resolve_schema(&config.migrations.schema).await?;
98 let schema = schema.as_str();
99 let table = &config.migrations.table;
100
101 if !history::history_table_exists_db(client, schema, table).await? {
102 let resolved = scan_migrations(&config.migrations.locations)?;
103 return Ok(pending_only(resolved));
104 }
105 let applied = history::get_applied_migrations_db(client, schema, table).await?;
106 let resolved = scan_migrations(&config.migrations.locations)?;
107 Ok(merge(applied, resolved))
108}
109
110fn pending_only(resolved: Vec<ResolvedMigration>) -> Vec<MigrationInfo> {
112 resolved
113 .into_iter()
114 .filter(|m| !m.is_undo())
115 .map(|m| {
116 let version = m.version().map(|v| v.raw.clone());
117 let migration_type = m.migration_type().to_string();
118 MigrationInfo {
119 version,
120 description: m.description,
121 migration_type,
122 script: m.script,
123 state: MigrationState::Pending,
124 installed_on: None,
125 execution_time: None,
126 checksum: Some(m.checksum),
127 }
128 })
129 .collect()
130}
131
132fn merge(applied: Vec<AppliedMigration>, resolved: Vec<ResolvedMigration>) -> Vec<MigrationInfo> {
134 let effective = history::effective_applied_versions(&applied);
135
136 let resolved_by_version: HashMap<String, &ResolvedMigration> = resolved
137 .iter()
138 .filter(|m| m.is_versioned())
139 .filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
140 .collect();
141
142 let resolved_by_script: HashMap<String, &ResolvedMigration> = resolved
143 .iter()
144 .filter(|m| !m.is_versioned() && !m.is_undo())
145 .map(|m| (m.script.clone(), m))
146 .collect();
147
148 let baseline_version = applied
149 .iter()
150 .find(|a| a.migration_type == "BASELINE")
151 .and_then(|a| a.version.as_ref())
152 .and_then(|v| MigrationVersion::parse(v).ok());
153
154 let highest_applied = effective
155 .iter()
156 .filter_map(|v| MigrationVersion::parse(v).ok())
157 .max();
158
159 let mut infos: Vec<MigrationInfo> = Vec::new();
160 let mut seen_versions: HashMap<String, bool> = HashMap::new();
161 let mut seen_scripts: HashMap<String, bool> = HashMap::new();
162
163 for am in &applied {
164 let is_versioned = am.version.is_some();
165 let is_repeatable = am.version.is_none() && am.migration_type != "BASELINE";
166
167 let state = if am.migration_type == "BASELINE" {
168 MigrationState::Baseline
169 } else if am.migration_type == "UNDO_SQL" {
170 MigrationState::Undone
171 } else if !am.success {
172 MigrationState::Failed
173 } else if is_versioned {
174 if let Some(ref version) = am.version {
175 if !effective.contains(version) {
176 MigrationState::Undone
177 } else if resolved_by_version.contains_key(version) {
178 MigrationState::Applied
179 } else {
180 MigrationState::Missing
181 }
182 } else {
183 MigrationState::Applied
184 }
185 } else if is_repeatable {
186 if let Some(resolved) = resolved_by_script.get(&am.script) {
187 if Some(resolved.checksum) != am.checksum {
188 MigrationState::Outdated
189 } else {
190 MigrationState::Applied
191 }
192 } else {
193 MigrationState::Missing
194 }
195 } else {
196 MigrationState::Applied
197 };
198
199 if let Some(ref v) = am.version {
200 seen_versions.insert(v.clone(), true);
201 }
202 if am.version.is_none() {
203 seen_scripts.insert(am.script.clone(), true);
204 }
205
206 infos.push(MigrationInfo {
207 version: am.version.clone(),
208 description: am.description.clone(),
209 migration_type: am.migration_type.clone(),
210 script: am.script.clone(),
211 state,
212 installed_on: Some(am.installed_on),
213 execution_time: Some(am.execution_time),
214 checksum: am.checksum,
215 });
216 }
217
218 for m in &resolved {
219 if m.is_undo() {
220 continue;
221 }
222 match &m.kind {
223 MigrationKind::Versioned(version) => {
224 if seen_versions.contains_key(&version.raw) {
225 continue;
226 }
227 let state = if let Some(ref bv) = baseline_version {
228 if version <= bv {
229 MigrationState::BelowBaseline
230 } else if let Some(ref highest) = highest_applied {
231 if version < highest {
232 MigrationState::OutOfOrder
233 } else {
234 MigrationState::Pending
235 }
236 } else {
237 MigrationState::Pending
238 }
239 } else if let Some(ref highest) = highest_applied {
240 if version < highest {
241 MigrationState::OutOfOrder
242 } else {
243 MigrationState::Pending
244 }
245 } else {
246 MigrationState::Pending
247 };
248
249 infos.push(MigrationInfo {
250 version: Some(version.raw.clone()),
251 description: m.description.clone(),
252 migration_type: m.migration_type().to_string(),
253 script: m.script.clone(),
254 state,
255 installed_on: None,
256 execution_time: None,
257 checksum: Some(m.checksum),
258 });
259 }
260 MigrationKind::Repeatable => {
261 if seen_scripts.contains_key(&m.script) {
262 continue;
263 }
264 infos.push(MigrationInfo {
265 version: None,
266 description: m.description.clone(),
267 migration_type: m.migration_type().to_string(),
268 script: m.script.clone(),
269 state: MigrationState::Pending,
270 installed_on: None,
271 execution_time: None,
272 checksum: Some(m.checksum),
273 });
274 }
275 MigrationKind::Undo(_) => unreachable!("undo files are skipped above"),
276 }
277 }
278
279 infos.sort_by(|a, b| match (&a.version, &b.version) {
280 (Some(av), Some(bv)) => {
281 let pa = MigrationVersion::parse(av);
282 let pb = MigrationVersion::parse(bv);
283 match (pa, pb) {
284 (Ok(pa), Ok(pb)) => pa.cmp(&pb),
285 _ => av.cmp(bv),
286 }
287 }
288 (Some(_), None) => std::cmp::Ordering::Less,
289 (None, Some(_)) => std::cmp::Ordering::Greater,
290 (None, None) => a.description.cmp(&b.description),
291 });
292
293 infos
294}