1use std::collections::HashMap;
2
3use vespertide_core::{MigrationAction, MigrationPlan, TableDef};
4
5use crate::error::PlannerError;
6
7pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result<MigrationPlan, PlannerError> {
9 let mut actions: Vec<MigrationAction> = Vec::new();
10
11 let from_map: HashMap<_, _> = from.iter().map(|t| (t.name.as_str(), t)).collect();
12 let to_map: HashMap<_, _> = to.iter().map(|t| (t.name.as_str(), t)).collect();
13
14 for name in from_map.keys() {
16 if !to_map.contains_key(name) {
17 actions.push(MigrationAction::DeleteTable {
18 table: (*name).to_string(),
19 });
20 }
21 }
22
23 for (name, to_tbl) in &to_map {
25 if let Some(from_tbl) = from_map.get(name) {
26 let from_cols: HashMap<_, _> = from_tbl
28 .columns
29 .iter()
30 .map(|c| (c.name.as_str(), c))
31 .collect();
32 let to_cols: HashMap<_, _> = to_tbl
33 .columns
34 .iter()
35 .map(|c| (c.name.as_str(), c))
36 .collect();
37
38 for col in from_cols.keys() {
40 if !to_cols.contains_key(col) {
41 actions.push(MigrationAction::DeleteColumn {
42 table: (*name).to_string(),
43 column: (*col).to_string(),
44 });
45 }
46 }
47
48 for (col, to_def) in &to_cols {
50 if let Some(from_def) = from_cols.get(col)
51 && from_def.r#type != to_def.r#type
52 {
53 actions.push(MigrationAction::ModifyColumnType {
54 table: (*name).to_string(),
55 column: (*col).to_string(),
56 new_type: to_def.r#type.clone(),
57 });
58 }
59 }
60
61 for (col, def) in &to_cols {
63 if !from_cols.contains_key(col) {
64 actions.push(MigrationAction::AddColumn {
65 table: (*name).to_string(),
66 column: (*def).clone(),
67 fill_with: None,
68 });
69 }
70 }
71
72 let from_indexes: HashMap<_, _> = from_tbl
74 .indexes
75 .iter()
76 .map(|i| (i.name.as_str(), i))
77 .collect();
78 let to_indexes: HashMap<_, _> = to_tbl
79 .indexes
80 .iter()
81 .map(|i| (i.name.as_str(), i))
82 .collect();
83
84 for idx in from_indexes.keys() {
85 if !to_indexes.contains_key(idx) {
86 actions.push(MigrationAction::RemoveIndex {
87 table: (*name).to_string(),
88 name: (*idx).to_string(),
89 });
90 }
91 }
92 for (idx, def) in &to_indexes {
93 if !from_indexes.contains_key(idx) {
94 actions.push(MigrationAction::AddIndex {
95 table: (*name).to_string(),
96 index: (*def).clone(),
97 });
98 }
99 }
100 }
101 }
102
103 for (name, tbl) in &to_map {
105 if !from_map.contains_key(name) {
106 actions.push(MigrationAction::CreateTable {
107 table: tbl.name.clone(),
108 columns: tbl.columns.clone(),
109 constraints: tbl.constraints.clone(),
110 });
111 for idx in &tbl.indexes {
112 actions.push(MigrationAction::AddIndex {
113 table: tbl.name.clone(),
114 index: idx.clone(),
115 });
116 }
117 }
118 }
119
120 Ok(MigrationPlan {
121 comment: None,
122 created_at: None,
123 version: 0,
124 actions,
125 })
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use rstest::rstest;
132 use vespertide_core::{ColumnDef, ColumnType, IndexDef, MigrationAction};
133
134 fn col(name: &str, ty: ColumnType) -> ColumnDef {
135 ColumnDef {
136 name: name.to_string(),
137 r#type: ty,
138 nullable: true,
139 default: None,
140 }
141 }
142
143 fn table(
144 name: &str,
145 columns: Vec<ColumnDef>,
146 constraints: Vec<vespertide_core::TableConstraint>,
147 indexes: Vec<IndexDef>,
148 ) -> TableDef {
149 TableDef {
150 name: name.to_string(),
151 columns,
152 constraints,
153 indexes,
154 }
155 }
156
157 #[rstest]
158 #[case::add_column_and_index(
159 vec![table(
160 "users",
161 vec![col("id", ColumnType::Integer)],
162 vec![],
163 vec![],
164 )],
165 vec![table(
166 "users",
167 vec![
168 col("id", ColumnType::Integer),
169 col("name", ColumnType::Text),
170 ],
171 vec![],
172 vec![IndexDef {
173 name: "idx_users_name".into(),
174 columns: vec!["name".into()],
175 unique: false,
176 }],
177 )],
178 vec![
179 MigrationAction::AddColumn {
180 table: "users".into(),
181 column: col("name", ColumnType::Text),
182 fill_with: None,
183 },
184 MigrationAction::AddIndex {
185 table: "users".into(),
186 index: IndexDef {
187 name: "idx_users_name".into(),
188 columns: vec!["name".into()],
189 unique: false,
190 },
191 },
192 ]
193 )]
194 #[case::drop_table(
195 vec![table(
196 "users",
197 vec![col("id", ColumnType::Integer)],
198 vec![],
199 vec![],
200 )],
201 vec![],
202 vec![MigrationAction::DeleteTable {
203 table: "users".into()
204 }]
205 )]
206 #[case::add_table(
207 vec![],
208 vec![table(
209 "users",
210 vec![col("id", ColumnType::Integer)],
211 vec![],
212 vec![IndexDef {
213 name: "idx_users_id".into(),
214 columns: vec!["id".into()],
215 unique: true,
216 }],
217 )],
218 vec![
219 MigrationAction::CreateTable {
220 table: "users".into(),
221 columns: vec![col("id", ColumnType::Integer)],
222 constraints: vec![],
223 },
224 MigrationAction::AddIndex {
225 table: "users".into(),
226 index: IndexDef {
227 name: "idx_users_id".into(),
228 columns: vec!["id".into()],
229 unique: true,
230 },
231 },
232 ]
233 )]
234 #[case::delete_column(
235 vec![table(
236 "users",
237 vec![col("id", ColumnType::Integer), col("name", ColumnType::Text)],
238 vec![],
239 vec![],
240 )],
241 vec![table(
242 "users",
243 vec![col("id", ColumnType::Integer)],
244 vec![],
245 vec![],
246 )],
247 vec![MigrationAction::DeleteColumn {
248 table: "users".into(),
249 column: "name".into(),
250 }]
251 )]
252 #[case::modify_column_type(
253 vec![table(
254 "users",
255 vec![col("id", ColumnType::Integer)],
256 vec![],
257 vec![],
258 )],
259 vec![table(
260 "users",
261 vec![col("id", ColumnType::Text)],
262 vec![],
263 vec![],
264 )],
265 vec![MigrationAction::ModifyColumnType {
266 table: "users".into(),
267 column: "id".into(),
268 new_type: ColumnType::Text,
269 }]
270 )]
271 #[case::remove_index(
272 vec![table(
273 "users",
274 vec![col("id", ColumnType::Integer)],
275 vec![],
276 vec![IndexDef {
277 name: "idx_users_id".into(),
278 columns: vec!["id".into()],
279 unique: false,
280 }],
281 )],
282 vec![table(
283 "users",
284 vec![col("id", ColumnType::Integer)],
285 vec![],
286 vec![],
287 )],
288 vec![MigrationAction::RemoveIndex {
289 table: "users".into(),
290 name: "idx_users_id".into(),
291 }]
292 )]
293 #[case::add_index_existing_table(
294 vec![table(
295 "users",
296 vec![col("id", ColumnType::Integer)],
297 vec![],
298 vec![],
299 )],
300 vec![table(
301 "users",
302 vec![col("id", ColumnType::Integer)],
303 vec![],
304 vec![IndexDef {
305 name: "idx_users_id".into(),
306 columns: vec!["id".into()],
307 unique: true,
308 }],
309 )],
310 vec![MigrationAction::AddIndex {
311 table: "users".into(),
312 index: IndexDef {
313 name: "idx_users_id".into(),
314 columns: vec!["id".into()],
315 unique: true,
316 },
317 }]
318 )]
319 fn diff_schemas_detects_additions(
320 #[case] from_schema: Vec<TableDef>,
321 #[case] to_schema: Vec<TableDef>,
322 #[case] expected_actions: Vec<MigrationAction>,
323 ) {
324 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
325 assert_eq!(plan.actions, expected_actions);
326 }
327}