Skip to main content

sea_orm_cli/commands/
migrate.rs

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            // Construct the `--manifest-path`
44            let manifest_path = if migration_dir.ends_with('/') {
45                format!("{migration_dir}Cargo.toml")
46            } else {
47                format!("{migration_dir}/Cargo.toml")
48            };
49            // Construct the arguments that will be supplied to `cargo` command
50            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            // Run migrator CLI on user's behalf
70            println!("Running `cargo {}`", args.join(" "));
71            let exit_status = Command::new("cargo").args(args).envs(envs).status()?; // Get the status code
72            if !exit_status.success() {
73                // Propagate the error if any
74                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    // Make sure the migration name doesn't contain any characters that
135    // are invalid module names in Rust.
136    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    // build new migration filename
145    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
161/// `get_full_migration_dir` looks for a `src` directory
162/// inside of `migration_dir` and appends that to the returned path if found.
163///
164/// Otherwise, `migration_dir` can point directly to a directory containing the
165/// migrations. In that case, nothing is appended.
166///
167/// This way, `src` doesn't need to be appended in the standard case where
168/// migrations are in their own crate. If the migrations are in a submodule
169/// of another crate, `migration_dir` can point directly to that module.
170fn 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
218/// `get_migrator_filepath` looks for a file `migration_dir/src/lib.rs`
219/// and returns that path if found.
220///
221/// If `src` is not found, it will look directly in `migration_dir` for `lib.rs`.
222///
223/// If `lib.rs` is not found, it will look for `mod.rs` instead,
224/// e.g. `migration_dir/mod.rs`.
225///
226/// This way, `src` doesn't need to be appended in the standard case where
227/// migrations are in their own crate (with a file `lib.rs`). If the
228/// migrations are in a submodule of another crate (with a file `mod.rs`),
229/// `migration_dir` can point directly to that module.
230fn 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    // create a backup of the migrator file in case something goes wrong
250    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    // find existing mod declarations, add new line
255    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    // build new vector from declared migration modules
265    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}