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 timestamp = Utc::now().format("%Y%m%d_%H%M%S");
30 let migration_module = format!("m{timestamp}_{snake}");
31 let migration = render_migration_file(&migration_module, &imports, &up_body, &down_body);
32
33 let lib_path = format!("{MIGRATION_SRC_DIR}/lib.rs");
38 let existing =
39 std::fs::read_to_string(&lib_path).unwrap_or_else(|_| MIGRATION_LIB_BASE.to_string());
40 let lib = register_migration(&existing, &migration_module);
41
42 let test = crate::templates::get("migration/migration_test.rs.template")
43 .replace("{snake}", &snake);
44
45 Ok(vec![
46 GeneratedFile {
47 path: format!("{MIGRATION_SRC_DIR}/{migration_module}.rs"),
48 content: migration,
49 },
50 GeneratedFile {
51 path: lib_path,
52 content: lib,
53 },
54 GeneratedFile {
55 path: format!("tests/{snake}_migration_test.rs"),
56 content: test,
57 },
58 ])
59 }
60}
61
62enum MigrationOp {
64 CreateTable { table: String },
66 DropTable { table: String },
68 AddColumns { table: String },
70 RemoveColumns { table: String },
72 Generic { name: String },
74}
75
76impl MigrationOp {
77 fn parse(snake: &str) -> Self {
79 if let Some(table) = snake.strip_prefix("create_") {
80 return Self::CreateTable {
81 table: table.to_string(),
82 };
83 }
84 if let Some(rest) = snake.strip_prefix("add_") {
85 if let Some((_, table)) = rest.rsplit_once("_to_") {
86 return Self::AddColumns {
87 table: table.to_string(),
88 };
89 }
90 }
91 for prefix in ["remove_", "delete_", "drop_"] {
92 if let Some(rest) = snake.strip_prefix(prefix) {
93 if let Some((_, table)) = rest.rsplit_once("_from_") {
94 return Self::RemoveColumns {
95 table: table.to_string(),
96 };
97 }
98 }
99 }
100 if let Some(table) = snake.strip_prefix("drop_") {
101 return Self::DropTable {
102 table: table.to_string(),
103 };
104 }
105 Self::Generic {
106 name: snake.to_string(),
107 }
108 }
109
110 fn render(&self, fields: &[Field]) -> (String, String, String) {
112 match self {
113 Self::CreateTable { table } | Self::Generic { name: table } => (
114 create_table_imports(fields),
115 create_table_up(table, fields),
116 drop_table_down(table),
117 ),
118 Self::DropTable { table } => {
119 let up = format!(" drop_table(manager, \"{table}\").await\n");
120 if fields.is_empty() {
121 let down = format!(
122 " // TODO: re-create the `{table}` table to make this reversible.\n\
123 \x20 let _ = manager;\n Ok(())\n"
124 );
125 (
126 "use doido::model::migration::drop_table;".to_string(),
127 up,
128 down,
129 )
130 } else {
131 (
133 create_table_imports(fields),
134 up,
135 create_table_up(table, fields),
136 )
137 }
138 }
139 Self::AddColumns { table } => (
140 "use doido::model::migration::alter_table;".to_string(),
141 alter_body(table, &add_lines(fields)),
142 alter_body(table, &drop_lines(fields)),
143 ),
144 Self::RemoveColumns { table } => (
145 "use doido::model::migration::alter_table;".to_string(),
146 alter_body(table, &drop_lines(fields)),
147 alter_body(table, &add_lines(fields)),
148 ),
149 }
150 }
151}
152
153fn add_lines(fields: &[Field]) -> Vec<String> {
154 fields.iter().map(Field::alter_add_line).collect()
155}
156
157fn drop_lines(fields: &[Field]) -> Vec<String> {
158 fields.iter().map(Field::alter_drop_line).collect()
159}
160
161fn alter_body(table: &str, lines: &[String]) -> String {
164 if lines.is_empty() {
165 return format!(
166 " // TODO: describe the column changes, e.g.\n\
167 \x20 // `t.add_column(\"name\", |c| {{ c.string(); }});`.\n\
168 \x20 alter_table(manager, \"{table}\", |_t| {{}}).await\n"
169 );
170 }
171 let body: String = lines.iter().map(|l| format!(" {l}\n")).collect();
172 format!(" alter_table(manager, \"{table}\", |t| {{\n{body} }})\n .await\n")
173}