sz_rust_cli/cmd/
migrate.rs1use std::path::{Path, PathBuf};
18
19use clap::Args;
20
21use crate::error::CliError;
22
23#[derive(Args, Debug)]
25pub struct MigrateArgs {
26 #[arg(long)]
28 pub rollback: bool,
29
30 #[arg(short = 'p', long, default_value = "migrations")]
32 pub path: String,
33}
34
35pub fn execute_migrate(args: &MigrateArgs) -> Result<(), CliError> {
40 let path = PathBuf::from(&args.path);
41
42 if !path.exists() {
43 return Err(CliError::Migration(format!(
44 "Migration directory not found: {}",
45 path.display()
46 )));
47 }
48
49 if args.rollback {
50 println!("Rolling back last batch in: {}", path.display());
51 let migrations = list_migrations(&path)?;
53 if migrations.is_empty() {
54 println!("No migrations to rollback.");
55 return Ok(());
56 }
57 if let Some(last) = migrations.last() {
59 println!("Would rollback: {} ({})", last.0, last.1);
60 }
61 println!("Note: Actual rollback requires database connection.");
62 } else {
63 println!("Running migrations in: {}", path.display());
64 let migrations = list_migrations(&path)?;
65 if migrations.is_empty() {
66 println!("No migrations found.");
67 return Ok(());
68 }
69 for (version, name) in &migrations {
70 println!(" Would apply: {} ({})", version, name);
71 }
72 println!(
73 "Total: {} migration(s). Note: Actual execution requires database connection.",
74 migrations.len()
75 );
76 }
77
78 Ok(())
79}
80
81pub fn execute_status(path: &str) -> Result<(), CliError> {
85 let path_buf = PathBuf::from(path);
86
87 if !path_buf.exists() {
88 return Err(CliError::Migration(format!(
89 "Migration directory not found: {}",
90 path_buf.display()
91 )));
92 }
93
94 let migrations = list_migrations(&path_buf)?;
95
96 if migrations.is_empty() {
97 println!("No migrations found in: {}", path_buf.display());
98 return Ok(());
99 }
100
101 println!(
103 "{:<15} {:<30} {:<20}",
104 "Version", "Migration Name", "Status"
105 );
106 println!("{}", "-".repeat(65));
107
108 for (version, name) in &migrations {
109 println!("{:<15} {:<30} {:<20}", version, name, "Pending*");
111 }
112
113 println!();
114 println!("* Status cannot be determined without database connection (offline mode).");
115
116 Ok(())
117}
118
119fn list_migrations(path: &Path) -> Result<Vec<(String, String)>, CliError> {
124 let mut versions: std::collections::BTreeMap<String, String> =
125 std::collections::BTreeMap::new();
126
127 let entries = std::fs::read_dir(path)?;
128 for entry in entries {
129 let entry = entry?;
130 let file_path = entry.path();
131 let filename = match file_path.file_stem().and_then(|s| s.to_str()) {
132 Some(name) => name.to_string(),
133 None => continue,
134 };
135
136 let base = if let Some(rest) = filename.strip_suffix("_up") {
138 rest
139 } else if let Some(rest) = filename.strip_suffix("_down") {
140 rest
141 } else {
142 &filename
143 };
144
145 if let Some(underscore_pos) = base.find('_') {
146 let version = base[..underscore_pos].to_string();
147 let name = base[underscore_pos + 1..].to_string();
148 versions.entry(version).or_insert(name);
149 } else {
150 versions
151 .entry(base.to_string())
152 .or_insert_with(|| base.to_string());
153 }
154 }
155
156 Ok(versions.into_iter().collect())
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::fs;
163 use std::io::Write;
164
165 fn create_test_migration(dir: &Path, version: &str, name: &str) {
166 let up_name = format!("{}_{}_up.sql", version, name);
167 let down_name = format!("{}_{}_down.sql", version, name);
168
169 let up_path = dir.join(up_name);
170 let down_path = dir.join(down_name);
171
172 let mut up_file = fs::File::create(&up_path).unwrap();
173 writeln!(up_file, "-- {} up", name).unwrap();
174
175 let mut down_file = fs::File::create(&down_path).unwrap();
176 writeln!(down_file, "-- {} down", name).unwrap();
177 }
178
179 #[test]
180 fn test_list_migrations_empty() {
181 let temp = tempfile::tempdir().unwrap();
182 let path = temp.path().to_path_buf();
183 let result = list_migrations(&path).unwrap();
184 assert!(result.is_empty());
185 }
186
187 #[test]
188 fn test_list_migrations_with_files() {
189 let temp = tempfile::tempdir().unwrap();
190 let path = temp.path().to_path_buf();
191
192 create_test_migration(&path, "001", "create_users");
193 create_test_migration(&path, "002", "add_index");
194
195 let result = list_migrations(&path).unwrap();
196 assert_eq!(result.len(), 2);
197 assert_eq!(result[0], ("001".to_string(), "create_users".to_string()));
198 assert_eq!(result[1], ("002".to_string(), "add_index".to_string()));
199 }
200
201 #[test]
202 fn test_execute_status_nonexistent_dir() {
203 let result = execute_status("/nonexistent/path/migrations");
204 assert!(matches!(result, Err(CliError::Migration(_))));
205 }
206
207 #[test]
208 fn test_execute_status_empty_dir() {
209 let temp = tempfile::tempdir().unwrap();
210 let path = temp.path().to_str().unwrap();
211 let result = execute_status(path);
212 assert!(result.is_ok());
213 }
214
215 #[test]
216 fn test_execute_status_with_migrations() {
217 let temp = tempfile::tempdir().unwrap();
218 let path = temp.path().to_path_buf();
219 create_test_migration(&path, "001", "create_users");
220
221 let path_str = temp.path().to_str().unwrap();
222 let result = execute_status(path_str);
223 assert!(result.is_ok());
224 }
225
226 #[test]
227 fn test_execute_migrate_nonexistent_dir() {
228 let args = MigrateArgs {
229 rollback: false,
230 path: "/nonexistent/migrations".to_string(),
231 };
232 let result = execute_migrate(&args);
233 assert!(matches!(result, Err(CliError::Migration(_))));
234 }
235
236 #[test]
237 fn test_execute_migrate_empty_dir() {
238 let temp = tempfile::tempdir().unwrap();
239 let args = MigrateArgs {
240 rollback: false,
241 path: temp.path().to_str().unwrap().to_string(),
242 };
243 let result = execute_migrate(&args);
244 assert!(result.is_ok());
245 }
246
247 #[test]
248 fn test_execute_migrate_with_files() {
249 let temp = tempfile::tempdir().unwrap();
250 let path = temp.path().to_path_buf();
251 create_test_migration(&path, "001", "create_users");
252
253 let args = MigrateArgs {
254 rollback: false,
255 path: temp.path().to_str().unwrap().to_string(),
256 };
257 let result = execute_migrate(&args);
258 assert!(result.is_ok());
259 }
260
261 #[test]
262 fn test_execute_migrate_rollback() {
263 let temp = tempfile::tempdir().unwrap();
264 let path = temp.path().to_path_buf();
265 create_test_migration(&path, "001", "create_users");
266 create_test_migration(&path, "002", "add_index");
267
268 let args = MigrateArgs {
269 rollback: true,
270 path: temp.path().to_str().unwrap().to_string(),
271 };
272 let result = execute_migrate(&args);
273 assert!(result.is_ok());
274 }
275}