1use std::collections::HashMap;
2
3use thiserror::Error;
4use vespertide_core::{IndexDef, MigrationAction, MigrationPlan, TableConstraint, TableDef};
5
6#[derive(Debug, Error)]
7pub enum PlannerError {
8 #[error("table already exists: {0}")]
9 TableExists(String),
10 #[error("table not found: {0}")]
11 TableNotFound(String),
12 #[error("column already exists: {0}.{1}")]
13 ColumnExists(String, String),
14 #[error("column not found: {0}.{1}")]
15 ColumnNotFound(String, String),
16 #[error("index not found: {0}.{1}")]
17 IndexNotFound(String, String),
18}
19
20pub fn plan_next_migration(
24 current: &[TableDef],
25 applied_plans: &[MigrationPlan],
26) -> Result<MigrationPlan, PlannerError> {
27 let baseline = schema_from_plans(applied_plans)?;
28 let mut plan = diff_schemas(&baseline, current)?;
29
30 let next_version = applied_plans
31 .iter()
32 .map(|p| p.version)
33 .max()
34 .unwrap_or(0)
35 .saturating_add(1);
36 plan.version = next_version;
37 Ok(plan)
38}
39
40pub fn schema_from_plans(plans: &[MigrationPlan]) -> Result<Vec<TableDef>, PlannerError> {
42 let mut schema: Vec<TableDef> = Vec::new();
43 for plan in plans {
44 for action in &plan.actions {
45 apply_action(&mut schema, action)?;
46 }
47 }
48 Ok(schema)
49}
50
51pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result<MigrationPlan, PlannerError> {
53 let mut actions: Vec<MigrationAction> = Vec::new();
54
55 let from_map: HashMap<_, _> = from.iter().map(|t| (t.name.as_str(), t)).collect();
56 let to_map: HashMap<_, _> = to.iter().map(|t| (t.name.as_str(), t)).collect();
57
58 for name in from_map.keys() {
60 if !to_map.contains_key(name) {
61 actions.push(MigrationAction::DeleteTable {
62 table: (*name).to_string(),
63 });
64 }
65 }
66
67 for (name, to_tbl) in &to_map {
69 if let Some(from_tbl) = from_map.get(name) {
70 let from_cols: HashMap<_, _> = from_tbl
72 .columns
73 .iter()
74 .map(|c| (c.name.as_str(), c))
75 .collect();
76 let to_cols: HashMap<_, _> = to_tbl
77 .columns
78 .iter()
79 .map(|c| (c.name.as_str(), c))
80 .collect();
81
82 for col in from_cols.keys() {
84 if !to_cols.contains_key(col) {
85 actions.push(MigrationAction::DeleteColumn {
86 table: (*name).to_string(),
87 column: (*col).to_string(),
88 });
89 }
90 }
91
92 for (col, to_def) in &to_cols {
94 if let Some(from_def) = from_cols.get(col) {
95 if from_def.r#type != to_def.r#type {
96 actions.push(MigrationAction::ModifyColumnType {
97 table: (*name).to_string(),
98 column: (*col).to_string(),
99 new_type: to_def.r#type.clone(),
100 });
101 }
102 }
103 }
104
105 for (col, def) in &to_cols {
107 if !from_cols.contains_key(col) {
108 actions.push(MigrationAction::AddColumn {
109 table: (*name).to_string(),
110 column: (*def).clone(),
111 fill_with: None,
112 });
113 }
114 }
115
116 let from_indexes: HashMap<_, _> = from_tbl
118 .indexes
119 .iter()
120 .map(|i| (i.name.as_str(), i))
121 .collect();
122 let to_indexes: HashMap<_, _> = to_tbl
123 .indexes
124 .iter()
125 .map(|i| (i.name.as_str(), i))
126 .collect();
127
128 for idx in from_indexes.keys() {
129 if !to_indexes.contains_key(idx) {
130 actions.push(MigrationAction::RemoveIndex {
131 table: (*name).to_string(),
132 name: (*idx).to_string(),
133 });
134 }
135 }
136 for (idx, def) in &to_indexes {
137 if !from_indexes.contains_key(idx) {
138 actions.push(MigrationAction::AddIndex {
139 table: (*name).to_string(),
140 index: (*def).clone(),
141 });
142 }
143 }
144 }
145 }
146
147 for (name, tbl) in &to_map {
149 if !from_map.contains_key(name) {
150 actions.push(MigrationAction::CreateTable {
151 table: tbl.name.clone(),
152 columns: tbl.columns.clone(),
153 constraints: tbl.constraints.clone(),
154 });
155 for idx in &tbl.indexes {
156 actions.push(MigrationAction::AddIndex {
157 table: tbl.name.clone(),
158 index: idx.clone(),
159 });
160 }
161 }
162 }
163
164 Ok(MigrationPlan {
165 comment: None,
166 created_at: None,
167 version: 0,
168 actions,
169 })
170}
171
172pub fn apply_action(
174 schema: &mut Vec<TableDef>,
175 action: &MigrationAction,
176) -> Result<(), PlannerError> {
177 match action {
178 MigrationAction::CreateTable {
179 table,
180 columns,
181 constraints,
182 } => {
183 if schema.iter().any(|t| t.name == *table) {
184 return Err(PlannerError::TableExists(table.clone()));
185 }
186 schema.push(TableDef {
187 name: table.clone(),
188 columns: columns.clone(),
189 constraints: constraints.clone(),
190 indexes: Vec::new(),
191 });
192 Ok(())
193 }
194 MigrationAction::DeleteTable { table } => {
195 let before = schema.len();
196 schema.retain(|t| t.name != *table);
197 if schema.len() == before {
198 return Err(PlannerError::TableNotFound(table.clone()));
199 }
200 Ok(())
201 }
202 MigrationAction::AddColumn {
203 table,
204 column,
205 fill_with: _,
206 } => {
207 let tbl = schema
208 .iter_mut()
209 .find(|t| t.name == *table)
210 .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?;
211 if tbl.columns.iter().any(|c| c.name == column.name) {
212 return Err(PlannerError::ColumnExists(
213 table.clone(),
214 column.name.clone(),
215 ));
216 }
217 tbl.columns.push(column.clone());
218 Ok(())
219 }
220 MigrationAction::RenameColumn { table, from, to } => {
221 let tbl = schema
222 .iter_mut()
223 .find(|t| t.name == *table)
224 .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?;
225 let col = tbl
226 .columns
227 .iter_mut()
228 .find(|c| c.name == *from)
229 .ok_or_else(|| PlannerError::ColumnNotFound(table.clone(), from.clone()))?;
230 col.name = to.clone();
231 rename_column_in_constraints(&mut tbl.constraints, from, to);
232 rename_column_in_indexes(&mut tbl.indexes, from, to);
233 Ok(())
234 }
235 MigrationAction::DeleteColumn { table, column } => {
236 let tbl = schema
237 .iter_mut()
238 .find(|t| t.name == *table)
239 .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?;
240 let before = tbl.columns.len();
241 tbl.columns.retain(|c| c.name != *column);
242 if tbl.columns.len() == before {
243 return Err(PlannerError::ColumnNotFound(table.clone(), column.clone()));
244 }
245 drop_column_from_constraints(&mut tbl.constraints, column);
246 drop_column_from_indexes(&mut tbl.indexes, column);
247 Ok(())
248 }
249 MigrationAction::ModifyColumnType {
250 table,
251 column,
252 new_type,
253 } => {
254 let tbl = schema
255 .iter_mut()
256 .find(|t| t.name == *table)
257 .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?;
258 let col = tbl
259 .columns
260 .iter_mut()
261 .find(|c| c.name == *column)
262 .ok_or_else(|| PlannerError::ColumnNotFound(table.clone(), column.clone()))?;
263 col.r#type = new_type.clone();
264 Ok(())
265 }
266 MigrationAction::AddIndex { table, index } => {
267 let tbl = schema
268 .iter_mut()
269 .find(|t| t.name == *table)
270 .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?;
271 tbl.indexes.push(index.clone());
272 Ok(())
273 }
274 MigrationAction::RemoveIndex { table, name } => {
275 let tbl = schema
276 .iter_mut()
277 .find(|t| t.name == *table)
278 .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?;
279 let before = tbl.indexes.len();
280 tbl.indexes.retain(|i| i.name != *name);
281 if tbl.indexes.len() == before {
282 return Err(PlannerError::IndexNotFound(table.clone(), name.clone()));
283 }
284 Ok(())
285 }
286 MigrationAction::RenameTable { from, to } => {
287 if schema.iter().any(|t| t.name == *to) {
288 return Err(PlannerError::TableExists(to.clone()));
289 }
290 let tbl = schema
291 .iter_mut()
292 .find(|t| t.name == *from)
293 .ok_or_else(|| PlannerError::TableNotFound(from.clone()))?;
294 tbl.name = to.clone();
295 Ok(())
296 }
297 }
298}
299
300fn rename_column_in_constraints(constraints: &mut [TableConstraint], from: &str, to: &str) {
301 for constraint in constraints {
302 match constraint {
303 TableConstraint::PrimaryKey(cols) => {
304 for c in cols.iter_mut() {
305 if c == from {
306 *c = to.to_string();
307 }
308 }
309 }
310 TableConstraint::Unique { columns, .. } => {
311 for c in columns.iter_mut() {
312 if c == from {
313 *c = to.to_string();
314 }
315 }
316 }
317 TableConstraint::ForeignKey {
318 columns,
319 ref_columns,
320 ..
321 } => {
322 for c in columns.iter_mut() {
323 if c == from {
324 *c = to.to_string();
325 }
326 }
327 for c in ref_columns.iter_mut() {
328 if c == from {
329 *c = to.to_string();
330 }
331 }
332 }
333 TableConstraint::Check { .. } => {}
334 }
335 }
336}
337
338fn rename_column_in_indexes(indexes: &mut [IndexDef], from: &str, to: &str) {
339 for idx in indexes {
340 for c in idx.columns.iter_mut() {
341 if c == from {
342 *c = to.to_string();
343 }
344 }
345 }
346}
347
348fn drop_column_from_constraints(constraints: &mut Vec<TableConstraint>, column: &str) {
349 constraints.retain_mut(|c| match c {
350 TableConstraint::PrimaryKey(cols) => {
351 cols.retain(|c| c != column);
352 !cols.is_empty()
353 }
354 TableConstraint::Unique { columns, .. } => {
355 columns.retain(|c| c != column);
356 !columns.is_empty()
357 }
358 TableConstraint::ForeignKey {
359 columns,
360 ref_columns,
361 ..
362 } => {
363 columns.retain(|c| c != column);
364 ref_columns.retain(|c| c != column);
365 !columns.is_empty() && !ref_columns.is_empty()
366 }
367 TableConstraint::Check { .. } => true,
368 });
369}
370
371fn drop_column_from_indexes(indexes: &mut Vec<IndexDef>, column: &str) {
372 indexes.retain(|idx| !idx.columns.iter().any(|c| c == column));
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use rstest::rstest;
379 use vespertide_core::{ColumnDef, ColumnType, IndexDef, TableConstraint};
380
381 fn col(name: &str, ty: ColumnType) -> ColumnDef {
382 ColumnDef {
383 name: name.to_string(),
384 r#type: ty,
385 nullable: true,
386 default: None,
387 }
388 }
389
390 fn table(
391 name: &str,
392 columns: Vec<ColumnDef>,
393 constraints: Vec<TableConstraint>,
394 indexes: Vec<IndexDef>,
395 ) -> TableDef {
396 TableDef {
397 name: name.to_string(),
398 columns,
399 constraints,
400 indexes,
401 }
402 }
403
404 #[derive(Debug, Clone, Copy)]
405 enum ErrKind {
406 TableExists,
407 TableNotFound,
408 ColumnExists,
409 ColumnNotFound,
410 IndexNotFound,
411 }
412
413 fn assert_err_kind(err: PlannerError, kind: ErrKind) {
414 match (err, kind) {
415 (PlannerError::TableExists(_), ErrKind::TableExists) => {}
416 (PlannerError::TableNotFound(_), ErrKind::TableNotFound) => {}
417 (PlannerError::ColumnExists(_, _), ErrKind::ColumnExists) => {}
418 (PlannerError::ColumnNotFound(_, _), ErrKind::ColumnNotFound) => {}
419 (PlannerError::IndexNotFound(_, _), ErrKind::IndexNotFound) => {}
420 (other, expected) => panic!("unexpected error {other:?}, expected {:?}", expected),
421 }
422 }
423
424 #[rstest]
425 #[case::create_only(
426 vec![MigrationPlan {
427 comment: None,
428 created_at: None,
429 version: 1,
430 actions: vec![MigrationAction::CreateTable {
431 table: "users".into(),
432 columns: vec![col("id", ColumnType::Integer)],
433 constraints: vec![TableConstraint::PrimaryKey(vec!["id".into()])],
434 }],
435 }],
436 table(
437 "users",
438 vec![col("id", ColumnType::Integer)],
439 vec![TableConstraint::PrimaryKey(vec!["id".into()])],
440 vec![],
441 )
442 )]
443 #[case::create_and_add_column(
444 vec![
445 MigrationPlan {
446 comment: None,
447 created_at: None,
448 version: 1,
449 actions: vec![MigrationAction::CreateTable {
450 table: "users".into(),
451 columns: vec![col("id", ColumnType::Integer)],
452 constraints: vec![TableConstraint::PrimaryKey(vec!["id".into()])],
453 }],
454 },
455 MigrationPlan {
456 comment: None,
457 created_at: None,
458 version: 2,
459 actions: vec![MigrationAction::AddColumn {
460 table: "users".into(),
461 column: col("name", ColumnType::Text),
462 fill_with: None,
463 }],
464 },
465 ],
466 table(
467 "users",
468 vec![
469 col("id", ColumnType::Integer),
470 col("name", ColumnType::Text),
471 ],
472 vec![TableConstraint::PrimaryKey(vec!["id".into()])],
473 vec![],
474 )
475 )]
476 #[case::create_add_column_and_index(
477 vec![
478 MigrationPlan {
479 comment: None,
480 created_at: None,
481 version: 1,
482 actions: vec![MigrationAction::CreateTable {
483 table: "users".into(),
484 columns: vec![col("id", ColumnType::Integer)],
485 constraints: vec![TableConstraint::PrimaryKey(vec!["id".into()])],
486 }],
487 },
488 MigrationPlan {
489 comment: None,
490 created_at: None,
491 version: 2,
492 actions: vec![MigrationAction::AddColumn {
493 table: "users".into(),
494 column: col("name", ColumnType::Text),
495 fill_with: None,
496 }],
497 },
498 MigrationPlan {
499 comment: None,
500 created_at: None,
501 version: 3,
502 actions: vec![MigrationAction::AddIndex {
503 table: "users".into(),
504 index: IndexDef {
505 name: "idx_users_name".into(),
506 columns: vec!["name".into()],
507 unique: false,
508 },
509 }],
510 },
511 ],
512 table(
513 "users",
514 vec![
515 col("id", ColumnType::Integer),
516 col("name", ColumnType::Text),
517 ],
518 vec![TableConstraint::PrimaryKey(vec!["id".into()])],
519 vec![IndexDef {
520 name: "idx_users_name".into(),
521 columns: vec!["name".into()],
522 unique: false,
523 }],
524 )
525 )]
526 fn schema_from_plans_applies_actions(
527 #[case] plans: Vec<MigrationPlan>,
528 #[case] expected_users: TableDef,
529 ) {
530 let schema = schema_from_plans(&plans).unwrap();
531 let users = schema.iter().find(|t| t.name == "users").unwrap();
532 assert_eq!(users, &expected_users);
533 }
534
535 #[rstest]
536 #[case::add_column_and_index(
537 vec![table(
538 "users",
539 vec![col("id", ColumnType::Integer)],
540 vec![],
541 vec![],
542 )],
543 vec![table(
544 "users",
545 vec![
546 col("id", ColumnType::Integer),
547 col("name", ColumnType::Text),
548 ],
549 vec![],
550 vec![IndexDef {
551 name: "idx_users_name".into(),
552 columns: vec!["name".into()],
553 unique: false,
554 }],
555 )],
556 vec![
557 MigrationAction::AddColumn {
558 table: "users".into(),
559 column: col("name", ColumnType::Text),
560 fill_with: None,
561 },
562 MigrationAction::AddIndex {
563 table: "users".into(),
564 index: IndexDef {
565 name: "idx_users_name".into(),
566 columns: vec!["name".into()],
567 unique: false,
568 },
569 },
570 ]
571 )]
572 #[case::drop_table(
573 vec![table(
574 "users",
575 vec![col("id", ColumnType::Integer)],
576 vec![],
577 vec![],
578 )],
579 vec![],
580 vec![MigrationAction::DeleteTable {
581 table: "users".into()
582 }]
583 )]
584 #[case::add_table(
585 vec![],
586 vec![table(
587 "users",
588 vec![col("id", ColumnType::Integer)],
589 vec![],
590 vec![IndexDef {
591 name: "idx_users_id".into(),
592 columns: vec!["id".into()],
593 unique: true,
594 }],
595 )],
596 vec![
597 MigrationAction::CreateTable {
598 table: "users".into(),
599 columns: vec![col("id", ColumnType::Integer)],
600 constraints: vec![],
601 },
602 MigrationAction::AddIndex {
603 table: "users".into(),
604 index: IndexDef {
605 name: "idx_users_id".into(),
606 columns: vec!["id".into()],
607 unique: true,
608 },
609 },
610 ]
611 )]
612 fn diff_schemas_detects_additions(
613 #[case] from_schema: Vec<TableDef>,
614 #[case] to_schema: Vec<TableDef>,
615 #[case] expected_actions: Vec<MigrationAction>,
616 ) {
617 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
618 assert_eq!(plan.actions, expected_actions);
619 }
620
621 #[rstest]
622 fn plan_next_migration_sets_next_version() {
623 let applied = vec![MigrationPlan {
624 comment: None,
625 created_at: None,
626 version: 1,
627 actions: vec![MigrationAction::CreateTable {
628 table: "users".into(),
629 columns: vec![col("id", ColumnType::Integer)],
630 constraints: vec![],
631 }],
632 }];
633
634 let target_schema = vec![table(
635 "users",
636 vec![
637 col("id", ColumnType::Integer),
638 col("name", ColumnType::Text),
639 ],
640 vec![],
641 vec![],
642 )];
643
644 let plan = plan_next_migration(&target_schema, &applied).unwrap();
645 assert_eq!(plan.version, 2);
646 assert!(plan.actions.iter().any(
647 |a| matches!(a, MigrationAction::AddColumn { column, .. } if column.name == "name")
648 ));
649 }
650
651 #[rstest]
652 #[case(
653 vec![table("users", vec![], vec![], vec![])],
654 MigrationAction::CreateTable {
655 table: "users".into(),
656 columns: vec![],
657 constraints: vec![],
658 },
659 ErrKind::TableExists
660 )]
661 #[case(
662 vec![],
663 MigrationAction::DeleteTable {
664 table: "users".into()
665 },
666 ErrKind::TableNotFound
667 )]
668 #[case(
669 vec![table(
670 "users",
671 vec![col("id", ColumnType::Integer)],
672 vec![],
673 vec![]
674 )],
675 MigrationAction::AddColumn {
676 table: "users".into(),
677 column: col("id", ColumnType::Integer),
678 fill_with: None,
679 },
680 ErrKind::ColumnExists
681 )]
682 #[case(
683 vec![table(
684 "users",
685 vec![col("id", ColumnType::Integer)],
686 vec![],
687 vec![]
688 )],
689 MigrationAction::DeleteColumn {
690 table: "users".into(),
691 column: "missing".into()
692 },
693 ErrKind::ColumnNotFound
694 )]
695 #[case(
696 vec![table(
697 "users",
698 vec![col("id", ColumnType::Integer)],
699 vec![],
700 vec![]
701 )],
702 MigrationAction::RemoveIndex {
703 table: "users".into(),
704 name: "idx".into()
705 },
706 ErrKind::IndexNotFound
707 )]
708 fn apply_action_reports_errors(
709 #[case] mut schema: Vec<TableDef>,
710 #[case] action: MigrationAction,
711 #[case] expected: ErrKind,
712 ) {
713 let err = apply_action(&mut schema, &action).unwrap_err();
714 assert_err_kind(err, expected);
715 }
716}