toolu_orm_cli/
generate.rs1use std::path::Path;
4
5use toolu_orm_core::dialect::Dialect;
6use toolu_orm_core::diff::diff;
7use toolu_orm_core::error::DbCoreError;
8use toolu_orm_core::journal::{compute_hash, Journal};
9use toolu_orm_core::schema::SchemaRegistry;
10use toolu_orm_core::snapshot::Snapshot;
11use toolu_orm_core::sql::generate_sql_for;
12
13const ZERO_SNAPSHOT_PARENT: &str = "00000000-0000-0000-0000-000000000000";
14
15pub fn run_generate(
21 registry: &SchemaRegistry,
22 migrations_dir: &str,
23 name: &str,
24 dialect: Dialect,
25) -> Result<Option<String>, DbCoreError> {
26 let journal_path = Path::new(migrations_dir).join("_journal.json");
27 let journal_path_str = journal_path.to_str().ok_or_else(|| {
28 DbCoreError::JournalRead(format!("non-UTF8 path: {}", journal_path.display()))
29 })?;
30 let mut journal = Journal::read_from_path(journal_path_str)?;
31
32 let old_snapshot = {
33 let mut found = None;
34 for entry in journal.entries.iter().rev() {
35 let base = entry.name.strip_suffix(".sql").unwrap_or(&entry.name);
36 let snap_name = format!("{base}.snapshot.json");
37 let snap_path = Path::new(migrations_dir).join(&snap_name);
38 if snap_path.exists() {
39 let snap_str = snap_path.to_str().ok_or_else(|| {
40 DbCoreError::SnapshotRead(format!("non-UTF8 path: {}", snap_path.display()))
41 })?;
42 found = Some(Snapshot::read_from_path(snap_str)?);
43 break;
44 }
45 }
46 match found {
47 Some(snapshot) => snapshot,
48 None if journal.entries.is_empty() => Snapshot::empty(),
49 None => {
50 return Err(DbCoreError::SnapshotRead(
51 "no snapshot file found for any journal entry — create a snapshot for the latest migration before generating".to_owned(),
52 ));
53 },
54 }
55 };
56
57 let ops = diff(&old_snapshot, registry);
58 if ops.is_empty() {
59 return Ok(None);
60 }
61
62 let sql = generate_sql_for(&ops, dialect);
63 let hash = compute_hash(&sql);
64 let next = journal.next_migration_number();
65 let filename = format!("{next:04}_{name}.sql");
66 let snapshot_name = format!("{next:04}_{name}.snapshot.json");
67
68 let sql_path = Path::new(migrations_dir).join(&filename);
69 std::fs::write(&sql_path, &sql)
70 .map_err(|e| DbCoreError::MigrationWrite(format!("{}: {e}", sql_path.display())))?;
71
72 let mut new_snapshot = Snapshot::from_registry(registry);
73 new_snapshot.dialect = dialect.as_str().to_owned();
74 new_snapshot.prev_id = if old_snapshot.tables.is_empty() {
75 ZERO_SNAPSHOT_PARENT.to_owned()
76 } else {
77 old_snapshot.id.clone()
78 };
79 new_snapshot.enums.clone_from(&old_snapshot.enums);
80 new_snapshot.meta.clone_from(&old_snapshot.meta);
81
82 let snap_path = Path::new(migrations_dir).join(&snapshot_name);
83 let snap_str = snap_path
84 .to_str()
85 .ok_or_else(|| DbCoreError::SnapshotWrite(format!("non-UTF8 path: {}", snap_path.display())))?;
86 new_snapshot.write_to_path(snap_str)?;
87
88 journal.add_entry(&filename, &hash);
89 journal.write_to_path(journal_path_str)?;
90
91 Ok(Some(filename))
92}