1use sea_query::{Alias, ForeignKey, Index, Query, Table};
2
3use vespertide_core::{TableConstraint, TableDef};
4
5use super::helpers::{
6 build_sqlite_temp_table_create, recreate_indexes_after_rebuild, to_sea_fk_action,
7};
8
9fn merge_constraint(
15 existing: &[TableConstraint],
16 constraint: &TableConstraint,
17) -> Vec<TableConstraint> {
18 let mut out: Vec<TableConstraint> = Vec::with_capacity(existing.len() + 1);
19 let mut replaced = false;
20
21 for c in existing {
22 if constraints_overlap(c, constraint) {
23 if !replaced {
25 out.push(constraint.clone());
26 replaced = true;
27 }
28 } else {
30 out.push(c.clone());
31 }
32 }
33
34 if !replaced {
35 out.push(constraint.clone());
36 }
37 out
38}
39
40fn constraints_overlap(a: &TableConstraint, b: &TableConstraint) -> bool {
43 match (a, b) {
44 (
45 TableConstraint::ForeignKey {
46 columns: a_cols, ..
47 },
48 TableConstraint::ForeignKey {
49 columns: b_cols, ..
50 },
51 ) => a_cols == b_cols,
52 (
53 TableConstraint::PrimaryKey {
54 columns: a_cols, ..
55 },
56 TableConstraint::PrimaryKey {
57 columns: b_cols, ..
58 },
59 ) => a_cols == b_cols,
60 (
61 TableConstraint::Check {
62 name: a_name,
63 expr: a_expr,
64 },
65 TableConstraint::Check {
66 name: b_name,
67 expr: b_expr,
68 },
69 ) => a_name == b_name && a_expr == b_expr,
70 _ => false,
71 }
72}
73use super::rename_table::build_rename_table;
74use super::types::{BuiltQuery, DatabaseBackend, RawSql};
75use crate::error::QueryError;
76
77pub fn build_add_constraint(
78 backend: &DatabaseBackend,
79 table: &str,
80 constraint: &TableConstraint,
81 current_schema: &[TableDef],
82 pending_constraints: &[TableConstraint],
83) -> Result<Vec<BuiltQuery>, QueryError> {
84 match constraint {
85 TableConstraint::PrimaryKey { columns, .. } => {
86 if *backend == DatabaseBackend::Sqlite {
87 let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", table)))?;
90
91 let new_constraints = merge_constraint(&table_def.constraints, constraint);
93
94 let temp_table = format!("{}_temp", table);
95
96 let create_query = build_sqlite_temp_table_create(
98 backend,
99 &temp_table,
100 table,
101 &table_def.columns,
102 &new_constraints,
103 );
104
105 let column_aliases: Vec<Alias> = table_def
107 .columns
108 .iter()
109 .map(|c| Alias::new(&c.name))
110 .collect();
111 let mut select_query = Query::select();
112 for col_alias in &column_aliases {
113 select_query = select_query.column(col_alias.clone()).to_owned();
114 }
115 select_query = select_query.from(Alias::new(table)).to_owned();
116
117 let insert_stmt = Query::insert()
118 .into_table(Alias::new(&temp_table))
119 .columns(column_aliases.clone())
120 .select_from(select_query)
121 .unwrap()
122 .to_owned();
123 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
124
125 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
127 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
128
129 let rename_query = build_rename_table(&temp_table, table);
131
132 let index_queries = recreate_indexes_after_rebuild(
135 table,
136 &table_def.constraints,
137 pending_constraints,
138 );
139
140 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
141 queries.extend(index_queries);
142 Ok(queries)
143 } else {
144 let pg_cols = columns
146 .iter()
147 .map(|c| format!("\"{}\"", c))
148 .collect::<Vec<_>>()
149 .join(", ");
150 let mysql_cols = columns
151 .iter()
152 .map(|c| format!("`{}`", c))
153 .collect::<Vec<_>>()
154 .join(", ");
155 let pg_sql = format!("ALTER TABLE \"{}\" ADD PRIMARY KEY ({})", table, pg_cols);
156 let mysql_sql = format!("ALTER TABLE `{}` ADD PRIMARY KEY ({})", table, mysql_cols);
157 Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
158 pg_sql.clone(),
159 mysql_sql,
160 pg_sql,
161 ))])
162 }
163 }
164 TableConstraint::Unique { name, columns } => {
165 let index_name =
167 super::helpers::build_unique_constraint_name(table, columns, name.as_deref());
168 let mut idx = Index::create()
169 .table(Alias::new(table))
170 .name(&index_name)
171 .unique()
172 .to_owned();
173 for col in columns {
174 idx = idx.col(Alias::new(col)).to_owned();
175 }
176 Ok(vec![BuiltQuery::CreateIndex(Box::new(idx))])
177 }
178 TableConstraint::ForeignKey {
179 name,
180 columns,
181 ref_table,
182 ref_columns,
183 on_delete,
184 on_update,
185 } => {
186 if *backend == DatabaseBackend::Sqlite {
188 let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", table)))?;
190
191 let new_constraints = merge_constraint(&table_def.constraints, constraint);
193
194 let temp_table = format!("{}_temp", table);
195
196 let create_query = build_sqlite_temp_table_create(
198 backend,
199 &temp_table,
200 table,
201 &table_def.columns,
202 &new_constraints,
203 );
204
205 let column_aliases: Vec<Alias> = table_def
207 .columns
208 .iter()
209 .map(|c| Alias::new(&c.name))
210 .collect();
211 let mut select_query = Query::select();
212 for col_alias in &column_aliases {
213 select_query = select_query.column(col_alias.clone()).to_owned();
214 }
215 select_query = select_query.from(Alias::new(table)).to_owned();
216
217 let insert_stmt = Query::insert()
218 .into_table(Alias::new(&temp_table))
219 .columns(column_aliases.clone())
220 .select_from(select_query)
221 .unwrap()
222 .to_owned();
223 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
224
225 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
227 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
228
229 let rename_query = build_rename_table(&temp_table, table);
231
232 let index_queries = recreate_indexes_after_rebuild(
235 table,
236 &table_def.constraints,
237 pending_constraints,
238 );
239
240 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
241 queries.extend(index_queries);
242 Ok(queries)
243 } else {
244 let fk_name =
246 vespertide_naming::build_foreign_key_name(table, columns, name.as_deref());
247 let mut fk = ForeignKey::create();
248 fk = fk.name(&fk_name).to_owned();
249 fk = fk.from_tbl(Alias::new(table)).to_owned();
250 for col in columns {
251 fk = fk.from_col(Alias::new(col)).to_owned();
252 }
253 fk = fk.to_tbl(Alias::new(ref_table)).to_owned();
254 for col in ref_columns {
255 fk = fk.to_col(Alias::new(col)).to_owned();
256 }
257 if let Some(action) = on_delete {
258 fk = fk.on_delete(to_sea_fk_action(action)).to_owned();
259 }
260 if let Some(action) = on_update {
261 fk = fk.on_update(to_sea_fk_action(action)).to_owned();
262 }
263 Ok(vec![BuiltQuery::CreateForeignKey(Box::new(fk))])
264 }
265 }
266 TableConstraint::Index { name, columns } => {
267 let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref());
268 let mut idx = Index::create()
269 .table(Alias::new(table))
270 .name(&index_name)
271 .to_owned();
272 for col in columns {
273 idx = idx.col(Alias::new(col)).to_owned();
274 }
275 Ok(vec![BuiltQuery::CreateIndex(Box::new(idx))])
276 }
277 TableConstraint::Check { name, expr } => {
278 if *backend == DatabaseBackend::Sqlite {
280 let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", table)))?;
282
283 let new_constraints = merge_constraint(&table_def.constraints, constraint);
285
286 let temp_table = format!("{}_temp", table);
287
288 let create_query = build_sqlite_temp_table_create(
290 backend,
291 &temp_table,
292 table,
293 &table_def.columns,
294 &new_constraints,
295 );
296
297 let column_aliases: Vec<Alias> = table_def
299 .columns
300 .iter()
301 .map(|c| Alias::new(&c.name))
302 .collect();
303 let mut select_query = Query::select();
304 for col_alias in &column_aliases {
305 select_query = select_query.column(col_alias.clone()).to_owned();
306 }
307 select_query = select_query.from(Alias::new(table)).to_owned();
308
309 let insert_stmt = Query::insert()
310 .into_table(Alias::new(&temp_table))
311 .columns(column_aliases.clone())
312 .select_from(select_query)
313 .unwrap()
314 .to_owned();
315 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
316
317 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
319 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
320
321 let rename_query = build_rename_table(&temp_table, table);
323
324 let index_queries = recreate_indexes_after_rebuild(
327 table,
328 &table_def.constraints,
329 pending_constraints,
330 );
331
332 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
333 queries.extend(index_queries);
334 Ok(queries)
335 } else {
336 let pg_sql = format!(
337 "ALTER TABLE \"{}\" ADD CONSTRAINT \"{}\" CHECK ({})",
338 table, name, expr
339 );
340 let mysql_sql = format!(
341 "ALTER TABLE `{}` ADD CONSTRAINT `{}` CHECK ({})",
342 table, name, expr
343 );
344 Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
345 pg_sql.clone(),
346 mysql_sql,
347 pg_sql,
348 ))])
349 }
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::sql::types::DatabaseBackend;
358 use insta::{assert_snapshot, with_settings};
359 use rstest::rstest;
360 use vespertide_core::{
361 ColumnDef, ColumnType, ReferenceAction, SimpleColumnType, TableConstraint, TableDef,
362 };
363
364 #[rstest]
365 #[case::add_constraint_primary_key_postgres(
366 "add_constraint_primary_key_postgres",
367 DatabaseBackend::Postgres,
368 &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"]
369 )]
370 #[case::add_constraint_primary_key_mysql(
371 "add_constraint_primary_key_mysql",
372 DatabaseBackend::MySql,
373 &["ALTER TABLE `users` ADD PRIMARY KEY (`id`)"]
374 )]
375 #[case::add_constraint_primary_key_sqlite(
376 "add_constraint_primary_key_sqlite",
377 DatabaseBackend::Sqlite,
378 &["CREATE TABLE \"users_temp\""]
379 )]
380 #[case::add_constraint_unique_named_postgres(
381 "add_constraint_unique_named_postgres",
382 DatabaseBackend::Postgres,
383 &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"]
384 )]
385 #[case::add_constraint_unique_named_mysql(
386 "add_constraint_unique_named_mysql",
387 DatabaseBackend::MySql,
388 &["CREATE UNIQUE INDEX `uq_users__uq_email` ON `users` (`email`)"]
389 )]
390 #[case::add_constraint_unique_named_sqlite(
391 "add_constraint_unique_named_sqlite",
392 DatabaseBackend::Sqlite,
393 &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"]
394 )]
395 #[case::add_constraint_foreign_key_postgres(
396 "add_constraint_foreign_key_postgres",
397 DatabaseBackend::Postgres,
398 &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
399 )]
400 #[case::add_constraint_foreign_key_mysql(
401 "add_constraint_foreign_key_mysql",
402 DatabaseBackend::MySql,
403 &["FOREIGN KEY (`user_id`)", "REFERENCES `users` (`id`)", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
404 )]
405 #[case::add_constraint_foreign_key_sqlite(
406 "add_constraint_foreign_key_sqlite",
407 DatabaseBackend::Sqlite,
408 &["CREATE TABLE \"users_temp\""]
409 )]
410 #[case::add_constraint_check_named_postgres(
411 "add_constraint_check_named_postgres",
412 DatabaseBackend::Postgres,
413 &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"]
414 )]
415 #[case::add_constraint_check_named_mysql(
416 "add_constraint_check_named_mysql",
417 DatabaseBackend::MySql,
418 &["ADD CONSTRAINT `chk_age` CHECK (age > 0)"]
419 )]
420 #[case::add_constraint_check_named_sqlite(
421 "add_constraint_check_named_sqlite",
422 DatabaseBackend::Sqlite,
423 &["CREATE TABLE \"users_temp\""]
424 )]
425 fn test_add_constraint(
426 #[case] title: &str,
427 #[case] backend: DatabaseBackend,
428 #[case] expected: &[&str],
429 ) {
430 let constraint = if title.contains("primary_key") {
431 TableConstraint::PrimaryKey {
432 columns: vec!["id".into()],
433 auto_increment: false,
434 }
435 } else if title.contains("unique") {
436 TableConstraint::Unique {
437 name: Some("uq_email".into()),
438 columns: vec!["email".into()],
439 }
440 } else if title.contains("foreign_key") {
441 TableConstraint::ForeignKey {
442 name: Some("fk_user".into()),
443 columns: vec!["user_id".into()],
444 ref_table: "users".into(),
445 ref_columns: vec!["id".into()],
446 on_delete: Some(ReferenceAction::Cascade),
447 on_update: Some(ReferenceAction::Restrict),
448 }
449 } else {
450 TableConstraint::Check {
451 name: "chk_age".into(),
452 expr: "age > 0".into(),
453 }
454 };
455
456 let current_schema = vec![TableDef {
458 name: "users".into(),
459 description: None,
460 columns: if title.contains("foreign_key") {
461 vec![
462 ColumnDef {
463 name: "id".into(),
464 r#type: ColumnType::Simple(SimpleColumnType::Integer),
465 nullable: false,
466 default: None,
467 comment: None,
468 primary_key: None,
469 unique: None,
470 index: None,
471 foreign_key: None,
472 },
473 ColumnDef {
474 name: "user_id".into(),
475 r#type: ColumnType::Simple(SimpleColumnType::Integer),
476 nullable: true,
477 default: None,
478 comment: None,
479 primary_key: None,
480 unique: None,
481 index: None,
482 foreign_key: None,
483 },
484 ]
485 } else {
486 vec![
487 ColumnDef {
488 name: "id".into(),
489 r#type: ColumnType::Simple(SimpleColumnType::Integer),
490 nullable: false,
491 default: None,
492 comment: None,
493 primary_key: None,
494 unique: None,
495 index: None,
496 foreign_key: None,
497 },
498 ColumnDef {
499 name: if title.contains("check") {
500 "age".into()
501 } else {
502 "email".into()
503 },
504 r#type: ColumnType::Simple(SimpleColumnType::Text),
505 nullable: true,
506 default: None,
507 comment: None,
508 primary_key: None,
509 unique: None,
510 index: None,
511 foreign_key: None,
512 },
513 ]
514 },
515 constraints: vec![],
516 }];
517
518 let result =
519 build_add_constraint(&backend, "users", &constraint, ¤t_schema, &[]).unwrap();
520 let sql = result[0].build(backend);
521 for exp in expected {
522 assert!(
523 sql.contains(exp),
524 "Expected SQL to contain '{}', got: {}",
525 exp,
526 sql
527 );
528 }
529
530 with_settings!({ snapshot_suffix => format!("add_constraint_{}", title) }, {
531 assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::<Vec<String>>().join("\n"));
532 });
533 }
534
535 #[test]
536 fn test_add_constraint_primary_key_sqlite_table_not_found() {
537 let constraint = TableConstraint::PrimaryKey {
538 columns: vec!["id".into()],
539 auto_increment: false,
540 };
541 let current_schema = vec![]; let result = build_add_constraint(
543 &DatabaseBackend::Sqlite,
544 "users",
545 &constraint,
546 ¤t_schema,
547 &[],
548 );
549 assert!(result.is_err());
550 let err_msg = result.unwrap_err().to_string();
551 assert!(err_msg.contains("Table 'users' not found in current schema"));
552 }
553
554 #[test]
555 fn test_add_constraint_primary_key_sqlite_with_check_constraints() {
556 let constraint = TableConstraint::PrimaryKey {
557 columns: vec!["id".into()],
558 auto_increment: false,
559 };
560 let current_schema = vec![TableDef {
561 name: "users".into(),
562 description: None,
563 columns: vec![ColumnDef {
564 name: "id".into(),
565 r#type: ColumnType::Simple(SimpleColumnType::Integer),
566 nullable: false,
567 default: None,
568 comment: None,
569 primary_key: None,
570 unique: None,
571 index: None,
572 foreign_key: None,
573 }],
574 constraints: vec![TableConstraint::Check {
575 name: "chk_id".into(),
576 expr: "id > 0".into(),
577 }],
578 }];
579 let result = build_add_constraint(
580 &DatabaseBackend::Sqlite,
581 "users",
582 &constraint,
583 ¤t_schema,
584 &[],
585 );
586 assert!(result.is_ok());
587 let queries = result.unwrap();
588 let sql = queries
589 .iter()
590 .map(|q| q.build(DatabaseBackend::Sqlite))
591 .collect::<Vec<String>>()
592 .join("\n");
593 assert!(sql.contains("CONSTRAINT \"chk_id\" CHECK"));
595 }
596
597 #[test]
598 fn test_add_constraint_primary_key_sqlite_with_indexes() {
599 let constraint = TableConstraint::PrimaryKey {
600 columns: vec!["id".into()],
601 auto_increment: false,
602 };
603 let current_schema = vec![TableDef {
604 name: "users".into(),
605 description: None,
606 columns: vec![ColumnDef {
607 name: "id".into(),
608 r#type: ColumnType::Simple(SimpleColumnType::Integer),
609 nullable: false,
610 default: None,
611 comment: None,
612 primary_key: None,
613 unique: None,
614 index: None,
615 foreign_key: None,
616 }],
617 constraints: vec![TableConstraint::Index {
618 name: Some("idx_id".into()),
619 columns: vec!["id".into()],
620 }],
621 }];
622 let result = build_add_constraint(
623 &DatabaseBackend::Sqlite,
624 "users",
625 &constraint,
626 ¤t_schema,
627 &[],
628 );
629 assert!(result.is_ok());
630 let queries = result.unwrap();
631 let sql = queries
632 .iter()
633 .map(|q| q.build(DatabaseBackend::Sqlite))
634 .collect::<Vec<String>>()
635 .join("\n");
636 assert!(sql.contains("CREATE INDEX"));
638 assert!(sql.contains("idx_id"));
639 }
640
641 #[test]
642 fn test_add_constraint_primary_key_sqlite_with_unique_constraint() {
643 let constraint = TableConstraint::PrimaryKey {
646 columns: vec!["id".into()],
647 auto_increment: false,
648 };
649 let current_schema = vec![TableDef {
650 name: "users".into(),
651 description: None,
652 columns: vec![ColumnDef {
653 name: "id".into(),
654 r#type: ColumnType::Simple(SimpleColumnType::Integer),
655 nullable: false,
656 default: None,
657 comment: None,
658 primary_key: None,
659 unique: None,
660 index: None,
661 foreign_key: None,
662 }],
663 constraints: vec![TableConstraint::Unique {
664 name: Some("uq_email".into()),
665 columns: vec!["email".into()],
666 }],
667 }];
668 let result = build_add_constraint(
669 &DatabaseBackend::Sqlite,
670 "users",
671 &constraint,
672 ¤t_schema,
673 &[],
674 );
675 assert!(result.is_ok());
676 let queries = result.unwrap();
677 let sql = queries
678 .iter()
679 .map(|q| q.build(DatabaseBackend::Sqlite))
680 .collect::<Vec<String>>()
681 .join("\n");
682 assert!(sql.contains("CREATE TABLE"));
684 }
685
686 #[test]
687 fn test_add_constraint_foreign_key_sqlite_table_not_found() {
688 let constraint = TableConstraint::ForeignKey {
689 name: Some("fk_user".into()),
690 columns: vec!["user_id".into()],
691 ref_table: "users".into(),
692 ref_columns: vec!["id".into()],
693 on_delete: None,
694 on_update: None,
695 };
696 let current_schema = vec![]; let result = build_add_constraint(
698 &DatabaseBackend::Sqlite,
699 "posts",
700 &constraint,
701 ¤t_schema,
702 &[],
703 );
704 assert!(result.is_err());
705 let err_msg = result.unwrap_err().to_string();
706 assert!(err_msg.contains("Table 'posts' not found in current schema"));
707 }
708
709 #[test]
710 fn test_add_constraint_foreign_key_sqlite_with_check_constraints() {
711 let constraint = TableConstraint::ForeignKey {
712 name: Some("fk_user".into()),
713 columns: vec!["user_id".into()],
714 ref_table: "users".into(),
715 ref_columns: vec!["id".into()],
716 on_delete: None,
717 on_update: None,
718 };
719 let current_schema = vec![TableDef {
720 name: "posts".into(),
721 description: None,
722 columns: vec![ColumnDef {
723 name: "user_id".into(),
724 r#type: ColumnType::Simple(SimpleColumnType::Integer),
725 nullable: true,
726 default: None,
727 comment: None,
728 primary_key: None,
729 unique: None,
730 index: None,
731 foreign_key: None,
732 }],
733 constraints: vec![TableConstraint::Check {
734 name: "chk_user_id".into(),
735 expr: "user_id > 0".into(),
736 }],
737 }];
738 let result = build_add_constraint(
739 &DatabaseBackend::Sqlite,
740 "posts",
741 &constraint,
742 ¤t_schema,
743 &[],
744 );
745 assert!(result.is_ok());
746 let queries = result.unwrap();
747 let sql = queries
748 .iter()
749 .map(|q| q.build(DatabaseBackend::Sqlite))
750 .collect::<Vec<String>>()
751 .join("\n");
752 assert!(sql.contains("CONSTRAINT \"chk_user_id\" CHECK"));
754 }
755
756 #[test]
757 fn test_add_constraint_foreign_key_sqlite_with_indexes() {
758 let constraint = TableConstraint::ForeignKey {
759 name: Some("fk_user".into()),
760 columns: vec!["user_id".into()],
761 ref_table: "users".into(),
762 ref_columns: vec!["id".into()],
763 on_delete: None,
764 on_update: None,
765 };
766 let current_schema = vec![TableDef {
767 name: "posts".into(),
768 description: None,
769 columns: vec![ColumnDef {
770 name: "user_id".into(),
771 r#type: ColumnType::Simple(SimpleColumnType::Integer),
772 nullable: true,
773 default: None,
774 comment: None,
775 primary_key: None,
776 unique: None,
777 index: None,
778 foreign_key: None,
779 }],
780 constraints: vec![TableConstraint::Index {
781 name: Some("idx_user_id".into()),
782 columns: vec!["user_id".into()],
783 }],
784 }];
785 let result = build_add_constraint(
786 &DatabaseBackend::Sqlite,
787 "posts",
788 &constraint,
789 ¤t_schema,
790 &[],
791 );
792 assert!(result.is_ok());
793 let queries = result.unwrap();
794 let sql = queries
795 .iter()
796 .map(|q| q.build(DatabaseBackend::Sqlite))
797 .collect::<Vec<String>>()
798 .join("\n");
799 assert!(sql.contains("CREATE INDEX"));
801 assert!(sql.contains("idx_user_id"));
802 }
803
804 #[test]
805 fn test_add_constraint_foreign_key_sqlite_with_unique_constraint() {
806 let constraint = TableConstraint::ForeignKey {
808 name: Some("fk_user".into()),
809 columns: vec!["user_id".into()],
810 ref_table: "users".into(),
811 ref_columns: vec!["id".into()],
812 on_delete: None,
813 on_update: None,
814 };
815 let current_schema = vec![TableDef {
816 name: "posts".into(),
817 description: None,
818 columns: vec![ColumnDef {
819 name: "user_id".into(),
820 r#type: ColumnType::Simple(SimpleColumnType::Integer),
821 nullable: true,
822 default: None,
823 comment: None,
824 primary_key: None,
825 unique: None,
826 index: None,
827 foreign_key: None,
828 }],
829 constraints: vec![TableConstraint::Unique {
830 name: Some("uq_user_id".into()),
831 columns: vec!["user_id".into()],
832 }],
833 }];
834 let result = build_add_constraint(
835 &DatabaseBackend::Sqlite,
836 "posts",
837 &constraint,
838 ¤t_schema,
839 &[],
840 );
841 assert!(result.is_ok());
842 let queries = result.unwrap();
843 let sql = queries
844 .iter()
845 .map(|q| q.build(DatabaseBackend::Sqlite))
846 .collect::<Vec<String>>()
847 .join("\n");
848 assert!(sql.contains("CREATE TABLE"));
850 }
851
852 #[test]
853 fn test_add_constraint_check_sqlite_table_not_found() {
854 let constraint = TableConstraint::Check {
855 name: "chk_age".into(),
856 expr: "age > 0".into(),
857 };
858 let current_schema = vec![]; let result = build_add_constraint(
860 &DatabaseBackend::Sqlite,
861 "users",
862 &constraint,
863 ¤t_schema,
864 &[],
865 );
866 assert!(result.is_err());
867 let err_msg = result.unwrap_err().to_string();
868 assert!(err_msg.contains("Table 'users' not found in current schema"));
869 }
870
871 #[test]
872 fn test_add_constraint_check_sqlite_without_existing_check() {
873 let constraint = TableConstraint::Check {
875 name: "chk_age".into(),
876 expr: "age > 0".into(),
877 };
878 let current_schema = vec![TableDef {
879 name: "users".into(),
880 description: None,
881 columns: vec![ColumnDef {
882 name: "age".into(),
883 r#type: ColumnType::Simple(SimpleColumnType::Integer),
884 nullable: true,
885 default: None,
886 comment: None,
887 primary_key: None,
888 unique: None,
889 index: None,
890 foreign_key: None,
891 }],
892 constraints: vec![], }];
894 let result = build_add_constraint(
895 &DatabaseBackend::Sqlite,
896 "users",
897 &constraint,
898 ¤t_schema,
899 &[],
900 );
901 assert!(result.is_ok());
902 let queries = result.unwrap();
903 let sql = queries
904 .iter()
905 .map(|q| q.build(DatabaseBackend::Sqlite))
906 .collect::<Vec<String>>()
907 .join("\n");
908 assert!(sql.contains("CREATE TABLE"));
910 assert!(sql.contains("CONSTRAINT \"chk_age\" CHECK"));
911 }
912
913 #[test]
914 fn test_add_constraint_primary_key_sqlite_without_existing_check() {
915 let constraint = TableConstraint::PrimaryKey {
918 columns: vec!["id".into()],
919 auto_increment: false,
920 };
921 let current_schema = vec![TableDef {
922 name: "users".into(),
923 description: None,
924 columns: vec![ColumnDef {
925 name: "id".into(),
926 r#type: ColumnType::Simple(SimpleColumnType::Integer),
927 nullable: true,
928 default: None,
929 comment: None,
930 primary_key: None,
931 unique: None,
932 index: None,
933 foreign_key: None,
934 }],
935 constraints: vec![], }];
937 let result = build_add_constraint(
938 &DatabaseBackend::Sqlite,
939 "users",
940 &constraint,
941 ¤t_schema,
942 &[],
943 );
944 assert!(result.is_ok());
945 let queries = result.unwrap();
946 let sql = queries
947 .iter()
948 .map(|q| q.build(DatabaseBackend::Sqlite))
949 .collect::<Vec<String>>()
950 .join("\n");
951 assert!(sql.contains("CREATE TABLE"));
953 assert!(sql.contains("PRIMARY KEY"));
954 }
955
956 #[test]
957 fn test_add_constraint_foreign_key_sqlite_without_existing_check() {
958 let constraint = TableConstraint::ForeignKey {
961 name: Some("fk_user".into()),
962 columns: vec!["user_id".into()],
963 ref_table: "users".into(),
964 ref_columns: vec!["id".into()],
965 on_delete: None,
966 on_update: None,
967 };
968 let current_schema = vec![TableDef {
969 name: "posts".into(),
970 description: None,
971 columns: vec![ColumnDef {
972 name: "user_id".into(),
973 r#type: ColumnType::Simple(SimpleColumnType::Integer),
974 nullable: true,
975 default: None,
976 comment: None,
977 primary_key: None,
978 unique: None,
979 index: None,
980 foreign_key: None,
981 }],
982 constraints: vec![], }];
984 let result = build_add_constraint(
985 &DatabaseBackend::Sqlite,
986 "posts",
987 &constraint,
988 ¤t_schema,
989 &[],
990 );
991 assert!(result.is_ok());
992 let queries = result.unwrap();
993 let sql = queries
994 .iter()
995 .map(|q| q.build(DatabaseBackend::Sqlite))
996 .collect::<Vec<String>>()
997 .join("\n");
998 assert!(sql.contains("CREATE TABLE"));
1000 assert!(sql.contains("FOREIGN KEY"));
1001 }
1002
1003 #[test]
1004 fn test_add_constraint_check_sqlite_with_indexes() {
1005 let constraint = TableConstraint::Check {
1006 name: "chk_age".into(),
1007 expr: "age > 0".into(),
1008 };
1009 let current_schema = vec![TableDef {
1010 name: "users".into(),
1011 description: None,
1012 columns: vec![ColumnDef {
1013 name: "age".into(),
1014 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1015 nullable: true,
1016 default: None,
1017 comment: None,
1018 primary_key: None,
1019 unique: None,
1020 index: None,
1021 foreign_key: None,
1022 }],
1023 constraints: vec![TableConstraint::Index {
1024 name: Some("idx_age".into()),
1025 columns: vec!["age".into()],
1026 }],
1027 }];
1028 let result = build_add_constraint(
1029 &DatabaseBackend::Sqlite,
1030 "users",
1031 &constraint,
1032 ¤t_schema,
1033 &[],
1034 );
1035 assert!(result.is_ok());
1036 let queries = result.unwrap();
1037 let sql = queries
1038 .iter()
1039 .map(|q| q.build(DatabaseBackend::Sqlite))
1040 .collect::<Vec<String>>()
1041 .join("\n");
1042 assert!(sql.contains("CREATE INDEX"));
1044 assert!(sql.contains("idx_age"));
1045 }
1046
1047 #[test]
1048 fn test_add_constraint_check_sqlite_with_unique_constraint() {
1049 let constraint = TableConstraint::Check {
1051 name: "chk_age".into(),
1052 expr: "age > 0".into(),
1053 };
1054 let current_schema = vec![TableDef {
1055 name: "users".into(),
1056 description: None,
1057 columns: vec![ColumnDef {
1058 name: "age".into(),
1059 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1060 nullable: true,
1061 default: None,
1062 comment: None,
1063 primary_key: None,
1064 unique: None,
1065 index: None,
1066 foreign_key: None,
1067 }],
1068 constraints: vec![TableConstraint::Unique {
1069 name: Some("uq_age".into()),
1070 columns: vec!["age".into()],
1071 }],
1072 }];
1073 let result = build_add_constraint(
1074 &DatabaseBackend::Sqlite,
1075 "users",
1076 &constraint,
1077 ¤t_schema,
1078 &[],
1079 );
1080 assert!(result.is_ok());
1081 let queries = result.unwrap();
1082 let sql = queries
1083 .iter()
1084 .map(|q| q.build(DatabaseBackend::Sqlite))
1085 .collect::<Vec<String>>()
1086 .join("\n");
1087 assert!(sql.contains("CREATE TABLE"));
1089 }
1090
1091 #[test]
1092 fn test_add_constraint_composite_primary_key_postgres() {
1093 let constraint = TableConstraint::PrimaryKey {
1094 columns: vec!["user_id".into(), "role_id".into()],
1095 auto_increment: false,
1096 };
1097 let current_schema = vec![TableDef {
1098 name: "user_roles".into(),
1099 description: None,
1100 columns: vec![
1101 ColumnDef {
1102 name: "user_id".into(),
1103 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1104 nullable: false,
1105 default: None,
1106 comment: None,
1107 primary_key: None,
1108 unique: None,
1109 index: None,
1110 foreign_key: None,
1111 },
1112 ColumnDef {
1113 name: "role_id".into(),
1114 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1115 nullable: false,
1116 default: None,
1117 comment: None,
1118 primary_key: None,
1119 unique: None,
1120 index: None,
1121 foreign_key: None,
1122 },
1123 ],
1124 constraints: vec![],
1125 }];
1126 let result = build_add_constraint(
1127 &DatabaseBackend::Postgres,
1128 "user_roles",
1129 &constraint,
1130 ¤t_schema,
1131 &[],
1132 )
1133 .unwrap();
1134 let sql = result[0].build(DatabaseBackend::Postgres);
1135 assert!(sql.contains("ADD PRIMARY KEY"));
1136 assert!(sql.contains("\"user_id\""));
1137 assert!(sql.contains("\"role_id\""));
1138 }
1139
1140 #[test]
1141 fn test_add_constraint_composite_primary_key_mysql() {
1142 let constraint = TableConstraint::PrimaryKey {
1143 columns: vec!["user_id".into(), "role_id".into()],
1144 auto_increment: false,
1145 };
1146 let current_schema = vec![TableDef {
1147 name: "user_roles".into(),
1148 description: None,
1149 columns: vec![
1150 ColumnDef {
1151 name: "user_id".into(),
1152 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1153 nullable: false,
1154 default: None,
1155 comment: None,
1156 primary_key: None,
1157 unique: None,
1158 index: None,
1159 foreign_key: None,
1160 },
1161 ColumnDef {
1162 name: "role_id".into(),
1163 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1164 nullable: false,
1165 default: None,
1166 comment: None,
1167 primary_key: None,
1168 unique: None,
1169 index: None,
1170 foreign_key: None,
1171 },
1172 ],
1173 constraints: vec![],
1174 }];
1175 let result = build_add_constraint(
1176 &DatabaseBackend::MySql,
1177 "user_roles",
1178 &constraint,
1179 ¤t_schema,
1180 &[],
1181 )
1182 .unwrap();
1183 let sql = result[0].build(DatabaseBackend::MySql);
1184 assert!(sql.contains("ADD PRIMARY KEY"));
1185 assert!(sql.contains("`user_id`"));
1186 assert!(sql.contains("`role_id`"));
1187 }
1188
1189 #[test]
1190 fn test_constraints_overlap_primary_key_same_columns() {
1191 let a = TableConstraint::PrimaryKey {
1192 columns: vec!["id".into()],
1193 auto_increment: false,
1194 };
1195 let b = TableConstraint::PrimaryKey {
1196 columns: vec!["id".into()],
1197 auto_increment: true,
1198 };
1199 assert!(constraints_overlap(&a, &b));
1200 }
1201
1202 #[test]
1203 fn test_constraints_overlap_primary_key_different_columns() {
1204 let a = TableConstraint::PrimaryKey {
1205 columns: vec!["id".into()],
1206 auto_increment: false,
1207 };
1208 let b = TableConstraint::PrimaryKey {
1209 columns: vec!["uid".into()],
1210 auto_increment: false,
1211 };
1212 assert!(!constraints_overlap(&a, &b));
1213 }
1214
1215 #[test]
1216 fn test_constraints_overlap_check_same() {
1217 let a = TableConstraint::Check {
1218 name: "chk_age".into(),
1219 expr: "age > 0".into(),
1220 };
1221 let b = TableConstraint::Check {
1222 name: "chk_age".into(),
1223 expr: "age > 0".into(),
1224 };
1225 assert!(constraints_overlap(&a, &b));
1226 }
1227
1228 #[test]
1229 fn test_constraints_overlap_check_different_name() {
1230 let a = TableConstraint::Check {
1231 name: "chk_age".into(),
1232 expr: "age > 0".into(),
1233 };
1234 let b = TableConstraint::Check {
1235 name: "chk_age2".into(),
1236 expr: "age > 0".into(),
1237 };
1238 assert!(!constraints_overlap(&a, &b));
1239 }
1240
1241 #[test]
1242 fn test_constraints_overlap_check_different_expr() {
1243 let a = TableConstraint::Check {
1244 name: "chk_age".into(),
1245 expr: "age > 0".into(),
1246 };
1247 let b = TableConstraint::Check {
1248 name: "chk_age".into(),
1249 expr: "age > 10".into(),
1250 };
1251 assert!(!constraints_overlap(&a, &b));
1252 }
1253
1254 #[test]
1255 fn test_constraints_overlap_different_variants() {
1256 let a = TableConstraint::PrimaryKey {
1257 columns: vec!["id".into()],
1258 auto_increment: false,
1259 };
1260 let b = TableConstraint::Check {
1261 name: "chk".into(),
1262 expr: "id > 0".into(),
1263 };
1264 assert!(!constraints_overlap(&a, &b));
1265 }
1266
1267 #[test]
1268 fn test_constraints_overlap_fk_same_columns() {
1269 let a = TableConstraint::ForeignKey {
1270 name: None,
1271 columns: vec!["user_id".into()],
1272 ref_table: "users".into(),
1273 ref_columns: vec!["id".into()],
1274 on_delete: None,
1275 on_update: None,
1276 };
1277 let b = TableConstraint::ForeignKey {
1278 name: Some("fk".into()),
1279 columns: vec!["user_id".into()],
1280 ref_table: "other".into(),
1281 ref_columns: vec!["oid".into()],
1282 on_delete: Some(ReferenceAction::Cascade),
1283 on_update: None,
1284 };
1285 assert!(constraints_overlap(&a, &b));
1286 }
1287
1288 #[test]
1289 fn test_merge_constraint_replaces_overlapping() {
1290 let existing = vec![
1291 TableConstraint::PrimaryKey {
1292 columns: vec!["id".into()],
1293 auto_increment: false,
1294 },
1295 TableConstraint::Index {
1296 name: None,
1297 columns: vec!["email".into()],
1298 },
1299 ];
1300 let new_pk = TableConstraint::PrimaryKey {
1301 columns: vec!["id".into()],
1302 auto_increment: true,
1303 };
1304 let result = merge_constraint(&existing, &new_pk);
1305 assert_eq!(result.len(), 2); }
1307
1308 #[test]
1309 fn test_merge_constraint_appends_non_overlapping() {
1310 let existing = vec![TableConstraint::Index {
1311 name: None,
1312 columns: vec!["email".into()],
1313 }];
1314 let new_pk = TableConstraint::PrimaryKey {
1315 columns: vec!["id".into()],
1316 auto_increment: false,
1317 };
1318 let result = merge_constraint(&existing, &new_pk);
1319 assert_eq!(result.len(), 2); }
1321
1322 #[test]
1323 fn test_extract_check_clauses_with_mixed_constraints() {
1324 let constraints = vec![
1326 TableConstraint::Check {
1327 name: "chk1".into(),
1328 expr: "a > 0".into(),
1329 },
1330 TableConstraint::PrimaryKey {
1331 columns: vec!["id".into()],
1332 auto_increment: false,
1333 },
1334 TableConstraint::Check {
1335 name: "chk2".into(),
1336 expr: "b < 100".into(),
1337 },
1338 TableConstraint::Unique {
1339 name: Some("uq".into()),
1340 columns: vec!["email".into()],
1341 },
1342 ];
1343 let clauses = crate::sql::helpers::extract_check_clauses(&constraints);
1344 assert_eq!(clauses.len(), 2);
1345 assert!(clauses[0].contains("chk1"));
1346 assert!(clauses[1].contains("chk2"));
1347 }
1348
1349 #[test]
1350 fn test_extract_check_clauses_with_no_check_constraints() {
1351 let constraints = vec![
1352 TableConstraint::PrimaryKey {
1353 columns: vec!["id".into()],
1354 auto_increment: false,
1355 },
1356 TableConstraint::Unique {
1357 name: None,
1358 columns: vec!["email".into()],
1359 },
1360 ];
1361 let clauses = crate::sql::helpers::extract_check_clauses(&constraints);
1362 assert!(clauses.is_empty());
1363 }
1364
1365 #[test]
1366 fn test_build_create_with_checks_empty_clauses() {
1367 use crate::sql::create_table::build_create_table_for_backend;
1368
1369 let create_stmt = build_create_table_for_backend(
1370 &DatabaseBackend::Sqlite,
1371 "test_table",
1372 &[ColumnDef {
1373 name: "id".into(),
1374 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1375 nullable: false,
1376 default: None,
1377 comment: None,
1378 primary_key: None,
1379 unique: None,
1380 index: None,
1381 foreign_key: None,
1382 }],
1383 &[],
1384 );
1385
1386 let result = crate::sql::helpers::build_create_with_checks(
1388 &DatabaseBackend::Sqlite,
1389 &create_stmt,
1390 &[],
1391 );
1392 let sql = result.build(DatabaseBackend::Sqlite);
1393 assert!(sql.contains("CREATE TABLE"));
1394 }
1395
1396 #[test]
1397 fn test_build_create_with_checks_with_clauses() {
1398 use crate::sql::create_table::build_create_table_for_backend;
1399
1400 let create_stmt = build_create_table_for_backend(
1401 &DatabaseBackend::Sqlite,
1402 "test_table",
1403 &[ColumnDef {
1404 name: "id".into(),
1405 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1406 nullable: false,
1407 default: None,
1408 comment: None,
1409 primary_key: None,
1410 unique: None,
1411 index: None,
1412 foreign_key: None,
1413 }],
1414 &[],
1415 );
1416
1417 let check_clauses = vec!["CONSTRAINT \"chk1\" CHECK (id > 0)".to_string()];
1419 let result = crate::sql::helpers::build_create_with_checks(
1420 &DatabaseBackend::Sqlite,
1421 &create_stmt,
1422 &check_clauses,
1423 );
1424 let sql = result.build(DatabaseBackend::Sqlite);
1425 assert!(sql.contains("CREATE TABLE"));
1426 assert!(sql.contains("CONSTRAINT \"chk1\" CHECK (id > 0)"));
1427 }
1428}