lean_ctx/core/patterns/
flyway.rs1use crate::core::compressor::strip_ansi;
9
10pub fn compress(_cmd: &str, output: &str) -> Option<String> {
11 let trimmed = output.trim();
12 if trimmed.is_empty() {
13 return Some("flyway: ok".to_string());
14 }
15
16 let mut applied: Option<String> = None;
17 let mut migrating: Vec<String> = Vec::new();
18 let mut up_to_date = false;
19 let mut errors: Vec<String> = Vec::new();
20
21 for raw in trimmed.lines() {
22 let stripped = strip_ansi(raw);
23 let line = stripped.trim();
24 if line.is_empty() {
25 continue;
26 }
27
28 if line.contains("No migration necessary") || line.contains("is up to date") {
29 up_to_date = true;
30 } else if let Some(v) = extract_quoted_after(line, "to version ") {
31 migrating.push(format_version(&v));
32 } else if line.starts_with("Successfully applied") {
33 applied = Some(summarize_applied(line));
34 } else if is_error(line) {
35 errors.push(line.to_string());
36 }
37 }
38
39 if applied.is_none() && migrating.is_empty() && !up_to_date && errors.is_empty() {
40 return Some(fallback(trimmed));
41 }
42
43 let mut parts: Vec<String> = Vec::new();
44 if let Some(a) = applied {
45 parts.push(format!("flyway: {a}"));
46 } else if up_to_date {
47 parts.push("flyway: up to date".to_string());
48 } else if !errors.is_empty() {
49 parts.push("flyway: FAILED".to_string());
50 } else {
51 parts.push("flyway: migrating".to_string());
52 }
53 for m in &migrating {
54 parts.push(format!(" {m}"));
55 }
56 for e in errors.iter().take(5) {
57 parts.push(format!(" {e}"));
58 }
59 Some(parts.join("\n"))
60}
61
62fn extract_quoted_after(line: &str, marker: &str) -> Option<String> {
64 let after = line.split_once(marker)?.1;
65 let after = after.split_once('"')?.1;
66 let inner = after.split_once('"')?.0;
67 Some(inner.to_string())
68}
69
70fn format_version(v: &str) -> String {
72 match v.split_once(" - ") {
73 Some((ver, name)) => format!("v{} {}", ver.trim(), name.trim()),
74 None => format!("v{}", v.trim()),
75 }
76}
77
78fn summarize_applied(line: &str) -> String {
81 let count = line
82 .strip_prefix("Successfully applied ")
83 .and_then(|r| r.split_whitespace().next())
84 .unwrap_or("?");
85 let noun = if count == "1" {
86 "migration"
87 } else {
88 "migrations"
89 };
90 let version = extract_after(line, "now at version ")
91 .map(|v| {
92 v.split_whitespace()
93 .next()
94 .unwrap_or("")
95 .trim_end_matches([',', '.'])
96 .to_string()
97 })
98 .filter(|v| !v.is_empty());
99 match version {
100 Some(v) => format!("applied {count} {noun} -> {v}"),
101 None => format!("applied {count} {noun}"),
102 }
103}
104
105fn extract_after(line: &str, marker: &str) -> Option<String> {
106 line.split_once(marker).map(|(_, r)| r.to_string())
107}
108
109fn is_error(line: &str) -> bool {
110 line.starts_with("ERROR")
111 || line.contains("Migration") && (line.contains("failed") || line.contains("FAILED"))
112 || line.contains("SQL State")
113 || line.contains("FlywayException")
114}
115
116fn fallback(text: &str) -> String {
117 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
118 let n = lines.len().min(8);
119 let mut s = lines[..n].join("\n");
120 if lines.len() > n {
121 s.push_str(&format!("\n... (+{} lines)", lines.len() - n));
122 }
123 s
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 const MIGRATE: &str = "Flyway Community Edition 9.22.0 by Redgate\nDatabase: jdbc:postgresql://localhost:5432/mydb (PostgreSQL 15.2)\nSuccessfully validated 5 migrations (execution time 00:00.123s)\nCurrent version of schema \"public\": 4\nMigrating schema \"public\" to version \"5 - add orders\"\nSuccessfully applied 1 migration to schema \"public\", now at version v5 (execution time 00:00.456s)\n";
131
132 #[test]
133 fn keeps_applied_summary_and_version() {
134 let r = compress("flyway migrate", MIGRATE).unwrap();
135 assert!(r.contains("applied 1 migration -> v5"), "{r}");
136 assert!(r.contains("v5 add orders"), "{r}");
137 assert!(!r.contains("Redgate"), "drops banner: {r}");
138 assert!(!r.contains("jdbc:"), "drops jdbc url: {r}");
139 }
140
141 #[test]
142 fn detects_up_to_date() {
143 let out = "Flyway Community Edition 9.22.0 by Redgate\nSchema \"public\" is up to date. No migration necessary.\n";
144 let r = compress("flyway migrate", out).unwrap();
145 assert_eq!(r, "flyway: up to date");
146 }
147
148 #[test]
149 fn shorter_than_input() {
150 let r = compress("flyway migrate", MIGRATE).unwrap();
151 assert!(r.len() < MIGRATE.len());
152 }
153
154 #[test]
155 fn empty_is_ok() {
156 assert_eq!(compress("flyway migrate", "").unwrap(), "flyway: ok");
157 }
158}