1use crate::error::Error;
10use crate::model::{ColumnDef, DefaultValue, Schema, TableDef};
11use crate::sql::{column_sql, composite_index_name, create_table_sql, index_name};
12
13#[derive(Debug, Clone)]
14pub struct Migration {
15 pub sql: String,
17 pub summary: String,
19 pub destructive: bool,
21 pub needs_manual_edit: bool,
24}
25
26impl Migration {
27 pub fn filename_slug(&self) -> String {
29 let mut slug = String::new();
30 for ch in self.summary.chars() {
31 if ch.is_ascii_alphanumeric() {
32 slug.push(ch.to_ascii_lowercase());
33 } else if !slug.ends_with('_') && !slug.is_empty() {
34 slug.push('_');
35 }
36 }
37 let slug = slug.trim_matches('_').to_string();
38 slug.chars().take(48).collect()
39 }
40}
41
42pub fn diff_schemas(old: &Schema, new: &Schema) -> Result<Option<Migration>, Error> {
44 let mut stmts: Vec<String> = Vec::new();
45 let mut summary: Vec<String> = Vec::new();
46 let mut destructive = false;
47 let mut needs_manual_edit = false;
48
49 for t in &new.tables {
50 if old.table(&t.name).is_none() {
51 stmts.push(create_table_sql(t));
52 stmts.extend(crate::sql::index_sqls(t));
53 stmts.extend(crate::sql::composite_index_sqls(t));
54 summary.push(format!("create {}", t.name));
55 }
56 }
57 for t in &old.tables {
58 if new.table(&t.name).is_none() {
59 stmts.push(format!("DROP TABLE {};", t.name));
60 summary.push(format!("drop {}", t.name));
61 destructive = true;
62 }
63 }
64 for t in &new.tables {
65 if let Some(old_t) = old.table(&t.name) {
66 diff_table(
67 old_t,
68 t,
69 &mut stmts,
70 &mut summary,
71 &mut destructive,
72 &mut needs_manual_edit,
73 )?;
74 }
75 }
76
77 if stmts.is_empty() {
78 return Ok(None);
79 }
80 Ok(Some(Migration {
81 sql: stmts.join("\n\n") + "\n",
82 summary: summary.join("; "),
83 destructive,
84 needs_manual_edit,
85 }))
86}
87
88fn diff_table(
89 old: &TableDef,
90 new: &TableDef,
91 stmts: &mut Vec<String>,
92 summary: &mut Vec<String>,
93 destructive: &mut bool,
94 needs_manual_edit: &mut bool,
95) -> Result<(), Error> {
96 let mut eff = old.clone();
101 let mut renames: Vec<(String, String)> = Vec::new();
102 for c in &new.columns {
103 if let Some(from) = &c.renamed_from
104 && eff.column(&c.name).is_none()
105 && let Some(oc) = eff.columns.iter_mut().find(|oc| oc.name == *from)
106 {
107 oc.name = c.name.clone();
108 renames.push((from.clone(), c.name.clone()));
109 }
110 }
111
112 let added: Vec<&ColumnDef> = new
113 .columns
114 .iter()
115 .filter(|c| eff.column(&c.name).is_none())
116 .collect();
117 let dropped: Vec<ColumnDef> = eff
118 .columns
119 .iter()
120 .filter(|c| new.column(&c.name).is_none())
121 .cloned()
122 .collect();
123 let changed: Vec<String> = new
124 .columns
125 .iter()
126 .filter(|c| {
127 eff.column(&c.name)
128 .is_some_and(|oc| oc.signature() != c.signature())
129 })
130 .map(|c| c.name.clone())
131 .collect();
132 let index_changes: Vec<(&ColumnDef, bool)> = new
134 .columns
135 .iter()
136 .filter_map(|c| {
137 let old_c = eff.column(&c.name)?;
138 (old_c.index != c.index && old_c.signature() == c.signature())
139 .then_some((c, c.index))
140 })
141 .collect();
142
143 let unique_added: Vec<&Vec<String>> =
146 new.composite_uniques.iter().filter(|c| !old.composite_uniques.contains(c)).collect();
147 let unique_removed: Vec<&Vec<String>> =
148 old.composite_uniques.iter().filter(|c| !new.composite_uniques.contains(c)).collect();
149 let index_added: Vec<&Vec<String>> =
150 new.composite_indexes.iter().filter(|c| !old.composite_indexes.contains(c)).collect();
151 let index_removed: Vec<&Vec<String>> =
152 old.composite_indexes.iter().filter(|c| !new.composite_indexes.contains(c)).collect();
153
154 if renames.is_empty()
155 && added.is_empty()
156 && dropped.is_empty()
157 && changed.is_empty()
158 && index_changes.is_empty()
159 && unique_added.is_empty()
160 && unique_removed.is_empty()
161 && index_added.is_empty()
162 && index_removed.is_empty()
163 {
164 return Ok(());
165 }
166
167 let mut bits: Vec<String> = Vec::new();
168 bits.extend(renames.iter().map(|(f, t)| format!("rename {f} -> {t}")));
169 bits.extend(added.iter().map(|c| format!("add {}", c.name)));
170 bits.extend(dropped.iter().map(|c| format!("drop {}", c.name)));
171 bits.extend(changed.iter().map(|c| format!("change {c}")));
172 bits.extend(
173 index_changes
174 .iter()
175 .map(|(c, on)| format!("{} {}", if *on { "index" } else { "unindex" }, c.name)),
176 );
177 bits.extend(unique_added.iter().map(|c| format!("unique({})", c.join(","))));
178 bits.extend(unique_removed.iter().map(|c| format!("drop unique({})", c.join(","))));
179 bits.extend(index_added.iter().map(|c| format!("index({})", c.join(","))));
180 bits.extend(index_removed.iter().map(|c| format!("drop index({})", c.join(","))));
181
182 let rebuild = !changed.is_empty()
183 || added.iter().any(|c| !can_add_column(c))
184 || dropped.iter().any(|c| !can_drop_column(c));
185
186 if rebuild {
187 stmts.push(rebuild_table_sql(old, new, needs_manual_edit));
190 stmts.extend(crate::sql::index_sqls(new));
191 stmts.extend(crate::sql::composite_index_sqls(new));
192 summary.push(format!("rebuild {} ({})", new.name, bits.join(", ")));
193 } else {
194 for (from, to) in &renames {
195 if let Some(c) = new.column(to)
198 && c.index
199 {
200 stmts.push(format!("DROP INDEX IF EXISTS {};", index_name(&new.name, from)));
201 }
202 stmts.push(format!("ALTER TABLE {} RENAME COLUMN {from} TO {to};", new.name));
203 if let Some(c) = new.column(to)
204 && c.index
205 {
206 stmts.push(create_index_sql(&new.name, &c.name));
207 }
208 }
209 for c in &added {
210 stmts.push(format!("ALTER TABLE {} ADD COLUMN {};", new.name, column_sql(c, false)));
211 if c.index {
212 stmts.push(create_index_sql(&new.name, &c.name));
213 }
214 }
215 for c in &dropped {
216 stmts.push(format!("ALTER TABLE {} DROP COLUMN {};", new.name, c.name));
217 }
218 for (c, on) in &index_changes {
219 stmts.push(if *on {
220 create_index_sql(&new.name, &c.name)
221 } else {
222 format!("DROP INDEX IF EXISTS {};", index_name(&new.name, &c.name))
223 });
224 }
225 for cols in &unique_removed {
226 stmts.push(format!("DROP INDEX IF EXISTS {};", composite_index_name(&new.name, cols)));
227 }
228 for cols in &unique_added {
229 stmts.push(format!(
230 "CREATE UNIQUE INDEX {} ON {} ({});",
231 composite_index_name(&new.name, cols),
232 new.name,
233 cols.join(", "),
234 ));
235 }
236 for cols in &index_removed {
237 stmts.push(format!("DROP INDEX IF EXISTS {};", composite_index_name(&new.name, cols)));
238 }
239 for cols in &index_added {
240 stmts.push(format!(
241 "CREATE INDEX {} ON {} ({});",
242 composite_index_name(&new.name, cols),
243 new.name,
244 cols.join(", "),
245 ));
246 }
247 summary.push(format!("{}: {}", new.name, bits.join(", ")));
248 }
249 if !dropped.is_empty() {
250 *destructive = true;
251 }
252 Ok(())
253}
254
255fn create_index_sql(table: &str, column: &str) -> String {
256 format!("CREATE INDEX {} ON {table} ({column});", index_name(table, column))
257}
258
259fn can_add_column(c: &ColumnDef) -> bool {
263 let constant_default = matches!(&c.default, Some(d) if !matches!(d, DefaultValue::Now));
264 if c.primary_key || c.unique {
265 return false;
266 }
267 if c.references.is_some() {
268 return c.nullable && c.default.is_none();
269 }
270 c.nullable && !matches!(c.default, Some(DefaultValue::Now)) || constant_default
271}
272
273fn can_drop_column(c: &ColumnDef) -> bool {
275 !c.primary_key && !c.unique && !c.index
276}
277
278fn rebuild_table_sql(old: &TableDef, new: &TableDef, needs_manual_edit: &mut bool) -> String {
279 let table = &new.name;
280 let tmp_name = format!("{table}_new");
281 let tmp = TableDef {
282 name: tmp_name.clone(),
283 ..new.clone()
284 };
285 let create = create_table_sql(&tmp);
286
287 let cols: Vec<String> = new.columns.iter().map(|c| c.name.clone()).collect();
288 let exprs: Vec<String> = new
289 .columns
290 .iter()
291 .map(|c| {
292 if let Some(from) = &c.renamed_from
293 && old.column(from).is_some()
294 {
295 return from.clone();
296 }
297 if old.column(&c.name).is_some() {
298 return c.name.clone();
299 }
300 if let Some(d) = &c.default {
301 return d.sql();
302 }
303 if c.nullable {
304 return "NULL".into();
305 }
306 *needs_manual_edit = true;
307 format!("NULL /* TODO: backfill NOT NULL column {} */", c.name)
308 })
309 .collect();
310
311 format!(
312 "-- {table}: SQLite cannot express this change with ALTER TABLE, so the\n\
313 -- table is rebuilt and its rows copied over.\n\
314 {create}\n\n\
315 INSERT INTO {tmp_name} ({})\nSELECT {}\nFROM {table};\n\n\
316 DROP TABLE {table};\n\n\
317 ALTER TABLE {tmp_name} RENAME TO {table};",
318 cols.join(", "),
319 exprs.join(", "),
320 )
321}