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