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