1use chrono::{Local, Utc};
2use regex::Regex;
3use std::{
4 error::Error,
5 fmt::Display,
6 fs,
7 io::Write,
8 path::{Path, PathBuf},
9 process::Command,
10};
11
12#[cfg(feature = "cli")]
13use crate::MigrateSubcommands;
14
15#[cfg(feature = "cli")]
16pub fn run_migrate_command(
17 command: Option<MigrateSubcommands>,
18 migration_dir: &str,
19 database_schema: Option<String>,
20 database_url: Option<String>,
21 verbose: bool,
22) -> Result<(), Box<dyn Error>> {
23 match command {
24 Some(MigrateSubcommands::Init) => run_migrate_init(migration_dir)?,
25 Some(MigrateSubcommands::Generate {
26 migration_name,
27 universal_time: _,
28 local_time,
29 }) => run_migrate_generate(migration_dir, &migration_name, !local_time)?,
30 _ => {
31 let (subcommand, migration_dir, steps, verbose) = match command {
32 Some(MigrateSubcommands::Fresh) => ("fresh", migration_dir, None, verbose),
33 Some(MigrateSubcommands::Refresh) => ("refresh", migration_dir, None, verbose),
34 Some(MigrateSubcommands::Reset) => ("reset", migration_dir, None, verbose),
35 Some(MigrateSubcommands::Status) => ("status", migration_dir, None, verbose),
36 Some(MigrateSubcommands::Up { num }) => ("up", migration_dir, num, verbose),
37 Some(MigrateSubcommands::Down { num }) => {
38 ("down", migration_dir, Some(num), verbose)
39 }
40 _ => ("up", migration_dir, None, verbose),
41 };
42
43 let manifest_path = if migration_dir.ends_with('/') {
45 format!("{migration_dir}Cargo.toml")
46 } else {
47 format!("{migration_dir}/Cargo.toml")
48 };
49 let mut args = vec!["run", "--manifest-path", &manifest_path, "--", subcommand];
51
52 let mut num: String = "".to_string();
53 if let Some(steps) = steps {
54 num = steps.to_string();
55 }
56 if !num.is_empty() {
57 args.extend(["-n", &num])
58 }
59 if let Some(database_url) = &database_url {
60 args.extend(["-u", database_url]);
61 }
62 if let Some(database_schema) = &database_schema {
63 args.extend(["-s", database_schema]);
64 }
65 if verbose {
66 args.push("-v");
67 }
68 println!("Running `cargo {}`", args.join(" "));
70 let exit_status = Command::new("cargo").args(args).status()?; if !exit_status.success() {
72 return Err("Fail to run migration".into());
74 }
75 }
76 }
77
78 Ok(())
79}
80
81pub fn run_migrate_init(migration_dir: &str) -> Result<(), Box<dyn Error>> {
82 let migration_dir = match migration_dir.ends_with('/') {
83 true => migration_dir.to_string(),
84 false => format!("{migration_dir}/"),
85 };
86 println!("Initializing migration directory...");
87 macro_rules! write_file {
88 ($filename: literal) => {
89 let fn_content = |content: String| content;
90 write_file!($filename, $filename, fn_content);
91 };
92 ($filename: literal, $template: literal) => {
93 let fn_content = |content: String| content;
94 write_file!($filename, $template, fn_content);
95 };
96 ($filename: literal, $template: literal, $fn_content: expr) => {
97 let filepath = [&migration_dir, $filename].join("");
98 println!("Creating file `{}`", filepath);
99 let path = Path::new(&filepath);
100 let prefix = path.parent().unwrap();
101 fs::create_dir_all(prefix).unwrap();
102 let mut file = fs::File::create(path)?;
103 let content = include_str!(concat!("../../template/migration/", $template));
104 let content = $fn_content(content.to_string());
105 file.write_all(content.as_bytes())?;
106 };
107 }
108 write_file!("src/lib.rs");
109 write_file!("src/m20220101_000001_create_table.rs");
110 write_file!("src/main.rs");
111 write_file!("Cargo.toml", "_Cargo.toml", |content: String| {
112 let ver = format!(
113 "{}.{}.0",
114 env!("CARGO_PKG_VERSION_MAJOR"),
115 env!("CARGO_PKG_VERSION_MINOR")
116 );
117 content.replace("<sea-orm-migration-version>", &ver)
118 });
119 write_file!("README.md");
120 if glob::glob(&format!("{migration_dir}**/.git"))?.count() > 0 {
121 write_file!(".gitignore", "_gitignore");
122 }
123 println!("Done!");
124
125 Ok(())
126}
127
128pub fn run_migrate_generate(
129 migration_dir: &str,
130 migration_name: &str,
131 universal_time: bool,
132) -> Result<(), Box<dyn Error>> {
133 if migration_name.contains('-') {
136 return Err(Box::new(MigrationCommandError::InvalidName(
137 "Hyphen `-` cannot be used in migration name".to_string(),
138 )));
139 }
140
141 println!("Generating new migration...");
142
143 const FMT: &str = "%Y%m%d_%H%M%S";
145 let formatted_now = if universal_time {
146 Utc::now().format(FMT)
147 } else {
148 Local::now().format(FMT)
149 };
150
151 let migration_name = migration_name.trim().replace(' ', "_");
152 let migration_name = format!("m{formatted_now}_{migration_name}");
153
154 create_new_migration(&migration_name, migration_dir)?;
155 update_migrator(&migration_name, migration_dir)?;
156
157 Ok(())
158}
159
160fn get_full_migration_dir(migration_dir: &str) -> PathBuf {
170 let without_src = Path::new(migration_dir).to_owned();
171 let with_src = without_src.join("src");
172 match () {
173 _ if with_src.is_dir() => with_src,
174 _ => without_src,
175 }
176}
177
178fn create_new_migration(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
179 let migration_filepath =
180 get_full_migration_dir(migration_dir).join(format!("{}.rs", &migration_name));
181 println!("Creating migration file `{}`", migration_filepath.display());
182 let migration_template =
184 include_str!("../../template/migration/src/m20220101_000001_create_table.rs");
185 let mut migration_file = fs::File::create(migration_filepath)?;
186 migration_file.write_all(migration_template.as_bytes())?;
187 Ok(())
188}
189
190fn get_migrator_filepath(migration_dir: &str) -> PathBuf {
203 let full_migration_dir = get_full_migration_dir(migration_dir);
204 let with_lib = full_migration_dir.join("lib.rs");
205 match () {
206 _ if with_lib.is_file() => with_lib,
207 _ => full_migration_dir.join("mod.rs"),
208 }
209}
210
211fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
212 let migrator_filepath = get_migrator_filepath(migration_dir);
213 println!(
214 "Adding migration `{}` to `{}`",
215 migration_name,
216 migrator_filepath.display()
217 );
218 let migrator_content = fs::read_to_string(&migrator_filepath)?;
219 let mut updated_migrator_content = migrator_content.clone();
220
221 let migrator_backup_filepath = migrator_filepath.with_extension("rs.bak");
223 fs::copy(&migrator_filepath, &migrator_backup_filepath)?;
224 let mut migrator_file = fs::File::create(&migrator_filepath)?;
225
226 let mod_regex = Regex::new(r"mod\s+(?P<name>m\d{8}_\d{6}_\w+);")?;
228 let mods: Vec<_> = mod_regex.captures_iter(&migrator_content).collect();
229 let mods_end = if let Some(last_match) = mods.last() {
230 last_match.get(0).unwrap().end() + 1
231 } else {
232 migrator_content.len()
233 };
234 updated_migrator_content.insert_str(mods_end, format!("mod {migration_name};\n").as_str());
235
236 let mut migrations: Vec<&str> = mods
238 .iter()
239 .map(|cap| cap.name("name").unwrap().as_str())
240 .collect();
241 migrations.push(migration_name);
242 let mut boxed_migrations = migrations
243 .iter()
244 .map(|migration| format!(" Box::new({migration}::Migration),"))
245 .collect::<Vec<String>>()
246 .join("\n");
247 boxed_migrations.push('\n');
248 let boxed_migrations = format!("vec![\n{boxed_migrations} ]\n");
249 let vec_regex = Regex::new(r"vec!\[[\s\S]+\]\n")?;
250 let updated_migrator_content = vec_regex.replace(&updated_migrator_content, &boxed_migrations);
251
252 migrator_file.write_all(updated_migrator_content.as_bytes())?;
253 fs::remove_file(&migrator_backup_filepath)?;
254 Ok(())
255}
256
257#[derive(Debug)]
258enum MigrationCommandError {
259 InvalidName(String),
260}
261
262impl Display for MigrationCommandError {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 MigrationCommandError::InvalidName(name) => {
266 write!(f, "Invalid migration name: {name}")
267 }
268 }
269 }
270}
271
272impl Error for MigrationCommandError {}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_create_new_migration() {
280 let migration_name = "test_name";
281 let migration_dir = "/tmp/sea_orm_cli_test_new_migration/";
282 fs::create_dir_all(format!("{migration_dir}src")).unwrap();
283 create_new_migration(migration_name, migration_dir).unwrap();
284 let migration_filepath = Path::new(migration_dir)
285 .join("src")
286 .join(format!("{migration_name}.rs"));
287 assert!(migration_filepath.exists());
288 let migration_content = fs::read_to_string(migration_filepath).unwrap();
289 assert_eq!(
290 &migration_content,
291 include_str!("../../template/migration/src/m20220101_000001_create_table.rs")
292 );
293 fs::remove_dir_all("/tmp/sea_orm_cli_test_new_migration/").unwrap();
294 }
295
296 #[test]
297 fn test_update_migrator() {
298 let migration_name = "test_name";
299 let migration_dir = "/tmp/sea_orm_cli_test_update_migrator/";
300 fs::create_dir_all(format!("{migration_dir}src")).unwrap();
301 let migrator_filepath = Path::new(migration_dir).join("src").join("lib.rs");
302 fs::copy("./template/migration/src/lib.rs", &migrator_filepath).unwrap();
303 update_migrator(migration_name, migration_dir).unwrap();
304 assert!(&migrator_filepath.exists());
305 let migrator_content = fs::read_to_string(&migrator_filepath).unwrap();
306 let mod_regex = Regex::new(r"mod (?P<name>\w+);").unwrap();
307 let migrations: Vec<&str> = mod_regex
308 .captures_iter(&migrator_content)
309 .map(|cap| cap.name("name").unwrap().as_str())
310 .collect();
311 assert_eq!(migrations.len(), 2);
312 assert_eq!(
313 *migrations.first().unwrap(),
314 "m20220101_000001_create_table"
315 );
316 assert_eq!(migrations.last().unwrap(), &migration_name);
317 let boxed_regex = Regex::new(r"Box::new\((?P<name>\S+)::Migration\)").unwrap();
318 let migrations: Vec<&str> = boxed_regex
319 .captures_iter(&migrator_content)
320 .map(|cap| cap.name("name").unwrap().as_str())
321 .collect();
322 assert_eq!(migrations.len(), 2);
323 assert_eq!(
324 *migrations.first().unwrap(),
325 "m20220101_000001_create_table"
326 );
327 assert_eq!(migrations.last().unwrap(), &migration_name);
328 fs::remove_dir_all("/tmp/sea_orm_cli_test_update_migrator/").unwrap();
329 }
330}