1use crate::error::Error;
10use crate::model::{ColumnDef, DefaultValue, Schema, TableDef};
11use crate::sql::{column_sql, 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 summary.push(format!("create {}", t.name));
54 }
55 }
56 for t in &old.tables {
57 if new.table(&t.name).is_none() {
58 stmts.push(format!("DROP TABLE {};", t.name));
59 summary.push(format!("drop {}", t.name));
60 destructive = true;
61 }
62 }
63 for t in &new.tables {
64 if let Some(old_t) = old.table(&t.name) {
65 diff_table(
66 old_t,
67 t,
68 &mut stmts,
69 &mut summary,
70 &mut destructive,
71 &mut needs_manual_edit,
72 )?;
73 }
74 }
75
76 if stmts.is_empty() {
77 return Ok(None);
78 }
79 Ok(Some(Migration {
80 sql: stmts.join("\n\n") + "\n",
81 summary: summary.join("; "),
82 destructive,
83 needs_manual_edit,
84 }))
85}
86
87fn diff_table(
88 old: &TableDef,
89 new: &TableDef,
90 stmts: &mut Vec<String>,
91 summary: &mut Vec<String>,
92 destructive: &mut bool,
93 needs_manual_edit: &mut bool,
94) -> Result<(), Error> {
95 let mut eff = old.clone();
100 let mut renames: Vec<(String, String)> = Vec::new();
101 for c in &new.columns {
102 if let Some(from) = &c.renamed_from
103 && eff.column(&c.name).is_none()
104 && let Some(oc) = eff.columns.iter_mut().find(|oc| oc.name == *from)
105 {
106 oc.name = c.name.clone();
107 renames.push((from.clone(), c.name.clone()));
108 }
109 }
110
111 let added: Vec<&ColumnDef> = new
112 .columns
113 .iter()
114 .filter(|c| eff.column(&c.name).is_none())
115 .collect();
116 let dropped: Vec<ColumnDef> = eff
117 .columns
118 .iter()
119 .filter(|c| new.column(&c.name).is_none())
120 .cloned()
121 .collect();
122 let changed: Vec<String> = new
123 .columns
124 .iter()
125 .filter(|c| {
126 eff.column(&c.name)
127 .is_some_and(|oc| oc.signature() != c.signature())
128 })
129 .map(|c| c.name.clone())
130 .collect();
131 let index_changes: Vec<(&ColumnDef, bool)> = new
133 .columns
134 .iter()
135 .filter_map(|c| {
136 let old_c = eff.column(&c.name)?;
137 (old_c.index != c.index && old_c.signature() == c.signature())
138 .then_some((c, c.index))
139 })
140 .collect();
141
142 if renames.is_empty()
143 && added.is_empty()
144 && dropped.is_empty()
145 && changed.is_empty()
146 && index_changes.is_empty()
147 {
148 return Ok(());
149 }
150
151 let mut bits: Vec<String> = Vec::new();
152 bits.extend(renames.iter().map(|(f, t)| format!("rename {f} -> {t}")));
153 bits.extend(added.iter().map(|c| format!("add {}", c.name)));
154 bits.extend(dropped.iter().map(|c| format!("drop {}", c.name)));
155 bits.extend(changed.iter().map(|c| format!("change {c}")));
156 bits.extend(
157 index_changes
158 .iter()
159 .map(|(c, on)| format!("{} {}", if *on { "index" } else { "unindex" }, c.name)),
160 );
161
162 let rebuild = !changed.is_empty()
163 || added.iter().any(|c| !can_add_column(c))
164 || dropped.iter().any(|c| !can_drop_column(c));
165
166 if rebuild {
167 stmts.push(rebuild_table_sql(old, new, needs_manual_edit));
170 stmts.extend(crate::sql::index_sqls(new));
171 summary.push(format!("rebuild {} ({})", new.name, bits.join(", ")));
172 } else {
173 for (from, to) in &renames {
174 if let Some(c) = new.column(to)
177 && c.index
178 {
179 stmts.push(format!("DROP INDEX IF EXISTS {};", index_name(&new.name, from)));
180 }
181 stmts.push(format!("ALTER TABLE {} RENAME COLUMN {from} TO {to};", new.name));
182 if let Some(c) = new.column(to)
183 && c.index
184 {
185 stmts.push(create_index_sql(&new.name, &c.name));
186 }
187 }
188 for c in &added {
189 stmts.push(format!("ALTER TABLE {} ADD COLUMN {};", new.name, column_sql(c, false)));
190 if c.index {
191 stmts.push(create_index_sql(&new.name, &c.name));
192 }
193 }
194 for c in &dropped {
195 stmts.push(format!("ALTER TABLE {} DROP COLUMN {};", new.name, c.name));
196 }
197 for (c, on) in &index_changes {
198 stmts.push(if *on {
199 create_index_sql(&new.name, &c.name)
200 } else {
201 format!("DROP INDEX IF EXISTS {};", index_name(&new.name, &c.name))
202 });
203 }
204 summary.push(format!("{}: {}", new.name, bits.join(", ")));
205 }
206 if !dropped.is_empty() {
207 *destructive = true;
208 }
209 Ok(())
210}
211
212fn create_index_sql(table: &str, column: &str) -> String {
213 format!("CREATE INDEX {} ON {table} ({column});", index_name(table, column))
214}
215
216fn can_add_column(c: &ColumnDef) -> bool {
220 let constant_default = matches!(&c.default, Some(d) if !matches!(d, DefaultValue::Now));
221 if c.primary_key || c.unique {
222 return false;
223 }
224 if c.references.is_some() {
225 return c.nullable && c.default.is_none();
226 }
227 c.nullable && !matches!(c.default, Some(DefaultValue::Now)) || constant_default
228}
229
230fn can_drop_column(c: &ColumnDef) -> bool {
232 !c.primary_key && !c.unique && !c.index
233}
234
235fn rebuild_table_sql(old: &TableDef, new: &TableDef, needs_manual_edit: &mut bool) -> String {
236 let table = &new.name;
237 let tmp_name = format!("{table}_new");
238 let tmp = TableDef {
239 name: tmp_name.clone(),
240 ..new.clone()
241 };
242 let create = create_table_sql(&tmp);
243
244 let cols: Vec<String> = new.columns.iter().map(|c| c.name.clone()).collect();
245 let exprs: Vec<String> = new
246 .columns
247 .iter()
248 .map(|c| {
249 if let Some(from) = &c.renamed_from
250 && old.column(from).is_some()
251 {
252 return from.clone();
253 }
254 if old.column(&c.name).is_some() {
255 return c.name.clone();
256 }
257 if let Some(d) = &c.default {
258 return d.sql();
259 }
260 if c.nullable {
261 return "NULL".into();
262 }
263 *needs_manual_edit = true;
264 format!("NULL /* TODO: backfill NOT NULL column {} */", c.name)
265 })
266 .collect();
267
268 format!(
269 "-- {table}: SQLite cannot express this change with ALTER TABLE, so the\n\
270 -- table is rebuilt and its rows copied over.\n\
271 {create}\n\n\
272 INSERT INTO {tmp_name} ({})\nSELECT {}\nFROM {table};\n\n\
273 DROP TABLE {table};\n\n\
274 ALTER TABLE {tmp_name} RENAME TO {table};",
275 cols.join(", "),
276 exprs.join(", "),
277 )
278}