doido_generators/generators/
migration.rs1use crate::generator::{GeneratedFile, Generator};
2use crate::generators::field::Field;
3use crate::generators::migration_support::{
4 create_table_imports, create_table_up, drop_table_down, register_migration,
5 render_migration_file, MIGRATION_LIB_BASE, MIGRATION_SRC_DIR,
6};
7use crate::generators::to_snake;
8use chrono::Utc;
9use doido_core::Result;
10
11pub struct MigrationGenerator;
12
13impl Generator for MigrationGenerator {
14 fn name(&self) -> &str {
15 "migration"
16 }
17
18 fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
19 let name = args.first().copied().ok_or_else(|| {
20 doido_core::anyhow::anyhow!("migration generator requires a name argument")
21 })?;
22 let snake = to_snake(name);
23 let fields = Field::parse_all(&args[1..])?;
25
26 let (imports, up_body, down_body) = MigrationOp::parse(&snake).render(&fields);
29 let migration = render_migration_file(&imports, &up_body, &down_body);
30
31 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
34 let migration_module = format!("m{timestamp}_{snake}");
35
36 let lib_path = format!("{MIGRATION_SRC_DIR}/lib.rs");
39 let existing =
40 std::fs::read_to_string(&lib_path).unwrap_or_else(|_| MIGRATION_LIB_BASE.to_string());
41 let lib = register_migration(&existing, &migration_module);
42
43 let test = crate::templates::get("migration/migration_test.rs.template")
44 .replace("{snake}", &snake);
45
46 Ok(vec![
47 GeneratedFile {
48 path: format!("{MIGRATION_SRC_DIR}/{migration_module}.rs"),
49 content: migration,
50 },
51 GeneratedFile {
52 path: lib_path,
53 content: lib,
54 },
55 GeneratedFile {
56 path: format!("tests/{snake}_migration_test.rs"),
57 content: test,
58 },
59 ])
60 }
61}
62
63enum MigrationOp {
65 CreateTable { table: String },
67 DropTable { table: String },
69 AddColumns { table: String },
71 RemoveColumns { table: String },
73 Generic { name: String },
75}
76
77impl MigrationOp {
78 fn parse(snake: &str) -> Self {
80 if let Some(table) = snake.strip_prefix("create_") {
81 return Self::CreateTable {
82 table: table.to_string(),
83 };
84 }
85 if let Some(rest) = snake.strip_prefix("add_") {
86 if let Some((_, table)) = rest.rsplit_once("_to_") {
87 return Self::AddColumns {
88 table: table.to_string(),
89 };
90 }
91 }
92 for prefix in ["remove_", "delete_", "drop_"] {
93 if let Some(rest) = snake.strip_prefix(prefix) {
94 if let Some((_, table)) = rest.rsplit_once("_from_") {
95 return Self::RemoveColumns {
96 table: table.to_string(),
97 };
98 }
99 }
100 }
101 if let Some(table) = snake.strip_prefix("drop_") {
102 return Self::DropTable {
103 table: table.to_string(),
104 };
105 }
106 Self::Generic {
107 name: snake.to_string(),
108 }
109 }
110
111 fn render(&self, fields: &[Field]) -> (String, String, String) {
113 match self {
114 Self::CreateTable { table } | Self::Generic { name: table } => (
115 create_table_imports(fields),
116 create_table_up(table, fields),
117 drop_table_down(table),
118 ),
119 Self::DropTable { table } => {
120 let up = format!(" drop_table(manager, \"{table}\").await\n");
121 if fields.is_empty() {
122 let down = format!(
123 " // TODO: re-create the `{table}` table to make this reversible.\n\
124 \x20 let _ = manager;\n Ok(())\n"
125 );
126 (
127 "use doido_model::migration::drop_table;".to_string(),
128 up,
129 down,
130 )
131 } else {
132 (
134 create_table_imports(fields),
135 up,
136 create_table_up(table, fields),
137 )
138 }
139 }
140 Self::AddColumns { table } => (
141 "use doido_model::migration::alter_table;".to_string(),
142 alter_body(table, &add_lines(fields)),
143 alter_body(table, &drop_lines(fields)),
144 ),
145 Self::RemoveColumns { table } => (
146 "use doido_model::migration::alter_table;".to_string(),
147 alter_body(table, &drop_lines(fields)),
148 alter_body(table, &add_lines(fields)),
149 ),
150 }
151 }
152}
153
154fn add_lines(fields: &[Field]) -> Vec<String> {
155 fields.iter().map(Field::alter_add_line).collect()
156}
157
158fn drop_lines(fields: &[Field]) -> Vec<String> {
159 fields.iter().map(Field::alter_drop_line).collect()
160}
161
162fn alter_body(table: &str, lines: &[String]) -> String {
165 if lines.is_empty() {
166 return format!(
167 " // TODO: describe the column changes, e.g.\n\
168 \x20 // `t.add_column(\"name\", |c| {{ c.string(); }});`.\n\
169 \x20 alter_table(manager, \"{table}\", |_t| {{}}).await\n"
170 );
171 }
172 let body: String = lines.iter().map(|l| format!(" {l}\n")).collect();
173 format!(" alter_table(manager, \"{table}\", |t| {{\n{body} }})\n .await\n")
174}