use std::path::{Path, PathBuf};
use super::cargo::{ensure_workspace_deps, migrations_deps};
use super::support::{finish, resolve_start};
use crate::context::Context;
use crate::error::{CliError, CliResult};
use crate::naming::Names;
use crate::scaffold::{Renderer, Scaffold, ensure_decl};
use crate::templates::migration;
pub struct MigrationOptions {
pub name: String,
pub path: Option<PathBuf>,
pub dry_run: bool,
}
pub(crate) fn queue_db_crates(s: &mut Scaffold, root: &Path, mods: &[String]) {
s.create(
root.join("crates/migrations/Cargo.toml"),
migration::CRATE_CARGO.to_string(),
);
s.create(root.join("crates/migrations/src/lib.rs"), render_lib(mods));
s.create(
root.join("crates/migrations/src/migrator.rs"),
render_migrator(mods),
);
s.create(
root.join("crates/migrations/src/bin/migrate.rs"),
migration::CRATE_BIN.to_string(),
);
s.create(
root.join("crates/seed/Cargo.toml"),
migration::SEED_CARGO.to_string(),
);
s.create(
root.join("crates/seed/src/main.rs"),
migration::SEED_BIN.to_string(),
);
}
pub fn run(opts: MigrationOptions) -> CliResult<()> {
let ctx = Context::detect(&resolve_start(opts.path))?;
let ws = ctx.workspace.ok_or(CliError::NotNestrsWorkspace)?;
crate::naming::validate_feature_name(&opts.name).map_err(CliError::InvalidFeatureName)?;
let names = Names::parse(&opts.name);
let lib_path = ws.migrations_lib();
let bootstrapping = !lib_path.is_file();
let date = chrono::Utc::now().format("%Y%m%d").to_string();
let existing = if bootstrapping {
Vec::new()
} else {
read_migration_mods(&std::fs::read_to_string(&lib_path).map_err(CliError::Io)?)
};
let seq = next_seq(&existing, &date);
let stem = format!("m{date}_{seq:06}_{}", names.snake);
let mut mods = existing;
mods.push(stem.clone());
mods.sort();
mods.dedup();
let r = Renderer::new(&names);
let mut s = Scaffold::new();
if bootstrapping {
queue_db_crates(&mut s, &ws.root, &mods);
s.edit(
ws.root.join("Cargo.toml"),
ensure_workspace_deps(migrations_deps()),
);
}
s.create(
ws.migrations_root().join(format!("{stem}.rs")),
r.render(migration::MIGRATION),
);
if !bootstrapping {
s.edit(lib_path, ensure_decl(&format!("mod {stem};")));
let migrator = render_migrator(&mods);
s.edit(
ws.migrations_migrator(),
Box::new(move |_current: &str| Some(migrator.clone())),
);
}
finish(
s,
opts.dry_run,
&ws.root,
&format!("Created migration `{stem}`"),
)?;
print_next_steps(&stem);
Ok(())
}
fn read_migration_mods(lib: &str) -> Vec<String> {
lib.lines()
.filter_map(|line| {
let line = line.trim();
let rest = line.strip_prefix("mod ")?.strip_suffix(';')?;
(rest.starts_with('m') && rest != "migrator").then(|| rest.to_string())
})
.collect()
}
fn next_seq(existing: &[String], date: &str) -> u32 {
let prefix = format!("m{date}_");
existing
.iter()
.filter_map(|m| m.strip_prefix(&prefix))
.filter_map(|rest| rest.split('_').next())
.filter_map(|seq| seq.parse::<u32>().ok())
.max()
.map(|max| max + 1)
.unwrap_or(0)
}
fn render_lib(mods: &[String]) -> String {
let decls = mods
.iter()
.map(|m| format!("mod {m};\n"))
.collect::<String>();
format!(
"//! SeaORM migrations for this workspace.\n\
//!\n\
//! Add one with `nestrs g migration <name>`: it writes the file, the `mod` line\n\
//! here, and regenerates `migrator.rs` from that list — the two registrations\n\
//! cannot drift.\n\
\n\
{decls}mod migrator;\n\
\n\
pub use migrator::{{Migrator, migrate}};\n"
)
}
fn render_migrator(mods: &[String]) -> String {
let imports = if mods.is_empty() {
String::new()
} else {
let list = mods
.iter()
.map(|m| format!(" {m},"))
.collect::<Vec<_>>()
.join("\n");
format!("\nuse super::{{\n{list}\n}};\n")
};
let boxed = mods
.iter()
.map(|m| format!(" Box::new({m}::Migration),\n"))
.collect::<String>();
format!(
"use sea_orm_migration::prelude::*;\n\
{imports}\
\n\
pub struct Migrator;\n\
\n\
#[async_trait::async_trait]\n\
impl MigratorTrait for Migrator {{\n\
\x20 fn migrations() -> Vec<Box<dyn MigrationTrait>> {{\n\
\x20 vec![\n{boxed}\
\x20 ]\n\
\x20 }}\n\
}}\n\
\n\
pub async fn migrate(conn: &sea_orm::DatabaseConnection) -> anyhow::Result<()> {{\n\
\x20 Migrator::up(conn, None).await?;\n\
\x20 Ok(())\n\
}}\n"
)
}
fn print_next_steps(stem: &str) {
println!();
println!("Next steps:");
println!(" 1. Fill in `crates/migrations/src/{stem}.rs` (table + columns).");
println!(" 2. Apply it: nestrs run db up (or `db reset` to re-seed).");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_migration_mods_and_skips_migrator() {
let lib = "mod m20260101_000000_create_a;\nmod m20260102_000000_create_b;\nmod migrator;\n\npub use migrator::Migrator;";
assert_eq!(
read_migration_mods(lib),
vec!["m20260101_000000_create_a", "m20260102_000000_create_b"]
);
}
#[test]
fn next_seq_increments_within_the_day() {
let mods = vec![
"m20260718_000000_a".to_string(),
"m20260718_000001_b".to_string(),
"m20260717_000000_c".to_string(),
];
assert_eq!(next_seq(&mods, "20260718"), 2);
assert_eq!(next_seq(&mods, "20260719"), 0);
}
#[test]
fn regenerated_migrator_lists_every_module_once() {
let out = render_migrator(&["m1_a".to_string(), "m2_b".to_string()]);
assert!(out.contains("use super::{\n m1_a,\n m2_b,\n};"));
assert!(out.contains("Box::new(m1_a::Migration),"));
assert!(out.contains("Box::new(m2_b::Migration),"));
assert!(out.contains("pub async fn migrate"));
}
}