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
184 let migration_template = fmt_migration_template(migration_name);
185 let mut migration_file = fs::File::create(migration_filepath)?;
186 migration_file.write_all(migration_template.as_bytes())?;
187 Ok(())
188}
189
190fn fmt_migration_template(migration_name: &str) -> String {
191 format! {
192 r#"use sea_orm_migration::{{prelude::*, schema::*}};
193
194pub struct Migration;
195
196impl MigrationName for Migration {{
197 fn name(&self) -> &str {{
198 "{migration_name}"
199 }}
200}}
201
202#[async_trait::async_trait]
203impl MigrationTrait for Migration {{
204 async fn up(&self, _manager: &SchemaManager) -> Result<(), DbErr> {{
205 // Replace the sample below with your own migration scripts
206 todo!();
207 }}
208
209 async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {{
210 // Replace the sample below with your own migration scripts
211 todo!();
212 }}
213}}
214"#
215 }
216}
217
218fn get_migrator_filepath(migration_dir: &str) -> PathBuf {
231 let full_migration_dir = get_full_migration_dir(migration_dir);
232 let with_lib = full_migration_dir.join("lib.rs");
233 match () {
234 _ if with_lib.is_file() => with_lib,
235 _ => full_migration_dir.join("mod.rs"),
236 }
237}
238
239fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
240 let migrator_filepath = get_migrator_filepath(migration_dir);
241 println!(
242 "Adding migration `{}` to `{}`",
243 migration_name,
244 migrator_filepath.display()
245 );
246 let migrator_content = fs::read_to_string(&migrator_filepath)?;
247 let mut updated_migrator_content = migrator_content.clone();
248
249 let migrator_backup_filepath = migrator_filepath.with_extension("rs.bak");
251 fs::copy(&migrator_filepath, &migrator_backup_filepath)?;
252 let mut migrator_file = fs::File::create(&migrator_filepath)?;
253
254 let mod_regex = Regex::new(r"mod\s+(?P<name>m\d{8}_\d{6}_\w+);")?;
256 let mods: Vec<_> = mod_regex.captures_iter(&migrator_content).collect();
257 let mods_end = if let Some(last_match) = mods.last() {
258 last_match.get(0).unwrap().end() + 1
259 } else {
260 migrator_content.len()
261 };
262 updated_migrator_content.insert_str(mods_end, format!("mod {migration_name};\n").as_str());
263
264 let mut migrations: Vec<&str> = mods
266 .iter()
267 .map(|cap| cap.name("name").unwrap().as_str())
268 .collect();
269 migrations.push(migration_name);
270 let mut boxed_migrations = migrations
271 .iter()
272 .map(|migration| format!(" Box::new({migration}::Migration),"))
273 .collect::<Vec<String>>()
274 .join("\n");
275 boxed_migrations.push('\n');
276 let boxed_migrations = format!("vec![\n{boxed_migrations} ]\n");
277 let vec_regex = Regex::new(r"vec!\[[\s\S]+\]\n")?;
278 let updated_migrator_content = vec_regex.replace(&updated_migrator_content, &boxed_migrations);
279
280 migrator_file.write_all(updated_migrator_content.as_bytes())?;
281 fs::remove_file(&migrator_backup_filepath)?;
282 Ok(())
283}
284
285#[derive(Debug)]
286enum MigrationCommandError {
287 InvalidName(String),
288}
289
290impl Display for MigrationCommandError {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 match self {
293 MigrationCommandError::InvalidName(name) => {
294 write!(f, "Invalid migration name: {name}")
295 }
296 }
297 }
298}
299
300impl Error for MigrationCommandError {}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 const EXPECTED_TEMPLATE: &str = r#"use sea_orm_migration::{prelude::*, schema::*};
307
308pub struct Migration;
309
310impl MigrationName for Migration {
311 fn name(&self) -> &str {
312 "test_name"
313 }
314}
315
316#[async_trait::async_trait]
317impl MigrationTrait for Migration {
318 async fn up(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
319 // Replace the sample below with your own migration scripts
320 todo!();
321 }
322
323 async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
324 // Replace the sample below with your own migration scripts
325 todo!();
326 }
327}
328"#;
329
330 #[test]
331 fn test_create_new_migration() {
332 let migration_name = "test_name";
333 let migration_dir = "/tmp/sea_orm_cli_test_new_migration/";
334 fs::create_dir_all(format!("{migration_dir}src")).unwrap();
335 create_new_migration(migration_name, migration_dir).unwrap();
336 let migration_filepath = Path::new(migration_dir)
337 .join("src")
338 .join(format!("{migration_name}.rs"));
339 assert!(migration_filepath.exists());
340 let migration_content = fs::read_to_string(migration_filepath).unwrap();
341 assert_eq!(&migration_content, EXPECTED_TEMPLATE);
342 fs::remove_dir_all("/tmp/sea_orm_cli_test_new_migration/").unwrap();
343 }
344
345 #[test]
346 fn test_update_migrator() {
347 let migration_name = "test_name";
348 let migration_dir = "/tmp/sea_orm_cli_test_update_migrator/";
349 fs::create_dir_all(format!("{migration_dir}src")).unwrap();
350 let migrator_filepath = Path::new(migration_dir).join("src").join("lib.rs");
351 fs::copy("./template/migration/src/lib.rs", &migrator_filepath).unwrap();
352 update_migrator(migration_name, migration_dir).unwrap();
353 assert!(&migrator_filepath.exists());
354 let migrator_content = fs::read_to_string(&migrator_filepath).unwrap();
355 let mod_regex = Regex::new(r"mod (?P<name>\w+);").unwrap();
356 let migrations: Vec<&str> = mod_regex
357 .captures_iter(&migrator_content)
358 .map(|cap| cap.name("name").unwrap().as_str())
359 .collect();
360 assert_eq!(migrations.len(), 2);
361 assert_eq!(
362 *migrations.first().unwrap(),
363 "m20220101_000001_create_table"
364 );
365 assert_eq!(migrations.last().unwrap(), &migration_name);
366 let boxed_regex = Regex::new(r"Box::new\((?P<name>\S+)::Migration\)").unwrap();
367 let migrations: Vec<&str> = boxed_regex
368 .captures_iter(&migrator_content)
369 .map(|cap| cap.name("name").unwrap().as_str())
370 .collect();
371 assert_eq!(migrations.len(), 2);
372 assert_eq!(
373 *migrations.first().unwrap(),
374 "m20220101_000001_create_table"
375 );
376 assert_eq!(migrations.last().unwrap(), &migration_name);
377 fs::remove_dir_all("/tmp/sea_orm_cli_test_update_migrator/").unwrap();
378 }
379}