1use std::collections::HashMap;
5
6use serde::Serialize;
7
8#[cfg(feature = "postgres")]
9use tokio_postgres::Client;
10
11use crate::config::WaypointConfig;
12#[cfg(feature = "postgres")]
13use crate::db;
14use crate::db::DbClient;
15use crate::dialect::DialectKind;
16use crate::error::{Result, WaypointError};
17use crate::history;
18use crate::migration::{MigrationVersion, ResolvedMigration, scan_migrations};
19use crate::placeholder::{build_placeholders, replace_placeholders};
20
21#[derive(Debug, Clone)]
28pub enum UndoTarget {
29 Last,
31 Version(MigrationVersion),
33 Count(usize),
35}
36
37#[derive(Debug, Serialize)]
39pub struct UndoReport {
40 pub migrations_undone: usize,
42 pub total_time_ms: i32,
44 pub details: Vec<UndoDetail>,
46}
47
48#[derive(Debug, Serialize)]
50pub struct UndoDetail {
51 pub version: String,
53 pub description: String,
55 pub script: String,
57 pub execution_time_ms: i32,
59 pub auto_reversal: bool,
61}
62
63#[cfg(feature = "postgres")]
69#[allow(clippy::too_many_arguments)]
70async fn execute_undo_sql(
71 client: &Client,
72 schema: &str,
73 table: &str,
74 version: &str,
75 description: &str,
76 script: &str,
77 checksum: Option<i32>,
78 installed_by: &str,
79 sql: &str,
80) -> Result<i32> {
81 let start = std::time::Instant::now();
82 client.batch_execute("BEGIN").await?;
83
84 match client.batch_execute(sql).await {
85 Ok(()) => {
86 let exec_time = start.elapsed().as_millis() as i32;
87 match history::insert_applied_migration(
88 client,
89 schema,
90 table,
91 Some(version),
92 description,
93 "UNDO_SQL",
94 script,
95 checksum,
96 installed_by,
97 exec_time,
98 true,
99 )
100 .await
101 {
102 Ok(()) => {
103 client.batch_execute("COMMIT").await?;
104 Ok(exec_time)
105 }
106 Err(e) => {
107 if let Err(rb) = client.batch_execute("ROLLBACK").await {
108 log::error!("Failed to rollback undo transaction: {}", rb);
109 }
110 Err(e)
111 }
112 }
113 }
114 Err(e) => {
115 if let Err(rollback_err) = client.batch_execute("ROLLBACK").await {
116 log::error!("Failed to rollback undo transaction: {}", rollback_err);
117 }
118
119 if let Err(record_err) = history::insert_applied_migration(
121 client,
122 schema,
123 table,
124 Some(version),
125 description,
126 "UNDO_SQL",
127 script,
128 checksum,
129 installed_by,
130 0,
131 false,
132 )
133 .await
134 {
135 log::warn!(
136 "Failed to record undo failure; script={}, error={}",
137 script,
138 record_err
139 );
140 }
141
142 let reason = crate::error::format_db_error(&e);
143 Err(WaypointError::UndoFailed {
144 script: script.to_string(),
145 reason,
146 })
147 }
148 }
149}
150
151#[cfg(feature = "postgres")]
153pub async fn execute(
154 client: &Client,
155 config: &WaypointConfig,
156 target: UndoTarget,
157) -> Result<UndoReport> {
158 let schema = &config.migrations.schema;
159 let table = &config.migrations.table;
160
161 db::acquire_advisory_lock(client, schema, table).await?;
163
164 let result = run_undo(client, config, target).await;
165
166 if let Err(e) = db::release_advisory_lock(client, schema, table).await {
168 log::error!("Failed to release advisory lock: {}", e);
169 }
170
171 match &result {
172 Ok(report) => {
173 log::info!(
174 "Undo completed; migrations_undone={}, total_time_ms={}",
175 report.migrations_undone,
176 report.total_time_ms
177 );
178 }
179 Err(e) => {
180 log::error!("Undo failed: {}", e);
181 }
182 }
183
184 result
185}
186
187#[cfg(feature = "postgres")]
188async fn run_undo(
189 client: &Client,
190 config: &WaypointConfig,
191 target: UndoTarget,
192) -> Result<UndoReport> {
193 let schema = &config.migrations.schema;
194 let table = &config.migrations.table;
195
196 history::create_history_table(client, schema, table).await?;
198
199 let resolved = scan_migrations(&config.migrations.locations)?;
201 let undo_by_version: HashMap<String, &ResolvedMigration> = resolved
202 .iter()
203 .filter(|m| m.is_undo())
204 .filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
205 .collect();
206
207 let applied = history::get_applied_migrations(client, schema, table).await?;
209 let effective = history::effective_applied_versions(&applied);
210
211 let mut applied_versions: Vec<MigrationVersion> = effective
213 .iter()
214 .filter_map(|v| MigrationVersion::parse(v).ok())
215 .collect();
216 applied_versions.sort();
217 applied_versions.reverse(); let versions_to_undo: Vec<MigrationVersion> = match target {
221 UndoTarget::Last => applied_versions.into_iter().take(1).collect(),
222 UndoTarget::Count(n) => applied_versions.into_iter().take(n).collect(),
223 UndoTarget::Version(ref target_ver) => applied_versions
224 .into_iter()
225 .filter(|v| v > target_ver)
226 .collect(),
227 };
228
229 let db_user = db::get_current_user(client)
231 .await
232 .unwrap_or_else(|_| "unknown".to_string());
233 let db_name = db::get_current_database(client)
234 .await
235 .unwrap_or_else(|_| "unknown".to_string());
236 let installed_by = config
237 .migrations
238 .installed_by
239 .as_deref()
240 .unwrap_or(&db_user);
241
242 let mut report = UndoReport {
243 migrations_undone: 0,
244 total_time_ms: 0,
245 details: Vec::new(),
246 };
247
248 for version in &versions_to_undo {
250 if let Some(undo_migration) = undo_by_version.get(&version.raw) {
252 log::info!(
254 "Undoing migration (manual); migration={}, schema={}",
255 undo_migration.script,
256 schema
257 );
258
259 let placeholders = build_placeholders(
260 &config.placeholders,
261 schema,
262 &db_user,
263 &db_name,
264 &undo_migration.script,
265 );
266 let sql = replace_placeholders(&undo_migration.sql, &placeholders)?;
267
268 let exec_time = execute_undo_sql(
269 client,
270 schema,
271 table,
272 &version.raw,
273 &undo_migration.description,
274 &undo_migration.script,
275 Some(undo_migration.checksum),
276 installed_by,
277 &sql,
278 )
279 .await?;
280
281 report.migrations_undone += 1;
282 report.total_time_ms += exec_time;
283 report.details.push(UndoDetail {
284 version: version.raw.clone(),
285 description: undo_migration.description.clone(),
286 script: undo_migration.script.clone(),
287 execution_time_ms: exec_time,
288 auto_reversal: false,
289 });
290 } else if config.reversals.enabled {
291 match crate::reversal::get_reversal(client, schema, table, &version.raw).await? {
293 Some(reversal_sql) => {
294 let script = format!("auto-reversal:V{}", version.raw);
295 log::info!(
296 "Undoing migration (auto-reversal); version={}, schema={}",
297 version.raw,
298 schema
299 );
300
301 let exec_time = execute_undo_sql(
302 client,
303 schema,
304 table,
305 &version.raw,
306 "Auto-generated reversal",
307 &script,
308 None,
309 installed_by,
310 &reversal_sql,
311 )
312 .await?;
313
314 report.migrations_undone += 1;
315 report.total_time_ms += exec_time;
316 report.details.push(UndoDetail {
317 version: version.raw.clone(),
318 description: "Auto-generated reversal".to_string(),
319 script,
320 execution_time_ms: exec_time,
321 auto_reversal: true,
322 });
323 }
324 None => {
325 return Err(WaypointError::UndoMissing {
326 version: version.raw.clone(),
327 });
328 }
329 }
330 } else {
331 return Err(WaypointError::UndoMissing {
332 version: version.raw.clone(),
333 });
334 }
335 }
336
337 Ok(report)
338}
339
340pub async fn execute_db(
348 client: &DbClient,
349 config: &WaypointConfig,
350 target: UndoTarget,
351) -> Result<UndoReport> {
352 match client.dialect_kind() {
353 #[cfg(feature = "postgres")]
354 DialectKind::Postgres => execute(client.as_postgres()?, config, target).await,
355 #[cfg(not(feature = "postgres"))]
356 DialectKind::Postgres => Err(WaypointError::ConfigError(
357 "PostgreSQL support is not compiled in (enable the `postgres` feature)".into(),
358 )),
359 #[cfg(feature = "mysql")]
360 DialectKind::Mysql => execute_mysql(client, config, target).await,
361 #[cfg(not(feature = "mysql"))]
362 DialectKind::Mysql => Err(WaypointError::ConfigError(
363 "MySQL support is not compiled in (enable the `mysql` feature)".into(),
364 )),
365 }
366}
367
368#[cfg(feature = "mysql")]
369async fn execute_mysql(
370 client: &DbClient,
371 config: &WaypointConfig,
372 target: UndoTarget,
373) -> Result<UndoReport> {
374 let schema = client.resolve_schema(&config.migrations.schema).await?;
375 let table = &config.migrations.table;
376
377 client.acquire_lock(&schema, table).await?;
378
379 let result = run_undo_mysql(client, config, target).await;
380
381 if let Err(e) = client.release_lock(&schema, table).await {
382 log::error!("Failed to release advisory lock: {}", e);
383 }
384
385 match &result {
386 Ok(report) => {
387 log::info!(
388 "Undo completed (mysql); migrations_undone={}, total_time_ms={}",
389 report.migrations_undone,
390 report.total_time_ms
391 );
392 }
393 Err(e) => {
394 log::error!("Undo failed (mysql): {}", e);
395 }
396 }
397
398 result
399}
400
401#[cfg(feature = "mysql")]
402async fn run_undo_mysql(
403 client: &DbClient,
404 config: &WaypointConfig,
405 target: UndoTarget,
406) -> Result<UndoReport> {
407 let schema = client.resolve_schema(&config.migrations.schema).await?;
408 let schema = schema.as_str();
409 let table = &config.migrations.table;
410
411 history::create_history_table_db(client, schema, table).await?;
412
413 let resolved = scan_migrations(&config.migrations.locations)?;
414 let undo_by_version: HashMap<String, &ResolvedMigration> = resolved
415 .iter()
416 .filter(|m| m.is_undo())
417 .filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
418 .collect();
419
420 let applied = history::get_applied_migrations_db(client, schema, table).await?;
421 let effective = history::effective_applied_versions(&applied);
422
423 let mut applied_versions: Vec<MigrationVersion> = effective
424 .iter()
425 .filter_map(|v| MigrationVersion::parse(v).ok())
426 .collect();
427 applied_versions.sort();
428 applied_versions.reverse();
429
430 let versions_to_undo: Vec<MigrationVersion> = match target {
431 UndoTarget::Last => applied_versions.into_iter().take(1).collect(),
432 UndoTarget::Count(n) => applied_versions.into_iter().take(n).collect(),
433 UndoTarget::Version(ref target_ver) => applied_versions
434 .into_iter()
435 .filter(|v| v > target_ver)
436 .collect(),
437 };
438
439 let db_user = client
440 .current_user()
441 .await
442 .unwrap_or_else(|_| "unknown".into());
443 let db_name = client
444 .current_database()
445 .await
446 .unwrap_or_else(|_| "unknown".into());
447 let installed_by = config
448 .migrations
449 .installed_by
450 .as_deref()
451 .unwrap_or(&db_user)
452 .to_string();
453
454 let mut report = UndoReport {
455 migrations_undone: 0,
456 total_time_ms: 0,
457 details: Vec::new(),
458 };
459
460 for version in &versions_to_undo {
461 let (sql, script, description, checksum, auto_reversal) = match undo_by_version
462 .get(&version.raw)
463 {
464 Some(m) => {
465 let placeholders =
467 build_placeholders(&config.placeholders, schema, &db_user, &db_name, &m.script);
468 let sql = replace_placeholders(&m.sql, &placeholders)?;
469 log::info!(
470 "Undoing migration (manual); migration={}, schema={}",
471 m.script,
472 schema
473 );
474 (
475 sql,
476 m.script.clone(),
477 m.description.clone(),
478 Some(m.checksum),
479 false,
480 )
481 }
482 None if config.reversals.enabled => {
483 match crate::reversal::get_reversal_db(client, schema, table, &version.raw).await? {
485 Some(reversal_sql) => {
486 let script = format!("auto-reversal:V{}", version.raw);
487 log::info!(
488 "Undoing migration (auto-reversal); version={}, schema={}",
489 version.raw,
490 schema
491 );
492 (
493 reversal_sql,
494 script,
495 "Auto-generated reversal".to_string(),
496 None,
497 true,
498 )
499 }
500 None => {
501 return Err(WaypointError::UndoMissing {
502 version: version.raw.clone(),
503 });
504 }
505 }
506 }
507 None => {
508 return Err(WaypointError::UndoMissing {
509 version: version.raw.clone(),
510 });
511 }
512 };
513
514 let start = std::time::Instant::now();
515 let exec_result = client.execute_raw(&sql).await;
516 let exec_time = start.elapsed().as_millis() as i32;
517
518 match exec_result {
519 Ok(_) => {
520 history::insert_applied_migration_db(
521 client,
522 schema,
523 table,
524 Some(&version.raw),
525 &description,
526 "UNDO_SQL",
527 &script,
528 checksum,
529 &installed_by,
530 exec_time,
531 true,
532 )
533 .await?;
534
535 report.migrations_undone += 1;
536 report.total_time_ms += exec_time;
537 report.details.push(UndoDetail {
538 version: version.raw.clone(),
539 description: description.clone(),
540 script: script.clone(),
541 execution_time_ms: exec_time,
542 auto_reversal,
543 });
544 }
545 Err(e) => {
546 if let Err(record_err) = history::insert_applied_migration_db(
549 client,
550 schema,
551 table,
552 Some(&version.raw),
553 &description,
554 "UNDO_SQL",
555 &script,
556 checksum,
557 &installed_by,
558 exec_time,
559 false,
560 )
561 .await
562 {
563 log::warn!(
564 "Failed to record undo failure; script={}, error={}",
565 script,
566 record_err
567 );
568 }
569 return Err(WaypointError::UndoFailed {
570 script: script.clone(),
571 reason: e.to_string(),
572 });
573 }
574 }
575 }
576
577 Ok(report)
578}