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