1use crate::error::Error;
10use crate::model::{ColumnDef, DefaultValue, Schema, TableDef};
11use crate::sql::{column_sql, composite_index_name, create_table_sql, index_name, quote_ident};
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 {};", quote_ident(&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()).then_some((c, c.index))
139 })
140 .collect();
141
142 let unique_added: Vec<&Vec<String>> = new
145 .composite_uniques
146 .iter()
147 .filter(|c| !old.composite_uniques.contains(c))
148 .collect();
149 let unique_removed: Vec<&Vec<String>> = old
150 .composite_uniques
151 .iter()
152 .filter(|c| !new.composite_uniques.contains(c))
153 .collect();
154 let index_added: Vec<&Vec<String>> = new
155 .composite_indexes
156 .iter()
157 .filter(|c| !old.composite_indexes.contains(c))
158 .collect();
159 let index_removed: Vec<&Vec<String>> = old
160 .composite_indexes
161 .iter()
162 .filter(|c| !new.composite_indexes.contains(c))
163 .collect();
164
165 if renames.is_empty()
166 && added.is_empty()
167 && dropped.is_empty()
168 && changed.is_empty()
169 && index_changes.is_empty()
170 && unique_added.is_empty()
171 && unique_removed.is_empty()
172 && index_added.is_empty()
173 && index_removed.is_empty()
174 {
175 return Ok(());
176 }
177
178 let mut bits: Vec<String> = Vec::new();
179 bits.extend(renames.iter().map(|(f, t)| format!("rename {f} -> {t}")));
180 bits.extend(added.iter().map(|c| format!("add {}", c.name)));
181 bits.extend(dropped.iter().map(|c| format!("drop {}", c.name)));
182 bits.extend(changed.iter().map(|c| format!("change {c}")));
183 bits.extend(
184 index_changes
185 .iter()
186 .map(|(c, on)| format!("{} {}", if *on { "index" } else { "unindex" }, c.name)),
187 );
188 bits.extend(
189 unique_added
190 .iter()
191 .map(|c| format!("unique({})", c.join(","))),
192 );
193 bits.extend(
194 unique_removed
195 .iter()
196 .map(|c| format!("drop unique({})", c.join(","))),
197 );
198 bits.extend(
199 index_added
200 .iter()
201 .map(|c| format!("index({})", c.join(","))),
202 );
203 bits.extend(
204 index_removed
205 .iter()
206 .map(|c| format!("drop index({})", c.join(","))),
207 );
208
209 let rebuild = !changed.is_empty()
210 || added.iter().any(|c| !can_add_column(c))
211 || dropped.iter().any(|c| !can_drop_column(c));
212
213 if rebuild {
214 stmts.push(rebuild_table_sql(old, new, needs_manual_edit));
217 stmts.extend(crate::sql::index_sqls(new));
218 stmts.extend(crate::sql::composite_index_sqls(new));
219 summary.push(format!("rebuild {} ({})", new.name, bits.join(", ")));
220 } else {
221 for (from, to) in &renames {
222 if let Some(c) = new.column(to)
225 && c.index
226 {
227 stmts.push(format!(
228 "DROP INDEX IF EXISTS {};",
229 quote_ident(&index_name(&new.name, from))
230 ));
231 }
232 stmts.push(format!(
233 "ALTER TABLE {} RENAME COLUMN {} TO {};",
234 quote_ident(&new.name),
235 quote_ident(from),
236 quote_ident(to)
237 ));
238 if let Some(c) = new.column(to)
239 && c.index
240 {
241 stmts.push(create_index_sql(&new.name, &c.name));
242 }
243 }
244 for c in &added {
245 stmts.push(format!(
246 "ALTER TABLE {} ADD COLUMN {};",
247 quote_ident(&new.name),
248 column_sql(c, false)
249 ));
250 if c.index {
251 stmts.push(create_index_sql(&new.name, &c.name));
252 }
253 }
254 for c in &dropped {
255 stmts.push(format!(
256 "ALTER TABLE {} DROP COLUMN {};",
257 quote_ident(&new.name),
258 quote_ident(&c.name)
259 ));
260 }
261 for (c, on) in &index_changes {
262 stmts.push(if *on {
263 create_index_sql(&new.name, &c.name)
264 } else {
265 format!(
266 "DROP INDEX IF EXISTS {};",
267 quote_ident(&index_name(&new.name, &c.name))
268 )
269 });
270 }
271 for cols in &unique_removed {
272 stmts.push(format!(
273 "DROP INDEX IF EXISTS {};",
274 quote_ident(&composite_index_name(&new.name, cols))
275 ));
276 }
277 for cols in &unique_added {
278 stmts.push(format!(
279 "CREATE UNIQUE INDEX {} ON {} ({});",
280 quote_ident(&composite_index_name(&new.name, cols)),
281 quote_ident(&new.name),
282 cols.iter()
283 .map(|c| quote_ident(c))
284 .collect::<Vec<_>>()
285 .join(", "),
286 ));
287 }
288 for cols in &index_removed {
289 stmts.push(format!(
290 "DROP INDEX IF EXISTS {};",
291 quote_ident(&composite_index_name(&new.name, cols))
292 ));
293 }
294 for cols in &index_added {
295 stmts.push(format!(
296 "CREATE INDEX {} ON {} ({});",
297 quote_ident(&composite_index_name(&new.name, cols)),
298 quote_ident(&new.name),
299 cols.iter()
300 .map(|c| quote_ident(c))
301 .collect::<Vec<_>>()
302 .join(", "),
303 ));
304 }
305 summary.push(format!("{}: {}", new.name, bits.join(", ")));
306 }
307 if !dropped.is_empty() {
308 *destructive = true;
309 }
310 Ok(())
311}
312
313fn create_index_sql(table: &str, column: &str) -> String {
314 format!(
315 "CREATE INDEX {} ON {} ({});",
316 quote_ident(&index_name(table, column)),
317 quote_ident(table),
318 quote_ident(column)
319 )
320}
321
322fn can_add_column(c: &ColumnDef) -> bool {
326 let constant_default = matches!(&c.default, Some(d) if !matches!(d, DefaultValue::Now));
327 if c.primary_key || c.unique {
328 return false;
329 }
330 if c.references.is_some() {
331 return c.nullable && c.default.is_none();
332 }
333 c.nullable && !matches!(c.default, Some(DefaultValue::Now)) || constant_default
334}
335
336fn can_drop_column(c: &ColumnDef) -> bool {
338 !c.primary_key && !c.unique && !c.index
339}
340
341fn rebuild_table_sql(old: &TableDef, new: &TableDef, needs_manual_edit: &mut bool) -> String {
342 let table = &new.name;
343 let tmp_name = format!("{table}_new");
344 let tmp = TableDef {
345 name: tmp_name.clone(),
346 ..new.clone()
347 };
348 let create = create_table_sql(&tmp);
349
350 let cols: Vec<String> = new.columns.iter().map(|c| quote_ident(&c.name)).collect();
351 let exprs: Vec<String> = new
352 .columns
353 .iter()
354 .map(|c| {
355 if let Some(from) = &c.renamed_from
356 && old.column(from).is_some()
357 {
358 return quote_ident(from);
359 }
360 if old.column(&c.name).is_some() {
361 return quote_ident(&c.name);
362 }
363 if let Some(d) = &c.default {
364 return d.sql();
365 }
366 if c.nullable {
367 return "NULL".into();
368 }
369 *needs_manual_edit = true;
370 format!("NULL /* TODO: backfill NOT NULL column {} */", c.name)
371 })
372 .collect();
373
374 format!(
375 "-- {table}: SQLite cannot express this change with ALTER TABLE, so the\n\
376 -- table is rebuilt and its rows copied over.\n\
377 {create}\n\n\
378 INSERT INTO {qtmp} ({})\nSELECT {}\nFROM {qtable};\n\n\
379 DROP TABLE {qtable};\n\n\
380 ALTER TABLE {qtmp} RENAME TO {qtable};",
381 cols.join(", "),
382 exprs.join(", "),
383 qtmp = quote_ident(&tmp_name),
384 qtable = quote_ident(table),
385 )
386}