1use sea_query::{Alias, ForeignKey, Index, Query, Table};
2
3use vespertide_core::{TableConstraint, TableDef};
4
5use super::create_table::build_create_table_for_backend;
6use super::helpers::{build_schema_statement, to_sea_fk_action};
7use super::rename_table::build_rename_table;
8use super::types::{BuiltQuery, DatabaseBackend};
9use crate::error::QueryError;
10use crate::sql::RawSql;
11
12fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec<String> {
14 constraints
15 .iter()
16 .filter_map(|c| {
17 if let TableConstraint::Check { name, expr } = c {
18 Some(format!("CONSTRAINT \"{}\" CHECK ({})", name, expr))
19 } else {
20 None
21 }
22 })
23 .collect()
24}
25
26fn build_create_with_checks(
28 backend: &DatabaseBackend,
29 create_stmt: &sea_query::TableCreateStatement,
30 check_clauses: &[String],
31) -> BuiltQuery {
32 if check_clauses.is_empty() {
33 BuiltQuery::CreateTable(Box::new(create_stmt.clone()))
34 } else {
35 let base_sql = build_schema_statement(create_stmt, *backend);
36 let mut modified_sql = base_sql;
37 if let Some(pos) = modified_sql.rfind(')') {
38 let check_sql = check_clauses.join(", ");
39 modified_sql.insert_str(pos, &format!(", {}", check_sql));
40 }
41 BuiltQuery::Raw(RawSql::per_backend(
42 modified_sql.clone(),
43 modified_sql.clone(),
44 modified_sql,
45 ))
46 }
47}
48
49pub fn build_add_constraint(
50 backend: &DatabaseBackend,
51 table: &str,
52 constraint: &TableConstraint,
53 current_schema: &[TableDef],
54) -> Result<Vec<BuiltQuery>, QueryError> {
55 match constraint {
56 TableConstraint::PrimaryKey { columns, .. } => {
57 if *backend == DatabaseBackend::Sqlite {
58 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)))?;
61
62 let mut new_constraints = table_def.constraints.clone();
64 new_constraints.push(constraint.clone());
65
66 let temp_table = format!("{}_temp", table);
68
69 let create_temp_table = build_create_table_for_backend(
71 backend,
72 &temp_table,
73 &table_def.columns,
74 &new_constraints,
75 );
76
77 let check_clauses = extract_check_clauses(&new_constraints);
79 let create_query =
80 build_create_with_checks(backend, &create_temp_table, &check_clauses);
81
82 let column_aliases: Vec<Alias> = table_def
84 .columns
85 .iter()
86 .map(|c| Alias::new(&c.name))
87 .collect();
88 let mut select_query = Query::select();
89 for col_alias in &column_aliases {
90 select_query = select_query.column(col_alias.clone()).to_owned();
91 }
92 select_query = select_query.from(Alias::new(table)).to_owned();
93
94 let insert_stmt = Query::insert()
95 .into_table(Alias::new(&temp_table))
96 .columns(column_aliases.clone())
97 .select_from(select_query)
98 .unwrap()
99 .to_owned();
100 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
101
102 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
104 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
105
106 let rename_query = build_rename_table(&temp_table, table);
108
109 let mut index_queries = Vec::new();
111 for c in &table_def.constraints {
112 if let TableConstraint::Index {
113 name: idx_name,
114 columns: idx_cols,
115 } = c
116 {
117 let index_name = vespertide_naming::build_index_name(
118 table,
119 idx_cols,
120 idx_name.as_deref(),
121 );
122 let mut idx_stmt = sea_query::Index::create();
123 idx_stmt = idx_stmt.name(&index_name).to_owned();
124 for col_name in idx_cols {
125 idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned();
126 }
127 idx_stmt = idx_stmt.table(Alias::new(table)).to_owned();
128 index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt)));
129 }
130 }
131
132 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
133 queries.extend(index_queries);
134 Ok(queries)
135 } else {
136 let pg_cols = columns
138 .iter()
139 .map(|c| format!("\"{}\"", c))
140 .collect::<Vec<_>>()
141 .join(", ");
142 let mysql_cols = columns
143 .iter()
144 .map(|c| format!("`{}`", c))
145 .collect::<Vec<_>>()
146 .join(", ");
147 let pg_sql = format!("ALTER TABLE \"{}\" ADD PRIMARY KEY ({})", table, pg_cols);
148 let mysql_sql = format!("ALTER TABLE `{}` ADD PRIMARY KEY ({})", table, mysql_cols);
149 Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
150 pg_sql.clone(),
151 mysql_sql,
152 pg_sql,
153 ))])
154 }
155 }
156 TableConstraint::Unique { name, columns } => {
157 let index_name =
160 super::helpers::build_unique_constraint_name(table, columns, name.as_deref());
161 let mut idx = Index::create()
162 .table(Alias::new(table))
163 .name(&index_name)
164 .unique()
165 .to_owned();
166 for col in columns {
167 idx = idx.col(Alias::new(col)).to_owned();
168 }
169 Ok(vec![BuiltQuery::CreateIndex(Box::new(idx))])
170 }
171 TableConstraint::ForeignKey {
172 name,
173 columns,
174 ref_table,
175 ref_columns,
176 on_delete,
177 on_update,
178 } => {
179 if *backend == DatabaseBackend::Sqlite {
181 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)))?;
183
184 let mut new_constraints = table_def.constraints.clone();
186 new_constraints.push(constraint.clone());
187
188 let temp_table = format!("{}_temp", table);
190
191 let create_temp_table = build_create_table_for_backend(
193 backend,
194 &temp_table,
195 &table_def.columns,
196 &new_constraints,
197 );
198
199 let check_clauses = extract_check_clauses(&new_constraints);
201 let create_query =
202 build_create_with_checks(backend, &create_temp_table, &check_clauses);
203
204 let column_aliases: Vec<Alias> = table_def
206 .columns
207 .iter()
208 .map(|c| Alias::new(&c.name))
209 .collect();
210 let mut select_query = Query::select();
211 for col_alias in &column_aliases {
212 select_query = select_query.column(col_alias.clone()).to_owned();
213 }
214 select_query = select_query.from(Alias::new(table)).to_owned();
215
216 let insert_stmt = Query::insert()
217 .into_table(Alias::new(&temp_table))
218 .columns(column_aliases.clone())
219 .select_from(select_query)
220 .unwrap()
221 .to_owned();
222 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
223
224 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
226 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
227
228 let rename_query = build_rename_table(&temp_table, table);
230
231 let mut index_queries = Vec::new();
233 for c in &table_def.constraints {
234 if let TableConstraint::Index {
235 name: idx_name,
236 columns: idx_cols,
237 } = c
238 {
239 let index_name = vespertide_naming::build_index_name(
240 table,
241 idx_cols,
242 idx_name.as_deref(),
243 );
244 let mut idx_stmt = sea_query::Index::create();
245 idx_stmt = idx_stmt.name(&index_name).to_owned();
246 for col_name in idx_cols {
247 idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned();
248 }
249 idx_stmt = idx_stmt.table(Alias::new(table)).to_owned();
250 index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt)));
251 }
252 }
253
254 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
255 queries.extend(index_queries);
256 Ok(queries)
257 } else {
258 let fk_name =
260 vespertide_naming::build_foreign_key_name(table, columns, name.as_deref());
261 let mut fk = ForeignKey::create();
262 fk = fk.name(&fk_name).to_owned();
263 fk = fk.from_tbl(Alias::new(table)).to_owned();
264 for col in columns {
265 fk = fk.from_col(Alias::new(col)).to_owned();
266 }
267 fk = fk.to_tbl(Alias::new(ref_table)).to_owned();
268 for col in ref_columns {
269 fk = fk.to_col(Alias::new(col)).to_owned();
270 }
271 if let Some(action) = on_delete {
272 fk = fk.on_delete(to_sea_fk_action(action)).to_owned();
273 }
274 if let Some(action) = on_update {
275 fk = fk.on_update(to_sea_fk_action(action)).to_owned();
276 }
277 Ok(vec![BuiltQuery::CreateForeignKey(Box::new(fk))])
278 }
279 }
280 TableConstraint::Index { name, columns } => {
281 let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref());
283 let mut idx = Index::create()
284 .table(Alias::new(table))
285 .name(&index_name)
286 .to_owned();
287 for col in columns {
288 idx = idx.col(Alias::new(col)).to_owned();
289 }
290 Ok(vec![BuiltQuery::CreateIndex(Box::new(idx))])
291 }
292 TableConstraint::Check { name, expr } => {
293 if *backend == DatabaseBackend::Sqlite {
295 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)))?;
297
298 let mut new_constraints = table_def.constraints.clone();
300 new_constraints.push(constraint.clone());
301
302 let temp_table = format!("{}_temp", table);
304
305 let create_temp_table = build_create_table_for_backend(
307 backend,
308 &temp_table,
309 &table_def.columns,
310 &new_constraints,
311 );
312
313 let check_clauses = extract_check_clauses(&new_constraints);
315 let create_query =
316 build_create_with_checks(backend, &create_temp_table, &check_clauses);
317
318 let column_aliases: Vec<Alias> = table_def
320 .columns
321 .iter()
322 .map(|c| Alias::new(&c.name))
323 .collect();
324 let mut select_query = Query::select();
325 for col_alias in &column_aliases {
326 select_query = select_query.column(col_alias.clone()).to_owned();
327 }
328 select_query = select_query.from(Alias::new(table)).to_owned();
329
330 let insert_stmt = Query::insert()
331 .into_table(Alias::new(&temp_table))
332 .columns(column_aliases.clone())
333 .select_from(select_query)
334 .unwrap()
335 .to_owned();
336 let insert_query = BuiltQuery::Insert(Box::new(insert_stmt));
337
338 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
340 let drop_query = BuiltQuery::DropTable(Box::new(drop_table));
341
342 let rename_query = build_rename_table(&temp_table, table);
344
345 let mut index_queries = Vec::new();
347 for c in &table_def.constraints {
348 if let TableConstraint::Index {
349 name: idx_name,
350 columns: idx_cols,
351 } = c
352 {
353 let index_name = vespertide_naming::build_index_name(
354 table,
355 idx_cols,
356 idx_name.as_deref(),
357 );
358 let mut idx_stmt = sea_query::Index::create();
359 idx_stmt = idx_stmt.name(&index_name).to_owned();
360 for col_name in idx_cols {
361 idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned();
362 }
363 idx_stmt = idx_stmt.table(Alias::new(table)).to_owned();
364 index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt)));
365 }
366 }
367
368 let mut queries = vec![create_query, insert_query, drop_query, rename_query];
369 queries.extend(index_queries);
370 Ok(queries)
371 } else {
372 let pg_sql = format!(
373 "ALTER TABLE \"{}\" ADD CONSTRAINT \"{}\" CHECK ({})",
374 table, name, expr
375 );
376 let mysql_sql = format!(
377 "ALTER TABLE `{}` ADD CONSTRAINT `{}` CHECK ({})",
378 table, name, expr
379 );
380 Ok(vec![BuiltQuery::Raw(RawSql::per_backend(
381 pg_sql.clone(),
382 mysql_sql,
383 pg_sql,
384 ))])
385 }
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::sql::types::DatabaseBackend;
394 use insta::{assert_snapshot, with_settings};
395 use rstest::rstest;
396 use vespertide_core::{
397 ColumnDef, ColumnType, ReferenceAction, SimpleColumnType, TableConstraint, TableDef,
398 };
399
400 #[rstest]
401 #[case::add_constraint_primary_key_postgres(
402 "add_constraint_primary_key_postgres",
403 DatabaseBackend::Postgres,
404 &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"]
405 )]
406 #[case::add_constraint_primary_key_mysql(
407 "add_constraint_primary_key_mysql",
408 DatabaseBackend::MySql,
409 &["ALTER TABLE `users` ADD PRIMARY KEY (`id`)"]
410 )]
411 #[case::add_constraint_primary_key_sqlite(
412 "add_constraint_primary_key_sqlite",
413 DatabaseBackend::Sqlite,
414 &["CREATE TABLE \"users_temp\""]
415 )]
416 #[case::add_constraint_unique_named_postgres(
417 "add_constraint_unique_named_postgres",
418 DatabaseBackend::Postgres,
419 &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"]
420 )]
421 #[case::add_constraint_unique_named_mysql(
422 "add_constraint_unique_named_mysql",
423 DatabaseBackend::MySql,
424 &["CREATE UNIQUE INDEX `uq_users__uq_email` ON `users` (`email`)"]
425 )]
426 #[case::add_constraint_unique_named_sqlite(
427 "add_constraint_unique_named_sqlite",
428 DatabaseBackend::Sqlite,
429 &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"]
430 )]
431 #[case::add_constraint_foreign_key_postgres(
432 "add_constraint_foreign_key_postgres",
433 DatabaseBackend::Postgres,
434 &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
435 )]
436 #[case::add_constraint_foreign_key_mysql(
437 "add_constraint_foreign_key_mysql",
438 DatabaseBackend::MySql,
439 &["FOREIGN KEY (`user_id`)", "REFERENCES `users` (`id`)", "ON DELETE CASCADE", "ON UPDATE RESTRICT"]
440 )]
441 #[case::add_constraint_foreign_key_sqlite(
442 "add_constraint_foreign_key_sqlite",
443 DatabaseBackend::Sqlite,
444 &["CREATE TABLE \"users_temp\""]
445 )]
446 #[case::add_constraint_check_named_postgres(
447 "add_constraint_check_named_postgres",
448 DatabaseBackend::Postgres,
449 &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"]
450 )]
451 #[case::add_constraint_check_named_mysql(
452 "add_constraint_check_named_mysql",
453 DatabaseBackend::MySql,
454 &["ADD CONSTRAINT `chk_age` CHECK (age > 0)"]
455 )]
456 #[case::add_constraint_check_named_sqlite(
457 "add_constraint_check_named_sqlite",
458 DatabaseBackend::Sqlite,
459 &["CREATE TABLE \"users_temp\""]
460 )]
461 fn test_add_constraint(
462 #[case] title: &str,
463 #[case] backend: DatabaseBackend,
464 #[case] expected: &[&str],
465 ) {
466 let constraint = if title.contains("primary_key") {
467 TableConstraint::PrimaryKey {
468 columns: vec!["id".into()],
469 auto_increment: false,
470 }
471 } else if title.contains("unique") {
472 TableConstraint::Unique {
473 name: Some("uq_email".into()),
474 columns: vec!["email".into()],
475 }
476 } else if title.contains("foreign_key") {
477 TableConstraint::ForeignKey {
478 name: Some("fk_user".into()),
479 columns: vec!["user_id".into()],
480 ref_table: "users".into(),
481 ref_columns: vec!["id".into()],
482 on_delete: Some(ReferenceAction::Cascade),
483 on_update: Some(ReferenceAction::Restrict),
484 }
485 } else {
486 TableConstraint::Check {
487 name: "chk_age".into(),
488 expr: "age > 0".into(),
489 }
490 };
491
492 let current_schema = vec![TableDef {
494 name: "users".into(),
495 description: None,
496 columns: if title.contains("foreign_key") {
497 vec![
498 ColumnDef {
499 name: "id".into(),
500 r#type: ColumnType::Simple(SimpleColumnType::Integer),
501 nullable: false,
502 default: None,
503 comment: None,
504 primary_key: None,
505 unique: None,
506 index: None,
507 foreign_key: None,
508 },
509 ColumnDef {
510 name: "user_id".into(),
511 r#type: ColumnType::Simple(SimpleColumnType::Integer),
512 nullable: true,
513 default: None,
514 comment: None,
515 primary_key: None,
516 unique: None,
517 index: None,
518 foreign_key: None,
519 },
520 ]
521 } else {
522 vec![
523 ColumnDef {
524 name: "id".into(),
525 r#type: ColumnType::Simple(SimpleColumnType::Integer),
526 nullable: false,
527 default: None,
528 comment: None,
529 primary_key: None,
530 unique: None,
531 index: None,
532 foreign_key: None,
533 },
534 ColumnDef {
535 name: if title.contains("check") {
536 "age".into()
537 } else {
538 "email".into()
539 },
540 r#type: ColumnType::Simple(SimpleColumnType::Text),
541 nullable: true,
542 default: None,
543 comment: None,
544 primary_key: None,
545 unique: None,
546 index: None,
547 foreign_key: None,
548 },
549 ]
550 },
551 constraints: vec![],
552 }];
553
554 let result = build_add_constraint(&backend, "users", &constraint, ¤t_schema).unwrap();
555 let sql = result[0].build(backend);
556 for exp in expected {
557 assert!(
558 sql.contains(exp),
559 "Expected SQL to contain '{}', got: {}",
560 exp,
561 sql
562 );
563 }
564
565 with_settings!({ snapshot_suffix => format!("add_constraint_{}", title) }, {
566 assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::<Vec<String>>().join("\n"));
567 });
568 }
569
570 #[test]
571 fn test_add_constraint_primary_key_sqlite_table_not_found() {
572 let constraint = TableConstraint::PrimaryKey {
573 columns: vec!["id".into()],
574 auto_increment: false,
575 };
576 let current_schema = vec![]; let result = build_add_constraint(
578 &DatabaseBackend::Sqlite,
579 "users",
580 &constraint,
581 ¤t_schema,
582 );
583 assert!(result.is_err());
584 let err_msg = result.unwrap_err().to_string();
585 assert!(err_msg.contains("Table 'users' not found in current schema"));
586 }
587
588 #[test]
589 fn test_add_constraint_primary_key_sqlite_with_check_constraints() {
590 let constraint = TableConstraint::PrimaryKey {
591 columns: vec!["id".into()],
592 auto_increment: false,
593 };
594 let current_schema = vec![TableDef {
595 name: "users".into(),
596 description: None,
597 columns: vec![ColumnDef {
598 name: "id".into(),
599 r#type: ColumnType::Simple(SimpleColumnType::Integer),
600 nullable: false,
601 default: None,
602 comment: None,
603 primary_key: None,
604 unique: None,
605 index: None,
606 foreign_key: None,
607 }],
608 constraints: vec![TableConstraint::Check {
609 name: "chk_id".into(),
610 expr: "id > 0".into(),
611 }],
612 }];
613 let result = build_add_constraint(
614 &DatabaseBackend::Sqlite,
615 "users",
616 &constraint,
617 ¤t_schema,
618 );
619 assert!(result.is_ok());
620 let queries = result.unwrap();
621 let sql = queries
622 .iter()
623 .map(|q| q.build(DatabaseBackend::Sqlite))
624 .collect::<Vec<String>>()
625 .join("\n");
626 assert!(sql.contains("CONSTRAINT \"chk_id\" CHECK"));
628 }
629
630 #[test]
631 fn test_add_constraint_primary_key_sqlite_with_indexes() {
632 let constraint = TableConstraint::PrimaryKey {
633 columns: vec!["id".into()],
634 auto_increment: false,
635 };
636 let current_schema = vec![TableDef {
637 name: "users".into(),
638 description: None,
639 columns: vec![ColumnDef {
640 name: "id".into(),
641 r#type: ColumnType::Simple(SimpleColumnType::Integer),
642 nullable: false,
643 default: None,
644 comment: None,
645 primary_key: None,
646 unique: None,
647 index: None,
648 foreign_key: None,
649 }],
650 constraints: vec![TableConstraint::Index {
651 name: Some("idx_id".into()),
652 columns: vec!["id".into()],
653 }],
654 }];
655 let result = build_add_constraint(
656 &DatabaseBackend::Sqlite,
657 "users",
658 &constraint,
659 ¤t_schema,
660 );
661 assert!(result.is_ok());
662 let queries = result.unwrap();
663 let sql = queries
664 .iter()
665 .map(|q| q.build(DatabaseBackend::Sqlite))
666 .collect::<Vec<String>>()
667 .join("\n");
668 assert!(sql.contains("CREATE INDEX"));
670 assert!(sql.contains("idx_id"));
671 }
672
673 #[test]
674 fn test_add_constraint_primary_key_sqlite_with_unique_constraint() {
675 let constraint = TableConstraint::PrimaryKey {
678 columns: vec!["id".into()],
679 auto_increment: false,
680 };
681 let current_schema = vec![TableDef {
682 name: "users".into(),
683 description: None,
684 columns: vec![ColumnDef {
685 name: "id".into(),
686 r#type: ColumnType::Simple(SimpleColumnType::Integer),
687 nullable: false,
688 default: None,
689 comment: None,
690 primary_key: None,
691 unique: None,
692 index: None,
693 foreign_key: None,
694 }],
695 constraints: vec![TableConstraint::Unique {
696 name: Some("uq_email".into()),
697 columns: vec!["email".into()],
698 }],
699 }];
700 let result = build_add_constraint(
701 &DatabaseBackend::Sqlite,
702 "users",
703 &constraint,
704 ¤t_schema,
705 );
706 assert!(result.is_ok());
707 let queries = result.unwrap();
708 let sql = queries
709 .iter()
710 .map(|q| q.build(DatabaseBackend::Sqlite))
711 .collect::<Vec<String>>()
712 .join("\n");
713 assert!(sql.contains("CREATE TABLE"));
715 }
716
717 #[test]
718 fn test_add_constraint_foreign_key_sqlite_table_not_found() {
719 let constraint = TableConstraint::ForeignKey {
720 name: Some("fk_user".into()),
721 columns: vec!["user_id".into()],
722 ref_table: "users".into(),
723 ref_columns: vec!["id".into()],
724 on_delete: None,
725 on_update: None,
726 };
727 let current_schema = vec![]; let result = build_add_constraint(
729 &DatabaseBackend::Sqlite,
730 "posts",
731 &constraint,
732 ¤t_schema,
733 );
734 assert!(result.is_err());
735 let err_msg = result.unwrap_err().to_string();
736 assert!(err_msg.contains("Table 'posts' not found in current schema"));
737 }
738
739 #[test]
740 fn test_add_constraint_foreign_key_sqlite_with_check_constraints() {
741 let constraint = TableConstraint::ForeignKey {
742 name: Some("fk_user".into()),
743 columns: vec!["user_id".into()],
744 ref_table: "users".into(),
745 ref_columns: vec!["id".into()],
746 on_delete: None,
747 on_update: None,
748 };
749 let current_schema = vec![TableDef {
750 name: "posts".into(),
751 description: None,
752 columns: vec![ColumnDef {
753 name: "user_id".into(),
754 r#type: ColumnType::Simple(SimpleColumnType::Integer),
755 nullable: true,
756 default: None,
757 comment: None,
758 primary_key: None,
759 unique: None,
760 index: None,
761 foreign_key: None,
762 }],
763 constraints: vec![TableConstraint::Check {
764 name: "chk_user_id".into(),
765 expr: "user_id > 0".into(),
766 }],
767 }];
768 let result = build_add_constraint(
769 &DatabaseBackend::Sqlite,
770 "posts",
771 &constraint,
772 ¤t_schema,
773 );
774 assert!(result.is_ok());
775 let queries = result.unwrap();
776 let sql = queries
777 .iter()
778 .map(|q| q.build(DatabaseBackend::Sqlite))
779 .collect::<Vec<String>>()
780 .join("\n");
781 assert!(sql.contains("CONSTRAINT \"chk_user_id\" CHECK"));
783 }
784
785 #[test]
786 fn test_add_constraint_foreign_key_sqlite_with_indexes() {
787 let constraint = TableConstraint::ForeignKey {
788 name: Some("fk_user".into()),
789 columns: vec!["user_id".into()],
790 ref_table: "users".into(),
791 ref_columns: vec!["id".into()],
792 on_delete: None,
793 on_update: None,
794 };
795 let current_schema = vec![TableDef {
796 name: "posts".into(),
797 description: None,
798 columns: vec![ColumnDef {
799 name: "user_id".into(),
800 r#type: ColumnType::Simple(SimpleColumnType::Integer),
801 nullable: true,
802 default: None,
803 comment: None,
804 primary_key: None,
805 unique: None,
806 index: None,
807 foreign_key: None,
808 }],
809 constraints: vec![TableConstraint::Index {
810 name: Some("idx_user_id".into()),
811 columns: vec!["user_id".into()],
812 }],
813 }];
814 let result = build_add_constraint(
815 &DatabaseBackend::Sqlite,
816 "posts",
817 &constraint,
818 ¤t_schema,
819 );
820 assert!(result.is_ok());
821 let queries = result.unwrap();
822 let sql = queries
823 .iter()
824 .map(|q| q.build(DatabaseBackend::Sqlite))
825 .collect::<Vec<String>>()
826 .join("\n");
827 assert!(sql.contains("CREATE INDEX"));
829 assert!(sql.contains("idx_user_id"));
830 }
831
832 #[test]
833 fn test_add_constraint_foreign_key_sqlite_with_unique_constraint() {
834 let constraint = TableConstraint::ForeignKey {
836 name: Some("fk_user".into()),
837 columns: vec!["user_id".into()],
838 ref_table: "users".into(),
839 ref_columns: vec!["id".into()],
840 on_delete: None,
841 on_update: None,
842 };
843 let current_schema = vec![TableDef {
844 name: "posts".into(),
845 description: None,
846 columns: vec![ColumnDef {
847 name: "user_id".into(),
848 r#type: ColumnType::Simple(SimpleColumnType::Integer),
849 nullable: true,
850 default: None,
851 comment: None,
852 primary_key: None,
853 unique: None,
854 index: None,
855 foreign_key: None,
856 }],
857 constraints: vec![TableConstraint::Unique {
858 name: Some("uq_user_id".into()),
859 columns: vec!["user_id".into()],
860 }],
861 }];
862 let result = build_add_constraint(
863 &DatabaseBackend::Sqlite,
864 "posts",
865 &constraint,
866 ¤t_schema,
867 );
868 assert!(result.is_ok());
869 let queries = result.unwrap();
870 let sql = queries
871 .iter()
872 .map(|q| q.build(DatabaseBackend::Sqlite))
873 .collect::<Vec<String>>()
874 .join("\n");
875 assert!(sql.contains("CREATE TABLE"));
877 }
878
879 #[test]
880 fn test_add_constraint_check_sqlite_table_not_found() {
881 let constraint = TableConstraint::Check {
882 name: "chk_age".into(),
883 expr: "age > 0".into(),
884 };
885 let current_schema = vec![]; let result = build_add_constraint(
887 &DatabaseBackend::Sqlite,
888 "users",
889 &constraint,
890 ¤t_schema,
891 );
892 assert!(result.is_err());
893 let err_msg = result.unwrap_err().to_string();
894 assert!(err_msg.contains("Table 'users' not found in current schema"));
895 }
896
897 #[test]
898 fn test_add_constraint_check_sqlite_without_existing_check() {
899 let constraint = TableConstraint::Check {
901 name: "chk_age".into(),
902 expr: "age > 0".into(),
903 };
904 let current_schema = vec![TableDef {
905 name: "users".into(),
906 description: None,
907 columns: vec![ColumnDef {
908 name: "age".into(),
909 r#type: ColumnType::Simple(SimpleColumnType::Integer),
910 nullable: true,
911 default: None,
912 comment: None,
913 primary_key: None,
914 unique: None,
915 index: None,
916 foreign_key: None,
917 }],
918 constraints: vec![], }];
920 let result = build_add_constraint(
921 &DatabaseBackend::Sqlite,
922 "users",
923 &constraint,
924 ¤t_schema,
925 );
926 assert!(result.is_ok());
927 let queries = result.unwrap();
928 let sql = queries
929 .iter()
930 .map(|q| q.build(DatabaseBackend::Sqlite))
931 .collect::<Vec<String>>()
932 .join("\n");
933 assert!(sql.contains("CREATE TABLE"));
935 assert!(sql.contains("CONSTRAINT \"chk_age\" CHECK"));
936 }
937
938 #[test]
939 fn test_add_constraint_primary_key_sqlite_without_existing_check() {
940 let constraint = TableConstraint::PrimaryKey {
943 columns: vec!["id".into()],
944 auto_increment: false,
945 };
946 let current_schema = vec![TableDef {
947 name: "users".into(),
948 description: None,
949 columns: vec![ColumnDef {
950 name: "id".into(),
951 r#type: ColumnType::Simple(SimpleColumnType::Integer),
952 nullable: true,
953 default: None,
954 comment: None,
955 primary_key: None,
956 unique: None,
957 index: None,
958 foreign_key: None,
959 }],
960 constraints: vec![], }];
962 let result = build_add_constraint(
963 &DatabaseBackend::Sqlite,
964 "users",
965 &constraint,
966 ¤t_schema,
967 );
968 assert!(result.is_ok());
969 let queries = result.unwrap();
970 let sql = queries
971 .iter()
972 .map(|q| q.build(DatabaseBackend::Sqlite))
973 .collect::<Vec<String>>()
974 .join("\n");
975 assert!(sql.contains("CREATE TABLE"));
977 assert!(sql.contains("PRIMARY KEY"));
978 }
979
980 #[test]
981 fn test_add_constraint_foreign_key_sqlite_without_existing_check() {
982 let constraint = TableConstraint::ForeignKey {
985 name: Some("fk_user".into()),
986 columns: vec!["user_id".into()],
987 ref_table: "users".into(),
988 ref_columns: vec!["id".into()],
989 on_delete: None,
990 on_update: None,
991 };
992 let current_schema = vec![TableDef {
993 name: "posts".into(),
994 description: None,
995 columns: vec![ColumnDef {
996 name: "user_id".into(),
997 r#type: ColumnType::Simple(SimpleColumnType::Integer),
998 nullable: true,
999 default: None,
1000 comment: None,
1001 primary_key: None,
1002 unique: None,
1003 index: None,
1004 foreign_key: None,
1005 }],
1006 constraints: vec![], }];
1008 let result = build_add_constraint(
1009 &DatabaseBackend::Sqlite,
1010 "posts",
1011 &constraint,
1012 ¤t_schema,
1013 );
1014 assert!(result.is_ok());
1015 let queries = result.unwrap();
1016 let sql = queries
1017 .iter()
1018 .map(|q| q.build(DatabaseBackend::Sqlite))
1019 .collect::<Vec<String>>()
1020 .join("\n");
1021 assert!(sql.contains("CREATE TABLE"));
1023 assert!(sql.contains("FOREIGN KEY"));
1024 }
1025
1026 #[test]
1027 fn test_add_constraint_check_sqlite_with_indexes() {
1028 let constraint = TableConstraint::Check {
1029 name: "chk_age".into(),
1030 expr: "age > 0".into(),
1031 };
1032 let current_schema = vec![TableDef {
1033 name: "users".into(),
1034 description: None,
1035 columns: vec![ColumnDef {
1036 name: "age".into(),
1037 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1038 nullable: true,
1039 default: None,
1040 comment: None,
1041 primary_key: None,
1042 unique: None,
1043 index: None,
1044 foreign_key: None,
1045 }],
1046 constraints: vec![TableConstraint::Index {
1047 name: Some("idx_age".into()),
1048 columns: vec!["age".into()],
1049 }],
1050 }];
1051 let result = build_add_constraint(
1052 &DatabaseBackend::Sqlite,
1053 "users",
1054 &constraint,
1055 ¤t_schema,
1056 );
1057 assert!(result.is_ok());
1058 let queries = result.unwrap();
1059 let sql = queries
1060 .iter()
1061 .map(|q| q.build(DatabaseBackend::Sqlite))
1062 .collect::<Vec<String>>()
1063 .join("\n");
1064 assert!(sql.contains("CREATE INDEX"));
1066 assert!(sql.contains("idx_age"));
1067 }
1068
1069 #[test]
1070 fn test_add_constraint_check_sqlite_with_unique_constraint() {
1071 let constraint = TableConstraint::Check {
1073 name: "chk_age".into(),
1074 expr: "age > 0".into(),
1075 };
1076 let current_schema = vec![TableDef {
1077 name: "users".into(),
1078 description: None,
1079 columns: vec![ColumnDef {
1080 name: "age".into(),
1081 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1082 nullable: true,
1083 default: None,
1084 comment: None,
1085 primary_key: None,
1086 unique: None,
1087 index: None,
1088 foreign_key: None,
1089 }],
1090 constraints: vec![TableConstraint::Unique {
1091 name: Some("uq_age".into()),
1092 columns: vec!["age".into()],
1093 }],
1094 }];
1095 let result = build_add_constraint(
1096 &DatabaseBackend::Sqlite,
1097 "users",
1098 &constraint,
1099 ¤t_schema,
1100 );
1101 assert!(result.is_ok());
1102 let queries = result.unwrap();
1103 let sql = queries
1104 .iter()
1105 .map(|q| q.build(DatabaseBackend::Sqlite))
1106 .collect::<Vec<String>>()
1107 .join("\n");
1108 assert!(sql.contains("CREATE TABLE"));
1110 }
1111
1112 #[test]
1113 fn test_extract_check_clauses_with_mixed_constraints() {
1114 let constraints = vec![
1116 TableConstraint::Check {
1117 name: "chk1".into(),
1118 expr: "a > 0".into(),
1119 },
1120 TableConstraint::PrimaryKey {
1121 columns: vec!["id".into()],
1122 auto_increment: false,
1123 },
1124 TableConstraint::Check {
1125 name: "chk2".into(),
1126 expr: "b < 100".into(),
1127 },
1128 TableConstraint::Unique {
1129 name: Some("uq".into()),
1130 columns: vec!["email".into()],
1131 },
1132 ];
1133 let clauses = extract_check_clauses(&constraints);
1134 assert_eq!(clauses.len(), 2);
1135 assert!(clauses[0].contains("chk1"));
1136 assert!(clauses[1].contains("chk2"));
1137 }
1138
1139 #[test]
1140 fn test_extract_check_clauses_with_no_check_constraints() {
1141 let constraints = vec![
1142 TableConstraint::PrimaryKey {
1143 columns: vec!["id".into()],
1144 auto_increment: false,
1145 },
1146 TableConstraint::Unique {
1147 name: None,
1148 columns: vec!["email".into()],
1149 },
1150 ];
1151 let clauses = extract_check_clauses(&constraints);
1152 assert!(clauses.is_empty());
1153 }
1154
1155 #[test]
1156 fn test_build_create_with_checks_empty_clauses() {
1157 use super::build_create_table_for_backend;
1158
1159 let create_stmt = build_create_table_for_backend(
1160 &DatabaseBackend::Sqlite,
1161 "test_table",
1162 &[ColumnDef {
1163 name: "id".into(),
1164 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1165 nullable: false,
1166 default: None,
1167 comment: None,
1168 primary_key: None,
1169 unique: None,
1170 index: None,
1171 foreign_key: None,
1172 }],
1173 &[],
1174 );
1175
1176 let result = build_create_with_checks(&DatabaseBackend::Sqlite, &create_stmt, &[]);
1178 let sql = result.build(DatabaseBackend::Sqlite);
1179 assert!(sql.contains("CREATE TABLE"));
1180 }
1181
1182 #[test]
1183 fn test_build_create_with_checks_with_clauses() {
1184 use super::build_create_table_for_backend;
1185
1186 let create_stmt = build_create_table_for_backend(
1187 &DatabaseBackend::Sqlite,
1188 "test_table",
1189 &[ColumnDef {
1190 name: "id".into(),
1191 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1192 nullable: false,
1193 default: None,
1194 comment: None,
1195 primary_key: None,
1196 unique: None,
1197 index: None,
1198 foreign_key: None,
1199 }],
1200 &[],
1201 );
1202
1203 let check_clauses = vec!["CONSTRAINT \"chk1\" CHECK (id > 0)".to_string()];
1205 let result =
1206 build_create_with_checks(&DatabaseBackend::Sqlite, &create_stmt, &check_clauses);
1207 let sql = result.build(DatabaseBackend::Sqlite);
1208 assert!(sql.contains("CREATE TABLE"));
1209 assert!(sql.contains("CONSTRAINT \"chk1\" CHECK (id > 0)"));
1210 }
1211}