1mod check;
2mod foreign_key;
3mod index;
4mod primary_key;
5#[cfg(test)]
6mod tests {
7 use super::*;
8 use crate::sql::types::DatabaseBackend;
9 use insta::{assert_snapshot, with_settings};
10 use rstest::rstest;
11 use vespertide_core::{
12 ColumnDef, ColumnType, ReferenceAction, SimpleColumnType, TableConstraint, TableDef,
13 };
14 #[rstest]
15 #[case::add_constraint_primary_key_postgres(
16 "add_constraint_primary_key_postgres",
17 DatabaseBackend::Postgres,
18 &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"]
19 )]
20 #[case::add_constraint_primary_key_mysql(
21 "add_constraint_primary_key_mysql",
22 DatabaseBackend::MySql,
23 &["ALTER TABLE `users` ADD PRIMARY KEY (`id`)"]
24 )]
25 #[case::add_constraint_primary_key_sqlite(
26 "add_constraint_primary_key_sqlite",
27 DatabaseBackend::Sqlite,
28 &["CREATE TABLE \"users_temp\""]
29 )]
30 #[case::add_constraint_unique_named_postgres(
31 "add_constraint_unique_named_postgres",
32 DatabaseBackend::Postgres,
33 &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"]
34 )]
35 #[case::add_constraint_unique_named_mysql(
36 "add_constraint_unique_named_mysql",
37 DatabaseBackend::MySql,
38 &["CREATE UNIQUE INDEX `uq_users__uq_email` ON `users` (`email`)"]
39 )]
40 #[case::add_constraint_unique_named_sqlite(
41 "add_constraint_unique_named_sqlite",
42 DatabaseBackend::Sqlite,
43 &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"]
44 )]
45 #[case::add_constraint_foreign_key_postgres(
46 "add_constraint_foreign_key_postgres",
47 DatabaseBackend::Postgres,
48 &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
49 )]
50 #[case::add_constraint_foreign_key_mysql(
51 "add_constraint_foreign_key_mysql",
52 DatabaseBackend::MySql,
53 &["FOREIGN KEY (`user_id`)", "REFERENCES `users` (`id`)", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
54 )]
55 #[case::add_constraint_foreign_key_sqlite(
56 "add_constraint_foreign_key_sqlite",
57 DatabaseBackend::Sqlite,
58 &["CREATE TABLE \"users_temp\""]
59 )]
60 #[case::add_constraint_check_named_postgres(
61 "add_constraint_check_named_postgres",
62 DatabaseBackend::Postgres,
63 &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"]
64 )]
65 #[case::add_constraint_check_named_mysql(
66 "add_constraint_check_named_mysql",
67 DatabaseBackend::MySql,
68 &["ADD CONSTRAINT `chk_age` CHECK (age > 0)"]
69 )]
70 #[case::add_constraint_check_named_sqlite(
71 "add_constraint_check_named_sqlite",
72 DatabaseBackend::Sqlite,
73 &["CREATE TABLE \"users_temp\""]
74 )]
75 fn test_add_constraint(
76 #[case] title: &str,
77 #[case] backend: DatabaseBackend,
78 #[case] expected: &[&str],
79 ) {
80 let constraint = if title.contains("primary_key") {
81 TableConstraint::PrimaryKey {
82 columns: vec!["id".into()],
83 auto_increment: false,
84 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
85 }
86 } else if title.contains("unique") {
87 TableConstraint::Unique {
88 name: Some("uq_email".into()),
89 columns: vec!["email".into()],
90 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
91 keep: vespertide_core::KeepPolicy::First,
92 },
93 }
94 } else if title.contains("foreign_key") {
95 TableConstraint::ForeignKey {
96 name: Some("fk_user".into()),
97 columns: vec!["user_id".into()],
98 ref_table: "users".into(),
99 ref_columns: vec!["id".into()],
100 on_delete: Some(ReferenceAction::Cascade),
101 on_update: Some(ReferenceAction::Restrict),
102 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
103 }
104 } else {
105 TableConstraint::Check {
106 name: "chk_age".into(),
107 expr: "age > 0".into(),
108 strategy: vespertide_core::CheckViolationStrategy::default(),
109 }
110 };
111 let current_schema = vec![TableDef {
112 name: "users".into(),
113 description: None,
114 columns: if title.contains("foreign_key") {
115 vec![
116 ColumnDef {
117 name: "id".into(),
118 r#type: ColumnType::Simple(SimpleColumnType::Integer),
119 nullable: false,
120 default: None,
121 comment: None,
122 primary_key: None,
123 unique: None,
124 index: None,
125 foreign_key: None,
126 },
127 ColumnDef {
128 name: "user_id".into(),
129 r#type: ColumnType::Simple(SimpleColumnType::Integer),
130 nullable: true,
131 default: None,
132 comment: None,
133 primary_key: None,
134 unique: None,
135 index: None,
136 foreign_key: None,
137 },
138 ]
139 } else {
140 vec![
141 ColumnDef {
142 name: "id".into(),
143 r#type: ColumnType::Simple(SimpleColumnType::Integer),
144 nullable: false,
145 default: None,
146 comment: None,
147 primary_key: None,
148 unique: None,
149 index: None,
150 foreign_key: None,
151 },
152 ColumnDef {
153 name: if title.contains("check") {
154 "age".into()
155 } else {
156 "email".into()
157 },
158 r#type: ColumnType::Simple(SimpleColumnType::Text),
159 nullable: true,
160 default: None,
161 comment: None,
162 primary_key: None,
163 unique: None,
164 index: None,
165 foreign_key: None,
166 },
167 ]
168 },
169 constraints: vec![],
170 }];
171 let result =
172 build_add_constraint(backend, "users", &constraint, ¤t_schema, &[]).unwrap();
173 let sql = result
177 .iter()
178 .map(|q| q.build(backend))
179 .collect::<Vec<_>>()
180 .join("\n");
181 for exp in expected {
182 assert!(
183 sql.contains(exp),
184 "Expected SQL to contain '{exp}', got: {sql}"
185 );
186 }
187 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("add_constraint_{}", title) }, {
188 assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::<Vec<String>>().join("\n"));
189 });
190 }
191 #[test]
192 fn test_add_constraint_primary_key_sqlite_table_not_found() {
193 let constraint = TableConstraint::PrimaryKey {
194 columns: vec!["id".into()],
195 auto_increment: false,
196 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
197 };
198 let current_schema = vec![]; let result = build_add_constraint(
200 DatabaseBackend::Sqlite,
201 "users",
202 &constraint,
203 ¤t_schema,
204 &[],
205 );
206 assert!(result.is_err());
207 let err_msg = result.unwrap_err().to_string();
208 assert!(err_msg.contains("Table 'users' not found in current schema"));
209 }
210
211 #[test]
212 fn add_check_constraint_escapes_adversarial_identifiers() {
213 let constraint = TableConstraint::Check {
214 name: "chk_age\"quote".into(),
215 expr: "age > 0".into(),
216 strategy: vespertide_core::CheckViolationStrategy::default(),
217 };
218 let current_schema = vec![TableDef {
219 name: "users\"archive".into(),
220 description: None,
221 columns: vec![ColumnDef {
222 name: "age".into(),
223 r#type: ColumnType::Simple(SimpleColumnType::Integer),
224 nullable: false,
225 default: None,
226 comment: None,
227 primary_key: None,
228 unique: None,
229 index: None,
230 foreign_key: None,
231 }],
232 constraints: vec![],
233 }];
234
235 let pg_results = build_add_constraint(
236 DatabaseBackend::Postgres,
237 "users\"archive",
238 &constraint,
239 ¤t_schema,
240 &[],
241 )
242 .unwrap();
243 let pg_sql = pg_results
248 .iter()
249 .map(|q| q.build(DatabaseBackend::Postgres))
250 .collect::<Vec<_>>()
251 .join("\n");
252 assert!(pg_sql.contains("ALTER TABLE \"users\"\"archive\" ADD CONSTRAINT \"chk_age\"\"quote\" CHECK (age > 0) NOT VALID"), "PG NOT VALID statement missing or mis-escaped, got: {pg_sql}");
253 assert!(
254 pg_sql.contains(
255 "ALTER TABLE \"users\"\"archive\" VALIDATE CONSTRAINT \"chk_age\"\"quote\""
256 ),
257 "PG VALIDATE statement missing or mis-escaped, got: {pg_sql}"
258 );
259
260 let mysql_constraint = TableConstraint::Check {
261 name: "chk_age`quote".into(),
262 expr: "age > 0".into(),
263 strategy: vespertide_core::CheckViolationStrategy::default(),
264 };
265 let mysql_results = build_add_constraint(
266 DatabaseBackend::MySql,
267 "users`archive",
268 &mysql_constraint,
269 ¤t_schema,
270 &[],
271 )
272 .unwrap();
273 let mysql_sql = mysql_results
274 .last()
275 .expect("at least one query emitted")
276 .build(DatabaseBackend::MySql);
277 assert_eq!(
278 mysql_sql,
279 "ALTER TABLE `users``archive` ADD CONSTRAINT `chk_age``quote` CHECK (age > 0)"
280 );
281 }
282
283 #[test]
284 fn test_add_constraint_primary_key_sqlite_with_check_constraints() {
285 let constraint = TableConstraint::PrimaryKey {
286 columns: vec!["id".into()],
287 auto_increment: false,
288 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
289 };
290 let current_schema = vec![TableDef {
291 name: "users".into(),
292 description: None,
293 columns: vec![ColumnDef {
294 name: "id".into(),
295 r#type: ColumnType::Simple(SimpleColumnType::Integer),
296 nullable: false,
297 default: None,
298 comment: None,
299 primary_key: None,
300 unique: None,
301 index: None,
302 foreign_key: None,
303 }],
304 constraints: vec![TableConstraint::Check {
305 name: "chk_id".into(),
306 expr: "id > 0".into(),
307 strategy: vespertide_core::CheckViolationStrategy::default(),
308 }],
309 }];
310 let result = build_add_constraint(
311 DatabaseBackend::Sqlite,
312 "users",
313 &constraint,
314 ¤t_schema,
315 &[],
316 );
317 assert!(result.is_ok());
318 let queries = result.unwrap();
319 let sql = queries
320 .iter()
321 .map(|q| q.build(DatabaseBackend::Sqlite))
322 .collect::<Vec<String>>()
323 .join("\n");
324 assert!(sql.contains("CONSTRAINT \"chk_id\" CHECK"));
325 }
326 #[test]
327 fn test_add_constraint_primary_key_sqlite_with_indexes() {
328 let constraint = TableConstraint::PrimaryKey {
329 columns: vec!["id".into()],
330 auto_increment: false,
331 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
332 };
333 let current_schema = vec![TableDef {
334 name: "users".into(),
335 description: None,
336 columns: vec![ColumnDef {
337 name: "id".into(),
338 r#type: ColumnType::Simple(SimpleColumnType::Integer),
339 nullable: false,
340 default: None,
341 comment: None,
342 primary_key: None,
343 unique: None,
344 index: None,
345 foreign_key: None,
346 }],
347 constraints: vec![TableConstraint::Index {
348 name: Some("idx_id".into()),
349 columns: vec!["id".into()],
350 }],
351 }];
352 let result = build_add_constraint(
353 DatabaseBackend::Sqlite,
354 "users",
355 &constraint,
356 ¤t_schema,
357 &[],
358 );
359 assert!(result.is_ok());
360 let queries = result.unwrap();
361 let sql = queries
362 .iter()
363 .map(|q| q.build(DatabaseBackend::Sqlite))
364 .collect::<Vec<String>>()
365 .join("\n");
366 assert!(sql.contains("CREATE INDEX"));
367 assert!(sql.contains("idx_id"));
368 }
369 #[test]
370 fn test_add_constraint_primary_key_sqlite_with_unique_constraint() {
371 let constraint = TableConstraint::PrimaryKey {
372 columns: vec!["id".into()],
373 auto_increment: false,
374 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
375 };
376 let current_schema = vec![TableDef {
377 name: "users".into(),
378 description: None,
379 columns: vec![ColumnDef {
380 name: "id".into(),
381 r#type: ColumnType::Simple(SimpleColumnType::Integer),
382 nullable: false,
383 default: None,
384 comment: None,
385 primary_key: None,
386 unique: None,
387 index: None,
388 foreign_key: None,
389 }],
390 constraints: vec![TableConstraint::Unique {
391 name: Some("uq_email".into()),
392 columns: vec!["email".into()],
393 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
394 keep: vespertide_core::KeepPolicy::First,
395 },
396 }],
397 }];
398 let result = build_add_constraint(
399 DatabaseBackend::Sqlite,
400 "users",
401 &constraint,
402 ¤t_schema,
403 &[],
404 );
405 assert!(result.is_ok());
406 let queries = result.unwrap();
407 let sql = queries
408 .iter()
409 .map(|q| q.build(DatabaseBackend::Sqlite))
410 .collect::<Vec<String>>()
411 .join("\n");
412 assert!(sql.contains("CREATE TABLE"));
413 }
414 #[test]
415 fn test_add_constraint_check_sqlite_table_not_found() {
416 let constraint = TableConstraint::Check {
417 name: "chk_age".into(),
418 expr: "age > 0".into(),
419 strategy: vespertide_core::CheckViolationStrategy::default(),
420 };
421 let current_schema = vec![]; let result = build_add_constraint(
423 DatabaseBackend::Sqlite,
424 "users",
425 &constraint,
426 ¤t_schema,
427 &[],
428 );
429 assert!(result.is_err());
430 let err_msg = result.unwrap_err().to_string();
431 assert!(err_msg.contains("Table 'users' not found in current schema"));
432 }
433 #[test]
434 fn test_add_constraint_check_sqlite_without_existing_check() {
435 let constraint = TableConstraint::Check {
436 name: "chk_age".into(),
437 expr: "age > 0".into(),
438 strategy: vespertide_core::CheckViolationStrategy::default(),
439 };
440 let current_schema = vec![TableDef {
441 name: "users".into(),
442 description: None,
443 columns: vec![ColumnDef {
444 name: "age".into(),
445 r#type: ColumnType::Simple(SimpleColumnType::Integer),
446 nullable: true,
447 default: None,
448 comment: None,
449 primary_key: None,
450 unique: None,
451 index: None,
452 foreign_key: None,
453 }],
454 constraints: vec![], }];
456 let result = build_add_constraint(
457 DatabaseBackend::Sqlite,
458 "users",
459 &constraint,
460 ¤t_schema,
461 &[],
462 );
463 assert!(result.is_ok());
464 let queries = result.unwrap();
465 let sql = queries
466 .iter()
467 .map(|q| q.build(DatabaseBackend::Sqlite))
468 .collect::<Vec<String>>()
469 .join("\n");
470 assert!(sql.contains("CREATE TABLE"));
471 assert!(sql.contains("CONSTRAINT \"chk_age\" CHECK"));
472 }
473 #[test]
474 fn test_add_constraint_primary_key_sqlite_without_existing_check() {
475 let constraint = TableConstraint::PrimaryKey {
476 columns: vec!["id".into()],
477 auto_increment: false,
478 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
479 };
480 let current_schema = vec![TableDef {
481 name: "users".into(),
482 description: None,
483 columns: vec![ColumnDef {
484 name: "id".into(),
485 r#type: ColumnType::Simple(SimpleColumnType::Integer),
486 nullable: true,
487 default: None,
488 comment: None,
489 primary_key: None,
490 unique: None,
491 index: None,
492 foreign_key: None,
493 }],
494 constraints: vec![], }];
496 let result = build_add_constraint(
497 DatabaseBackend::Sqlite,
498 "users",
499 &constraint,
500 ¤t_schema,
501 &[],
502 );
503 assert!(result.is_ok());
504 let queries = result.unwrap();
505 let sql = queries
506 .iter()
507 .map(|q| q.build(DatabaseBackend::Sqlite))
508 .collect::<Vec<String>>()
509 .join("\n");
510 assert!(sql.contains("CREATE TABLE"));
511 assert!(sql.contains("PRIMARY KEY"));
512 }
513
514 #[test]
515 fn test_add_constraint_check_sqlite_with_indexes() {
516 let constraint = TableConstraint::Check {
517 name: "chk_age".into(),
518 expr: "age > 0".into(),
519 strategy: vespertide_core::CheckViolationStrategy::default(),
520 };
521 let current_schema = vec![TableDef {
522 name: "users".into(),
523 description: None,
524 columns: vec![ColumnDef {
525 name: "age".into(),
526 r#type: ColumnType::Simple(SimpleColumnType::Integer),
527 nullable: true,
528 default: None,
529 comment: None,
530 primary_key: None,
531 unique: None,
532 index: None,
533 foreign_key: None,
534 }],
535 constraints: vec![TableConstraint::Index {
536 name: Some("idx_age".into()),
537 columns: vec!["age".into()],
538 }],
539 }];
540 let result = build_add_constraint(
541 DatabaseBackend::Sqlite,
542 "users",
543 &constraint,
544 ¤t_schema,
545 &[],
546 );
547 assert!(result.is_ok());
548 let queries = result.unwrap();
549 let sql = queries
550 .iter()
551 .map(|q| q.build(DatabaseBackend::Sqlite))
552 .collect::<Vec<String>>()
553 .join("\n");
554 assert!(sql.contains("CREATE INDEX"));
555 assert!(sql.contains("idx_age"));
556 }
557 #[test]
558 fn test_add_constraint_check_sqlite_with_unique_constraint() {
559 let constraint = TableConstraint::Check {
560 name: "chk_age".into(),
561 expr: "age > 0".into(),
562 strategy: vespertide_core::CheckViolationStrategy::default(),
563 };
564 let current_schema = vec![TableDef {
565 name: "users".into(),
566 description: None,
567 columns: vec![ColumnDef {
568 name: "age".into(),
569 r#type: ColumnType::Simple(SimpleColumnType::Integer),
570 nullable: true,
571 default: None,
572 comment: None,
573 primary_key: None,
574 unique: None,
575 index: None,
576 foreign_key: None,
577 }],
578 constraints: vec![TableConstraint::Unique {
579 name: Some("uq_age".into()),
580 columns: vec!["age".into()],
581 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
582 keep: vespertide_core::KeepPolicy::First,
583 },
584 }],
585 }];
586 let result = build_add_constraint(
587 DatabaseBackend::Sqlite,
588 "users",
589 &constraint,
590 ¤t_schema,
591 &[],
592 );
593 assert!(result.is_ok());
594 let queries = result.unwrap();
595 let sql = queries
596 .iter()
597 .map(|q| q.build(DatabaseBackend::Sqlite))
598 .collect::<Vec<String>>()
599 .join("\n");
600 assert!(sql.contains("CREATE TABLE"));
601 }
602 #[test]
603 fn test_add_constraint_composite_primary_key_postgres() {
604 let constraint = TableConstraint::PrimaryKey {
605 columns: vec!["user_id".into(), "role_id".into()],
606 auto_increment: false,
607 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
608 };
609 let current_schema = vec![TableDef {
610 name: "user_roles".into(),
611 description: None,
612 columns: vec![
613 ColumnDef {
614 name: "user_id".into(),
615 r#type: ColumnType::Simple(SimpleColumnType::Integer),
616 nullable: false,
617 default: None,
618 comment: None,
619 primary_key: None,
620 unique: None,
621 index: None,
622 foreign_key: None,
623 },
624 ColumnDef {
625 name: "role_id".into(),
626 r#type: ColumnType::Simple(SimpleColumnType::Integer),
627 nullable: false,
628 default: None,
629 comment: None,
630 primary_key: None,
631 unique: None,
632 index: None,
633 foreign_key: None,
634 },
635 ],
636 constraints: vec![],
637 }];
638 let result = build_add_constraint(
639 DatabaseBackend::Postgres,
640 "user_roles",
641 &constraint,
642 ¤t_schema,
643 &[],
644 )
645 .unwrap();
646 let sql = result[0].build(DatabaseBackend::Postgres);
647 assert!(sql.contains("ADD PRIMARY KEY"));
648 assert!(sql.contains("\"user_id\""));
649 assert!(sql.contains("\"role_id\""));
650 }
651 #[test]
652 fn test_add_constraint_composite_primary_key_mysql() {
653 let constraint = TableConstraint::PrimaryKey {
654 columns: vec!["user_id".into(), "role_id".into()],
655 auto_increment: false,
656 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
657 };
658 let current_schema = vec![TableDef {
659 name: "user_roles".into(),
660 description: None,
661 columns: vec![
662 ColumnDef {
663 name: "user_id".into(),
664 r#type: ColumnType::Simple(SimpleColumnType::Integer),
665 nullable: false,
666 default: None,
667 comment: None,
668 primary_key: None,
669 unique: None,
670 index: None,
671 foreign_key: None,
672 },
673 ColumnDef {
674 name: "role_id".into(),
675 r#type: ColumnType::Simple(SimpleColumnType::Integer),
676 nullable: false,
677 default: None,
678 comment: None,
679 primary_key: None,
680 unique: None,
681 index: None,
682 foreign_key: None,
683 },
684 ],
685 constraints: vec![],
686 }];
687 let result = build_add_constraint(
688 DatabaseBackend::MySql,
689 "user_roles",
690 &constraint,
691 ¤t_schema,
692 &[],
693 )
694 .unwrap();
695 let sql = result[0].build(DatabaseBackend::MySql);
696 assert!(sql.contains("ADD PRIMARY KEY"));
697 assert!(sql.contains("`user_id`"));
698 assert!(sql.contains("`role_id`"));
699 }
700 #[test]
701 fn test_constraints_overlap_primary_key_same_columns() {
702 let a = TableConstraint::PrimaryKey {
703 columns: vec!["id".into()],
704 auto_increment: false,
705 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
706 };
707 let b = TableConstraint::PrimaryKey {
708 columns: vec!["id".into()],
709 auto_increment: true,
710 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
711 };
712 assert!(constraints_overlap(&a, &b));
713 }
714 #[test]
715 fn test_constraints_overlap_primary_key_different_columns() {
716 let a = TableConstraint::PrimaryKey {
717 columns: vec!["id".into()],
718 auto_increment: false,
719 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
720 };
721 let b = TableConstraint::PrimaryKey {
722 columns: vec!["uid".into()],
723 auto_increment: false,
724 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
725 };
726 assert!(!constraints_overlap(&a, &b));
727 }
728 #[test]
729 fn test_constraints_overlap_check_same() {
730 let a = TableConstraint::Check {
731 name: "chk_age".into(),
732 expr: "age > 0".into(),
733 strategy: vespertide_core::CheckViolationStrategy::default(),
734 };
735 let b = TableConstraint::Check {
736 name: "chk_age".into(),
737 expr: "age > 0".into(),
738 strategy: vespertide_core::CheckViolationStrategy::default(),
739 };
740 assert!(constraints_overlap(&a, &b));
741 }
742 #[test]
743 fn test_constraints_overlap_check_different_name() {
744 let a = TableConstraint::Check {
745 name: "chk_age".into(),
746 expr: "age > 0".into(),
747 strategy: vespertide_core::CheckViolationStrategy::default(),
748 };
749 let b = TableConstraint::Check {
750 name: "chk_age2".into(),
751 expr: "age > 0".into(),
752 strategy: vespertide_core::CheckViolationStrategy::default(),
753 };
754 assert!(!constraints_overlap(&a, &b));
755 }
756 #[test]
757 fn test_constraints_overlap_check_different_expr() {
758 let a = TableConstraint::Check {
759 name: "chk_age".into(),
760 expr: "age > 0".into(),
761 strategy: vespertide_core::CheckViolationStrategy::default(),
762 };
763 let b = TableConstraint::Check {
764 name: "chk_age".into(),
765 expr: "age > 10".into(),
766 strategy: vespertide_core::CheckViolationStrategy::default(),
767 };
768 assert!(!constraints_overlap(&a, &b));
769 }
770 #[test]
771 fn test_constraints_overlap_different_variants() {
772 let a = TableConstraint::PrimaryKey {
773 columns: vec!["id".into()],
774 auto_increment: false,
775 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
776 };
777 let b = TableConstraint::Check {
778 name: "chk".into(),
779 expr: "id > 0".into(),
780 strategy: vespertide_core::CheckViolationStrategy::default(),
781 };
782 assert!(!constraints_overlap(&a, &b));
783 }
784 #[test]
785 fn test_constraints_overlap_fk_same_columns() {
786 let a = TableConstraint::ForeignKey {
787 name: None,
788 columns: vec!["user_id".into()],
789 ref_table: "users".into(),
790 ref_columns: vec!["id".into()],
791 on_delete: None,
792 on_update: None,
793 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
794 };
795 let b = TableConstraint::ForeignKey {
796 name: Some("fk".into()),
797 columns: vec!["user_id".into()],
798 ref_table: "other".into(),
799 ref_columns: vec!["oid".into()],
800 on_delete: Some(ReferenceAction::Cascade),
801 on_update: None,
802 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
803 };
804 assert!(constraints_overlap(&a, &b));
805 }
806 #[test]
807 fn test_merge_constraint_replaces_overlapping() {
808 let existing = vec![
809 TableConstraint::PrimaryKey {
810 columns: vec!["id".into()],
811 auto_increment: false,
812 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
813 },
814 TableConstraint::Index {
815 name: None,
816 columns: vec!["email".into()],
817 },
818 ];
819 let new_pk = TableConstraint::PrimaryKey {
820 columns: vec!["id".into()],
821 auto_increment: true,
822 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
823 };
824 let result = merge_constraint(&existing, &new_pk);
825 assert_eq!(result.len(), 2); }
827 #[test]
832 fn merge_constraint_dedups_when_multiple_existing_overlap() {
833 let pk = TableConstraint::PrimaryKey {
834 columns: vec!["id".into()],
835 auto_increment: false,
836 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
837 };
838 let idx = TableConstraint::Index {
839 name: Some("idx_email".into()),
840 columns: vec!["email".into()],
841 };
842 let existing = vec![pk.clone(), idx.clone(), pk.clone()];
843 let new_pk = TableConstraint::PrimaryKey {
844 columns: vec!["id".into()],
845 auto_increment: true,
846 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
847 };
848 let result = merge_constraint(&existing, &new_pk);
849
850 assert_eq!(
853 result.len(),
854 2,
855 "merge_constraint must dedupe (one new PK + one preserved \
856 index); got: {result:?}"
857 );
858
859 let TableConstraint::PrimaryKey { auto_increment, .. } = &result[0] else {
863 panic!(
864 "result[0] must be the replacement PrimaryKey (pushed at \
865 the position of the FIRST overlap), not appended at the \
866 end via the trailing fallback; got: {:?}",
867 result[0]
868 );
869 };
870 assert!(
871 *auto_increment,
872 "the kept PK must be the new one (auto_increment = true); \
873 got: {result:?}"
874 );
875
876 let pk_count = result
879 .iter()
880 .filter(|c| matches!(c, TableConstraint::PrimaryKey { .. }))
881 .count();
882 assert_eq!(
883 pk_count, 1,
884 "exactly one PrimaryKey must remain after merging away two \
885 overlapping existing PKs; got {pk_count} in: {result:?}"
886 );
887
888 assert!(
890 result.iter().any(|c| matches!(
891 c,
892 TableConstraint::Index { name: Some(n), .. } if n == "idx_email"
893 )),
894 "non-overlapping idx_email must be preserved; got: {result:?}"
895 );
896 }
897
898 #[test]
899 fn test_merge_constraint_appends_non_overlapping() {
900 let existing = vec![TableConstraint::Index {
901 name: None,
902 columns: vec!["email".into()],
903 }];
904 let new_pk = TableConstraint::PrimaryKey {
905 columns: vec!["id".into()],
906 auto_increment: false,
907 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
908 };
909 let result = merge_constraint(&existing, &new_pk);
910 assert_eq!(result.len(), 2); }
912 #[test]
913 fn test_extract_check_clauses_with_mixed_constraints() {
914 let constraints = vec![
915 TableConstraint::Check {
916 name: "chk1".into(),
917 expr: "a > 0".into(),
918 strategy: vespertide_core::CheckViolationStrategy::default(),
919 },
920 TableConstraint::PrimaryKey {
921 columns: vec!["id".into()],
922 auto_increment: false,
923 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
924 },
925 TableConstraint::Check {
926 name: "chk2".into(),
927 expr: "b < 100".into(),
928 strategy: vespertide_core::CheckViolationStrategy::default(),
929 },
930 TableConstraint::Unique {
931 name: Some("uq".into()),
932 columns: vec!["email".into()],
933 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
934 keep: vespertide_core::KeepPolicy::First,
935 },
936 },
937 ];
938 let clauses = crate::sql::helpers::extract_check_clauses(&constraints);
939 assert_eq!(clauses.len(), 2);
940 assert!(clauses[0].contains("chk1"));
941 assert!(clauses[1].contains("chk2"));
942 }
943 #[test]
944 fn test_extract_check_clauses_with_no_check_constraints() {
945 let constraints = vec![
946 TableConstraint::PrimaryKey {
947 columns: vec!["id".into()],
948 auto_increment: false,
949 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
950 },
951 TableConstraint::Unique {
952 name: None,
953 columns: vec!["email".into()],
954 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
955 keep: vespertide_core::KeepPolicy::First,
956 },
957 },
958 ];
959 let clauses = crate::sql::helpers::extract_check_clauses(&constraints);
960 assert!(clauses.is_empty());
961 }
962}
963
964mod unique;
965
966use sea_query::{Alias, Query, Table};
967use vespertide_core::{TableConstraint, TableDef};
968
969use super::helpers::{build_sqlite_temp_table_create, recreate_indexes_after_rebuild};
970use super::rename_table::build_rename_table;
971use super::types::{BuiltQuery, DatabaseBackend};
972use crate::error::QueryError;
973
974pub fn build_add_constraint(
975 backend: DatabaseBackend,
976 table: &str,
977 constraint: &TableConstraint,
978 current_schema: &[TableDef],
979 pending_constraints: &[TableConstraint],
980) -> Result<Vec<BuiltQuery>, QueryError> {
981 if let TableConstraint::PrimaryKey {
982 columns, strategy, ..
983 } = constraint
984 {
985 return primary_key::build_primary_key(
986 backend,
987 table,
988 columns,
989 strategy,
990 constraint,
991 current_schema,
992 pending_constraints,
993 );
994 }
995
996 match constraint {
997 TableConstraint::Unique {
998 name,
999 columns,
1000 strategy,
1001 } => unique::build_unique(
1002 backend,
1003 table,
1004 name.as_deref(),
1005 columns,
1006 strategy,
1007 current_schema,
1008 ),
1009 TableConstraint::ForeignKey {
1010 name,
1011 columns,
1012 ref_table,
1013 ref_columns,
1014 on_delete,
1015 on_update,
1016 orphan_strategy,
1017 } => foreign_key::build_foreign_key(
1018 backend,
1019 table,
1020 name.as_deref(),
1021 columns,
1022 ref_table,
1023 ref_columns,
1024 on_delete.as_ref(),
1025 on_update.as_ref(),
1026 *orphan_strategy,
1027 constraint,
1028 current_schema,
1029 pending_constraints,
1030 ),
1031 TableConstraint::Index { name, columns } => {
1032 Ok(index::build_index(table, name.as_deref(), columns))
1033 }
1034 TableConstraint::Check {
1035 name,
1036 expr,
1037 strategy,
1038 } => check::build_check(
1039 backend,
1040 table,
1041 name,
1042 expr,
1043 strategy,
1044 constraint,
1045 current_schema,
1046 pending_constraints,
1047 ),
1048 _ => unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above"),
1049 }
1050}
1051
1052pub(super) fn merge_constraint(
1053 existing: &[TableConstraint],
1054 constraint: &TableConstraint,
1055) -> Vec<TableConstraint> {
1056 let mut out = Vec::with_capacity(existing.len() + 1);
1057 let mut replaced = false;
1058 for c in existing {
1059 if constraints_overlap(c, constraint) {
1060 if !replaced {
1061 out.push(constraint.clone());
1062 replaced = true;
1063 }
1064 } else {
1065 out.push(c.clone());
1066 }
1067 }
1068 if !replaced {
1069 out.push(constraint.clone());
1070 }
1071 out
1072}
1073
1074pub(super) fn constraints_overlap(a: &TableConstraint, b: &TableConstraint) -> bool {
1075 match (a, b) {
1076 (
1077 TableConstraint::ForeignKey {
1078 columns: a_cols, ..
1079 },
1080 TableConstraint::ForeignKey {
1081 columns: b_cols, ..
1082 },
1083 )
1084 | (
1085 TableConstraint::PrimaryKey {
1086 columns: a_cols, ..
1087 },
1088 TableConstraint::PrimaryKey {
1089 columns: b_cols, ..
1090 },
1091 ) => a_cols == b_cols,
1092 (
1093 TableConstraint::Check {
1094 name: a_name,
1095 expr: a_expr,
1096 ..
1097 },
1098 TableConstraint::Check {
1099 name: b_name,
1100 expr: b_expr,
1101 ..
1102 },
1103 ) => a_name == b_name && a_expr == b_expr,
1104 _ => false,
1105 }
1106}
1107
1108pub(super) fn rebuild_sqlite_table_with_added_constraint(
1109 backend: DatabaseBackend,
1110 table: &str,
1111 constraint: &TableConstraint,
1112 current_schema: &[TableDef],
1113 pending_constraints: &[TableConstraint],
1114) -> Result<Vec<BuiltQuery>, QueryError> {
1115 let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::SchemaError(format!("Table '{table}' not found in current schema. SQLite requires current schema information to add constraints.")))?;
1116 let new_constraints = merge_constraint(&table_def.constraints, constraint);
1117 let temp_table = format!("{table}_temp");
1118 let create_query = build_sqlite_temp_table_create(
1119 backend,
1120 &temp_table,
1121 table,
1122 &table_def.columns,
1123 &new_constraints,
1124 );
1125 let column_aliases: Vec<Alias> = table_def
1126 .columns
1127 .iter()
1128 .map(|c| Alias::new(&c.name))
1129 .collect();
1130 let mut select_query = Query::select();
1131 for col_alias in &column_aliases {
1132 select_query.column(col_alias.clone());
1133 }
1134 select_query.from(Alias::new(table));
1135 let insert_stmt = Query::insert()
1136 .into_table(Alias::new(&temp_table))
1137 .columns(column_aliases)
1138 .select_from(select_query)
1139 .unwrap()
1140 .to_owned();
1141 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
1142 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
1143 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
1144 let rename_query = build_rename_table(&temp_table, table);
1145 let index_queries =
1146 recreate_indexes_after_rebuild(table, &table_def.constraints, pending_constraints);
1147 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
1148 queries.extend(index_queries);
1149 Ok(queries)
1150}