1use vespertide_core::{MigrationAction, MigrationPlan, TableDef};
2use vespertide_planner::apply_action;
3
4use crate::DatabaseBackend;
5use crate::error::QueryError;
6use crate::sql::BuiltQuery;
7use crate::sql::build_action_queries_with_pending;
8
9pub struct PlanQueries {
10 pub action: MigrationAction,
11 pub postgres: Vec<BuiltQuery>,
12 pub mysql: Vec<BuiltQuery>,
13 pub sqlite: Vec<BuiltQuery>,
14}
15
16fn action_target_table(action: &MigrationAction) -> Option<&str> {
19 match action {
20 MigrationAction::CreateTable { table, .. }
21 | MigrationAction::DeleteTable { table }
22 | MigrationAction::AddColumn { table, .. }
23 | MigrationAction::RenameColumn { table, .. }
24 | MigrationAction::DeleteColumn { table, .. }
25 | MigrationAction::ModifyColumnType { table, .. }
26 | MigrationAction::ModifyColumnNullable { table, .. }
27 | MigrationAction::ModifyColumnDefault { table, .. }
28 | MigrationAction::ModifyColumnComment { table, .. }
29 | MigrationAction::AddConstraint { table, .. }
30 | MigrationAction::RemoveConstraint { table, .. }
31 | MigrationAction::ReplaceConstraint { table, .. } => Some(table),
32 MigrationAction::RenameTable { .. } | MigrationAction::RawSql { .. } => None,
33 }
34}
35
36pub fn build_plan_queries(
37 plan: &MigrationPlan,
38 current_schema: &[TableDef],
39) -> Result<Vec<PlanQueries>, QueryError> {
40 let mut queries: Vec<PlanQueries> = Vec::new();
41 let mut evolving_schema = current_schema.to_vec();
43
44 for (i, action) in plan.actions.iter().enumerate() {
45 let action_table = action_target_table(action);
55 let pending_constraints: Vec<vespertide_core::TableConstraint> =
56 if let Some(table) = action_table {
57 plan.actions[i + 1..]
58 .iter()
59 .filter_map(|a| {
60 if let MigrationAction::AddConstraint {
61 table: t,
62 constraint,
63 } = a
64 {
65 if t == table
66 && matches!(
67 constraint,
68 vespertide_core::TableConstraint::Index { .. }
69 | vespertide_core::TableConstraint::Unique { .. }
70 )
71 {
72 Some(constraint.clone())
73 } else {
74 None
75 }
76 } else {
77 None
78 }
79 })
80 .collect()
81 } else {
82 vec![]
83 };
84
85 let postgres_queries = build_action_queries_with_pending(
87 &DatabaseBackend::Postgres,
88 action,
89 &evolving_schema,
90 &pending_constraints,
91 )?;
92 let mysql_queries = build_action_queries_with_pending(
93 &DatabaseBackend::MySql,
94 action,
95 &evolving_schema,
96 &pending_constraints,
97 )?;
98 let sqlite_queries = build_action_queries_with_pending(
99 &DatabaseBackend::Sqlite,
100 action,
101 &evolving_schema,
102 &pending_constraints,
103 )?;
104 queries.push(PlanQueries {
105 action: action.clone(),
106 postgres: postgres_queries,
107 mysql: mysql_queries,
108 sqlite: sqlite_queries,
109 });
110
111 let _ = apply_action(&mut evolving_schema, action);
116 }
117 Ok(queries)
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::sql::DatabaseBackend;
124 use insta::{assert_snapshot, with_settings};
125 use rstest::rstest;
126 use vespertide_core::{
127 ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType,
128 };
129
130 fn col(name: &str, ty: ColumnType) -> ColumnDef {
131 ColumnDef {
132 name: name.to_string(),
133 r#type: ty,
134 nullable: true,
135 default: None,
136 comment: None,
137 primary_key: None,
138 unique: None,
139 index: None,
140 foreign_key: None,
141 }
142 }
143
144 #[rstest]
145 #[case::empty(
146 MigrationPlan {
147 id: String::new(),
148 comment: None,
149 created_at: None,
150 version: 1,
151 actions: vec![],
152 },
153 0
154 )]
155 #[case::single_action(
156 MigrationPlan {
157 id: String::new(),
158 comment: None,
159 created_at: None,
160 version: 1,
161 actions: vec![MigrationAction::DeleteTable {
162 table: "users".into(),
163 }],
164 },
165 1
166 )]
167 #[case::multiple_actions(
168 MigrationPlan {
169 id: String::new(),
170 comment: None,
171 created_at: None,
172 version: 1,
173 actions: vec![
174 MigrationAction::CreateTable {
175 table: "users".into(),
176 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
177 constraints: vec![],
178 },
179 MigrationAction::DeleteTable {
180 table: "posts".into(),
181 },
182 ],
183 },
184 2
185 )]
186 fn test_build_plan_queries(#[case] plan: MigrationPlan, #[case] expected_count: usize) {
187 let result = build_plan_queries(&plan, &[]).unwrap();
188 assert_eq!(
189 result.len(),
190 expected_count,
191 "Expected {} queries, got {}",
192 expected_count,
193 result.len()
194 );
195 }
196
197 fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
198 result
199 .iter()
200 .map(|q| q.build(backend))
201 .collect::<Vec<_>>()
202 .join(";\n")
203 }
204
205 #[rstest]
210 #[case::postgres("postgres", DatabaseBackend::Postgres)]
211 #[case::mysql("mysql", DatabaseBackend::MySql)]
212 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
213 fn test_delete_column_after_create_table_with_inline_unique(
214 #[case] title: &str,
215 #[case] backend: DatabaseBackend,
216 ) {
217 let mut col_with_unique = col("gift_code", ColumnType::Simple(SimpleColumnType::Text));
218 col_with_unique.unique = Some(vespertide_core::StrOrBoolOrArray::Bool(true));
219
220 let plan = MigrationPlan {
221 id: String::new(),
222 comment: None,
223 created_at: None,
224 version: 1,
225 actions: vec![
226 MigrationAction::CreateTable {
227 table: "gift".into(),
228 columns: vec![
229 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
230 col_with_unique,
231 ],
232 constraints: vec![], },
234 MigrationAction::DeleteColumn {
235 table: "gift".into(),
236 column: "gift_code".into(),
237 },
238 ],
239 };
240
241 let result = build_plan_queries(&plan, &[]).unwrap();
242 let queries = match backend {
243 DatabaseBackend::Postgres => &result[1].postgres,
244 DatabaseBackend::MySql => &result[1].mysql,
245 DatabaseBackend::Sqlite => &result[1].sqlite,
246 };
247 let sql = build_sql_snapshot(queries, backend);
248
249 with_settings!({ snapshot_suffix => format!("inline_unique_{}", title) }, {
250 assert_snapshot!(sql);
251 });
252 }
253
254 #[rstest]
256 #[case::postgres("postgres", DatabaseBackend::Postgres)]
257 #[case::mysql("mysql", DatabaseBackend::MySql)]
258 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
259 fn test_delete_column_after_create_table_with_inline_index(
260 #[case] title: &str,
261 #[case] backend: DatabaseBackend,
262 ) {
263 let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text));
264 col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Bool(true));
265
266 let plan = MigrationPlan {
267 id: String::new(),
268 comment: None,
269 created_at: None,
270 version: 1,
271 actions: vec![
272 MigrationAction::CreateTable {
273 table: "users".into(),
274 columns: vec![
275 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
276 col_with_index,
277 ],
278 constraints: vec![],
279 },
280 MigrationAction::DeleteColumn {
281 table: "users".into(),
282 column: "email".into(),
283 },
284 ],
285 };
286
287 let result = build_plan_queries(&plan, &[]).unwrap();
288 let queries = match backend {
289 DatabaseBackend::Postgres => &result[1].postgres,
290 DatabaseBackend::MySql => &result[1].mysql,
291 DatabaseBackend::Sqlite => &result[1].sqlite,
292 };
293 let sql = build_sql_snapshot(queries, backend);
294
295 with_settings!({ snapshot_suffix => format!("inline_index_{}", title) }, {
296 assert_snapshot!(sql);
297 });
298 }
299
300 #[test]
301 fn test_build_plan_queries_sql_content() {
302 let plan = MigrationPlan {
303 id: String::new(),
304 comment: None,
305 created_at: None,
306 version: 1,
307 actions: vec![
308 MigrationAction::CreateTable {
309 table: "users".into(),
310 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
311 constraints: vec![],
312 },
313 MigrationAction::DeleteTable {
314 table: "posts".into(),
315 },
316 ],
317 };
318
319 let result = build_plan_queries(&plan, &[]).unwrap();
320 assert_eq!(result.len(), 2);
321
322 let sql1 = result[0]
324 .postgres
325 .iter()
326 .map(|q| q.build(DatabaseBackend::Postgres))
327 .collect::<Vec<_>>()
328 .join(";\n");
329 assert!(sql1.contains("CREATE TABLE"));
330 assert!(sql1.contains("\"users\""));
331 assert!(sql1.contains("\"id\""));
332
333 let sql2 = result[1]
334 .postgres
335 .iter()
336 .map(|q| q.build(DatabaseBackend::Postgres))
337 .collect::<Vec<_>>()
338 .join(";\n");
339 assert!(sql2.contains("DROP TABLE"));
340 assert!(sql2.contains("\"posts\""));
341
342 let sql1_mysql = result[0]
344 .mysql
345 .iter()
346 .map(|q| q.build(DatabaseBackend::MySql))
347 .collect::<Vec<_>>()
348 .join(";\n");
349 assert!(sql1_mysql.contains("`users`"));
350
351 let sql2_mysql = result[1]
352 .mysql
353 .iter()
354 .map(|q| q.build(DatabaseBackend::MySql))
355 .collect::<Vec<_>>()
356 .join(";\n");
357 assert!(sql2_mysql.contains("`posts`"));
358 }
359
360 use vespertide_core::{ReferenceAction, TableConstraint};
363
364 fn fk_constraint() -> TableConstraint {
365 TableConstraint::ForeignKey {
366 name: None,
367 columns: vec!["category_id".into()],
368 ref_table: "category".into(),
369 ref_columns: vec!["id".into()],
370 on_delete: Some(ReferenceAction::Cascade),
371 on_update: None,
372 }
373 }
374
375 fn unique_constraint() -> TableConstraint {
376 TableConstraint::Unique {
377 name: None,
378 columns: vec!["category_id".into()],
379 }
380 }
381
382 fn index_constraint() -> TableConstraint {
383 TableConstraint::Index {
384 name: None,
385 columns: vec!["category_id".into()],
386 }
387 }
388
389 fn plan_add_column_with_constraints(order: &[TableConstraint]) -> MigrationPlan {
391 let mut actions: Vec<MigrationAction> = vec![MigrationAction::AddColumn {
392 table: "product".into(),
393 column: Box::new(col(
394 "category_id",
395 ColumnType::Simple(SimpleColumnType::BigInt),
396 )),
397 fill_with: None,
398 }];
399 for c in order {
400 actions.push(MigrationAction::AddConstraint {
401 table: "product".into(),
402 constraint: c.clone(),
403 });
404 }
405 MigrationPlan {
406 id: String::new(),
407 comment: None,
408 created_at: None,
409 version: 1,
410 actions,
411 }
412 }
413
414 fn plan_remove_constraints_then_drop(order: &[TableConstraint]) -> MigrationPlan {
416 let mut actions: Vec<MigrationAction> = Vec::new();
417 for c in order {
418 actions.push(MigrationAction::RemoveConstraint {
419 table: "product".into(),
420 constraint: c.clone(),
421 });
422 }
423 actions.push(MigrationAction::DeleteColumn {
424 table: "product".into(),
425 column: "category_id".into(),
426 });
427 MigrationPlan {
428 id: String::new(),
429 comment: None,
430 created_at: None,
431 version: 1,
432 actions,
433 }
434 }
435
436 fn base_schema_no_constraints() -> Vec<TableDef> {
438 vec![TableDef {
439 name: "product".into(),
440 description: None,
441 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
442 constraints: vec![],
443 }]
444 }
445
446 fn base_schema_with_all_constraints() -> Vec<TableDef> {
448 vec![TableDef {
449 name: "product".into(),
450 description: None,
451 columns: vec![
452 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
453 col("category_id", ColumnType::Simple(SimpleColumnType::BigInt)),
454 ],
455 constraints: vec![fk_constraint(), unique_constraint(), index_constraint()],
456 }]
457 }
458
459 fn collect_all_sql(result: &[PlanQueries], backend: DatabaseBackend) -> String {
461 result
462 .iter()
463 .enumerate()
464 .map(|(i, pq)| {
465 let queries = match backend {
466 DatabaseBackend::Postgres => &pq.postgres,
467 DatabaseBackend::MySql => &pq.mysql,
468 DatabaseBackend::Sqlite => &pq.sqlite,
469 };
470 let sql = build_sql_snapshot(queries, backend);
471 format!("-- Action {}: {:?}\n{}", i, pq.action, sql)
472 })
473 .collect::<Vec<_>>()
474 .join("\n\n")
475 }
476
477 fn assert_no_duplicate_indexes_per_action(result: &[PlanQueries]) {
482 for (i, pq) in result.iter().enumerate() {
483 let stmts: Vec<String> = pq
484 .sqlite
485 .iter()
486 .map(|q| q.build(DatabaseBackend::Sqlite))
487 .collect();
488
489 let index_stmts: Vec<&String> = stmts
490 .iter()
491 .filter(|s| s.contains("CREATE INDEX") || s.contains("CREATE UNIQUE INDEX"))
492 .collect();
493
494 let mut seen = std::collections::HashSet::new();
495 for stmt in &index_stmts {
496 assert!(
497 seen.insert(stmt.as_str()),
498 "Duplicate index within action {} ({:?}):\n {}\nAll index statements in this action:\n{}",
499 i,
500 pq.action,
501 stmt,
502 index_stmts
503 .iter()
504 .map(|s| format!(" {}", s))
505 .collect::<Vec<_>>()
506 .join("\n")
507 );
508 }
509 }
510 }
511
512 fn assert_no_orphan_duplicate_indexes(result: &[PlanQueries]) {
517 let mut live_indexes: std::collections::HashSet<String> = std::collections::HashSet::new();
520
521 for pq in result {
522 let stmts: Vec<String> = pq
523 .sqlite
524 .iter()
525 .map(|q| q.build(DatabaseBackend::Sqlite))
526 .collect();
527
528 if stmts.iter().any(|s| s.starts_with("DROP TABLE")) {
530 live_indexes.clear();
531 }
532
533 for stmt in &stmts {
534 if stmt.contains("CREATE INDEX") || stmt.contains("CREATE UNIQUE INDEX") {
535 assert!(
536 live_indexes.insert(stmt.clone()),
537 "Index would already exist when action {:?} tries to create it:\n {}\nCurrently live indexes:\n{}",
538 pq.action,
539 stmt,
540 live_indexes
541 .iter()
542 .map(|s| format!(" {}", s))
543 .collect::<Vec<_>>()
544 .join("\n")
545 );
546 }
547 }
548
549 for stmt in &stmts {
551 if stmt.starts_with("DROP INDEX") {
552 live_indexes.retain(|s| {
553 let drop_name = stmt
555 .strip_prefix("DROP INDEX \"")
556 .and_then(|s| s.strip_suffix('"'));
557 if let Some(name) = drop_name {
558 !s.contains(&format!("\"{}\"", name))
559 } else {
560 true
561 }
562 });
563 }
564 }
565 }
566 }
567
568 #[rstest]
571 #[case::fk_unique_index("fk_uq_ix", &[fk_constraint(), unique_constraint(), index_constraint()])]
572 #[case::fk_index_unique("fk_ix_uq", &[fk_constraint(), index_constraint(), unique_constraint()])]
573 #[case::unique_fk_index("uq_fk_ix", &[unique_constraint(), fk_constraint(), index_constraint()])]
574 #[case::unique_index_fk("uq_ix_fk", &[unique_constraint(), index_constraint(), fk_constraint()])]
575 #[case::index_fk_unique("ix_fk_uq", &[index_constraint(), fk_constraint(), unique_constraint()])]
576 #[case::index_unique_fk("ix_uq_fk", &[index_constraint(), unique_constraint(), fk_constraint()])]
577 fn test_add_column_with_fk_unique_index_all_orderings(
578 #[case] title: &str,
579 #[case] order: &[TableConstraint],
580 ) {
581 let plan = plan_add_column_with_constraints(order);
582 let schema = base_schema_no_constraints();
583 let result = build_plan_queries(&plan, &schema).unwrap();
584
585 assert_no_duplicate_indexes_per_action(&result);
587 assert_no_orphan_duplicate_indexes(&result);
588
589 for (backend, label) in [
591 (DatabaseBackend::Postgres, "postgres"),
592 (DatabaseBackend::MySql, "mysql"),
593 (DatabaseBackend::Sqlite, "sqlite"),
594 ] {
595 let sql = collect_all_sql(&result, backend);
596 with_settings!({ snapshot_suffix => format!("add_col_{}_{}", title, label) }, {
597 assert_snapshot!(sql);
598 });
599 }
600 }
601
602 #[rstest]
605 #[case::fk_unique_index("fk_uq_ix", &[fk_constraint(), unique_constraint(), index_constraint()])]
606 #[case::fk_index_unique("fk_ix_uq", &[fk_constraint(), index_constraint(), unique_constraint()])]
607 #[case::unique_fk_index("uq_fk_ix", &[unique_constraint(), fk_constraint(), index_constraint()])]
608 #[case::unique_index_fk("uq_ix_fk", &[unique_constraint(), index_constraint(), fk_constraint()])]
609 #[case::index_fk_unique("ix_fk_uq", &[index_constraint(), fk_constraint(), unique_constraint()])]
610 #[case::index_unique_fk("ix_uq_fk", &[index_constraint(), unique_constraint(), fk_constraint()])]
611 fn test_remove_fk_unique_index_then_drop_column_all_orderings(
612 #[case] title: &str,
613 #[case] order: &[TableConstraint],
614 ) {
615 let plan = plan_remove_constraints_then_drop(order);
616 let schema = base_schema_with_all_constraints();
617 let result = build_plan_queries(&plan, &schema).unwrap();
618
619 for (backend, label) in [
621 (DatabaseBackend::Postgres, "postgres"),
622 (DatabaseBackend::MySql, "mysql"),
623 (DatabaseBackend::Sqlite, "sqlite"),
624 ] {
625 let sql = collect_all_sql(&result, backend);
626 with_settings!({ snapshot_suffix => format!("rm_col_{}_{}", title, label) }, {
627 assert_snapshot!(sql);
628 });
629 }
630 }
631
632 #[rstest]
635 #[case::fk_then_index("fk_ix", &[fk_constraint(), index_constraint()])]
636 #[case::index_then_fk("ix_fk", &[index_constraint(), fk_constraint()])]
637 fn test_add_column_with_fk_and_index_pair(
638 #[case] title: &str,
639 #[case] order: &[TableConstraint],
640 ) {
641 let plan = plan_add_column_with_constraints(order);
642 let schema = base_schema_no_constraints();
643 let result = build_plan_queries(&plan, &schema).unwrap();
644
645 assert_no_duplicate_indexes_per_action(&result);
646 assert_no_orphan_duplicate_indexes(&result);
647
648 for (backend, label) in [
649 (DatabaseBackend::Postgres, "postgres"),
650 (DatabaseBackend::MySql, "mysql"),
651 (DatabaseBackend::Sqlite, "sqlite"),
652 ] {
653 let sql = collect_all_sql(&result, backend);
654 with_settings!({ snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
655 assert_snapshot!(sql);
656 });
657 }
658 }
659
660 #[rstest]
663 #[case::fk_then_unique("fk_uq", &[fk_constraint(), unique_constraint()])]
664 #[case::unique_then_fk("uq_fk", &[unique_constraint(), fk_constraint()])]
665 fn test_add_column_with_fk_and_unique_pair(
666 #[case] title: &str,
667 #[case] order: &[TableConstraint],
668 ) {
669 let plan = plan_add_column_with_constraints(order);
670 let schema = base_schema_no_constraints();
671 let result = build_plan_queries(&plan, &schema).unwrap();
672
673 assert_no_duplicate_indexes_per_action(&result);
674 assert_no_orphan_duplicate_indexes(&result);
675
676 for (backend, label) in [
677 (DatabaseBackend::Postgres, "postgres"),
678 (DatabaseBackend::MySql, "mysql"),
679 (DatabaseBackend::Sqlite, "sqlite"),
680 ] {
681 let sql = collect_all_sql(&result, backend);
682 with_settings!({ snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
683 assert_snapshot!(sql);
684 });
685 }
686 }
687
688 #[rstest]
695 #[case::postgres("postgres", DatabaseBackend::Postgres)]
696 #[case::mysql("mysql", DatabaseBackend::MySql)]
697 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
698 fn test_add_column_with_fk_no_duplicate_fk_in_temp_table(
699 #[case] label: &str,
700 #[case] backend: DatabaseBackend,
701 ) {
702 let schema = vec![
703 TableDef {
704 name: "project".into(),
705 description: None,
706 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
707 constraints: vec![],
708 },
709 TableDef {
710 name: "companion".into(),
711 description: None,
712 columns: vec![
713 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
714 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
715 ],
716 constraints: vec![
717 TableConstraint::ForeignKey {
718 name: None,
719 columns: vec!["user_id".into()],
720 ref_table: "user".into(),
721 ref_columns: vec!["id".into()],
722 on_delete: Some(ReferenceAction::Cascade),
723 on_update: None,
724 },
725 TableConstraint::Unique {
726 name: Some("invite_code".into()),
727 columns: vec!["invite_code".into()],
728 },
729 TableConstraint::Index {
730 name: None,
731 columns: vec!["user_id".into()],
732 },
733 ],
734 },
735 ];
736
737 let plan = MigrationPlan {
738 id: String::new(),
739 comment: None,
740 created_at: None,
741 version: 1,
742 actions: vec![
743 MigrationAction::AddColumn {
744 table: "companion".into(),
745 column: Box::new(ColumnDef {
746 name: "project_id".into(),
747 r#type: ColumnType::Simple(SimpleColumnType::BigInt),
748 nullable: false,
749 default: None,
750 comment: None,
751 primary_key: None,
752 unique: None,
753 index: None,
754 foreign_key: Some(
755 vespertide_core::schema::foreign_key::ForeignKeySyntax::String(
756 "project.id".into(),
757 ),
758 ),
759 }),
760 fill_with: None,
761 },
762 MigrationAction::AddConstraint {
763 table: "companion".into(),
764 constraint: TableConstraint::ForeignKey {
765 name: None,
766 columns: vec!["project_id".into()],
767 ref_table: "project".into(),
768 ref_columns: vec!["id".into()],
769 on_delete: Some(ReferenceAction::Cascade),
770 on_update: None,
771 },
772 },
773 MigrationAction::AddConstraint {
774 table: "companion".into(),
775 constraint: TableConstraint::Index {
776 name: None,
777 columns: vec!["project_id".into()],
778 },
779 },
780 ],
781 };
782
783 let result = build_plan_queries(&plan, &schema).unwrap();
784
785 assert_no_duplicate_indexes_per_action(&result);
786 assert_no_orphan_duplicate_indexes(&result);
787
788 let sql = collect_all_sql(&result, backend);
789 with_settings!({ snapshot_suffix => format!("dup_fk_{}", label) }, {
790 assert_snapshot!(sql);
791 });
792 }
793
794 #[rstest]
803 #[case::postgres("postgres", DatabaseBackend::Postgres)]
804 #[case::mysql("mysql", DatabaseBackend::MySql)]
805 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
806 fn test_two_not_null_add_columns_with_inline_index_no_duplicate(
807 #[case] label: &str,
808 #[case] backend: DatabaseBackend,
809 ) {
810 use vespertide_core::DefaultValue;
811 use vespertide_core::schema::str_or_bool::StrOrBoolOrArray;
812
813 let schema = vec![TableDef {
814 name: "article".into(),
815 description: None,
816 columns: vec![
817 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
818 col("title", ColumnType::Simple(SimpleColumnType::Text)),
819 ],
820 constraints: vec![],
821 }];
822
823 let plan = MigrationPlan {
824 id: String::new(),
825 comment: None,
826 created_at: None,
827 version: 1,
828 actions: vec![
829 MigrationAction::AddColumn {
831 table: "article".into(),
832 column: Box::new(ColumnDef {
833 name: "category_pinned".into(),
834 r#type: ColumnType::Simple(SimpleColumnType::Boolean),
835 nullable: false,
836 default: Some(DefaultValue::Bool(false)),
837 comment: None,
838 primary_key: None,
839 unique: None,
840 index: Some(StrOrBoolOrArray::Bool(true)),
841 foreign_key: None,
842 }),
843 fill_with: None,
844 },
845 MigrationAction::AddColumn {
847 table: "article".into(),
848 column: Box::new(ColumnDef {
849 name: "main_pinned".into(),
850 r#type: ColumnType::Simple(SimpleColumnType::Boolean),
851 nullable: false,
852 default: Some(DefaultValue::Bool(false)),
853 comment: None,
854 primary_key: None,
855 unique: None,
856 index: Some(StrOrBoolOrArray::Bool(true)),
857 foreign_key: None,
858 }),
859 fill_with: None,
860 },
861 MigrationAction::AddConstraint {
863 table: "article".into(),
864 constraint: TableConstraint::Index {
865 name: None,
866 columns: vec!["main_pinned".into()],
867 },
868 },
869 MigrationAction::AddConstraint {
871 table: "article".into(),
872 constraint: TableConstraint::Index {
873 name: None,
874 columns: vec!["category_pinned".into()],
875 },
876 },
877 ],
878 };
879
880 let result = build_plan_queries(&plan, &schema).unwrap();
881
882 assert_no_duplicate_indexes_per_action(&result);
884 assert_no_orphan_duplicate_indexes(&result);
885
886 let sql = collect_all_sql(&result, backend);
887 with_settings!({ snapshot_suffix => format!("two_not_null_inline_index_{}", label) }, {
888 assert_snapshot!(sql);
889 });
890 }
891}