1use sea_query::{Alias, ForeignKey, Index, Table, TableCreateStatement};
2
3use vespertide_core::{ColumnDef, ColumnType, ComplexColumnType, TableConstraint};
4
5use super::helpers::{
6 build_create_enum_type_sql, build_create_with_checks, build_sea_column_def_with_table,
7 collect_sqlite_enum_check_clauses, to_sea_fk_action,
8};
9use super::types::{BuiltQuery, DatabaseBackend};
10use crate::error::QueryError;
11
12pub(crate) fn build_create_table_for_backend(
13 backend: DatabaseBackend,
14 table: &str,
15 columns: &[ColumnDef],
16 constraints: &[TableConstraint],
17) -> TableCreateStatement {
18 let mut stmt = Table::create().table(Alias::new(table)).to_owned();
19 add_create_table_columns(&mut stmt, backend, table, columns, constraints);
20 add_create_table_constraints(&mut stmt, backend, table, columns, constraints);
21 stmt
22}
23
24fn add_create_table_columns(
25 stmt: &mut TableCreateStatement,
26 backend: DatabaseBackend,
27 table: &str,
28 columns: &[ColumnDef],
29 constraints: &[TableConstraint],
30) {
31 let has_table_primary_key = constraints
32 .iter()
33 .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
34 let auto_increment_columns = if constraints.iter().any(|c| {
38 matches!(
39 c,
40 TableConstraint::PrimaryKey {
41 auto_increment: true,
42 ..
43 }
44 )
45 }) {
46 collect_auto_increment_columns(constraints)
47 } else {
48 std::collections::HashSet::new()
49 };
50
51 for column in columns {
52 let mut col = build_sea_column_def_with_table(backend, table, column);
53 if column.primary_key.is_some() && !has_table_primary_key {
54 col.primary_key();
55 }
56 if auto_increment_columns.contains(column.name.as_str())
57 && column.r#type.supports_auto_increment()
58 {
59 if matches!(backend, DatabaseBackend::Sqlite) {
60 col.primary_key();
61 }
62 col.auto_increment();
63 }
64 stmt.col(col);
65 }
66}
67
68fn collect_auto_increment_columns(
69 constraints: &[TableConstraint],
70) -> std::collections::HashSet<&str> {
71 constraints
72 .iter()
73 .filter_map(|c| {
74 if let TableConstraint::PrimaryKey {
75 columns: pk_cols,
76 auto_increment: true,
77 ..
78 } = c
79 {
80 Some(pk_cols.iter().map(AsRef::as_ref))
81 } else {
82 None
83 }
84 })
85 .flatten()
86 .collect()
87}
88
89fn add_create_table_constraints(
90 stmt: &mut TableCreateStatement,
91 backend: DatabaseBackend,
92 table: &str,
93 columns: &[ColumnDef],
94 constraints: &[TableConstraint],
95) {
96 for constraint in constraints {
97 match constraint {
98 TableConstraint::PrimaryKey {
99 columns: pk_cols,
100 auto_increment,
101 ..
102 } => add_primary_key_constraint(stmt, backend, columns, pk_cols, *auto_increment),
103 TableConstraint::Unique {
104 name,
105 columns: unique_cols,
106 ..
107 } => add_mysql_unique_constraint(stmt, backend, table, unique_cols, name.as_deref()),
108 TableConstraint::ForeignKey { .. } => {
109 add_foreign_key_constraint(stmt, table, constraint);
110 }
111 TableConstraint::Check { .. } | TableConstraint::Index { .. } => {}
114 _ => {
115 unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above")
116 }
117 }
118 }
119}
120
121fn add_primary_key_constraint(
122 stmt: &mut TableCreateStatement,
123 backend: DatabaseBackend,
124 columns: &[ColumnDef],
125 pk_cols: &[impl AsRef<str>],
126 auto_increment: bool,
127) {
128 if should_skip_sqlite_auto_increment_pk(backend, columns, pk_cols, auto_increment) {
129 return;
130 }
131
132 let mut pk_idx = Index::create();
133 for c in pk_cols {
134 pk_idx.col(Alias::new(c.as_ref()));
135 }
136 stmt.primary_key(&mut pk_idx);
137}
138
139fn should_skip_sqlite_auto_increment_pk(
140 backend: DatabaseBackend,
141 columns: &[ColumnDef],
142 pk_cols: &[impl AsRef<str>],
143 auto_increment: bool,
144) -> bool {
145 matches!(backend, DatabaseBackend::Sqlite)
146 && auto_increment
147 && pk_cols.iter().all(|col_name| {
148 columns
149 .iter()
150 .any(|c| c.name == col_name.as_ref() && c.r#type.supports_auto_increment())
151 })
152}
153
154fn add_mysql_unique_constraint(
155 stmt: &mut TableCreateStatement,
156 backend: DatabaseBackend,
157 table: &str,
158 unique_cols: &[impl AsRef<str>],
159 name: Option<&str>,
160) {
161 if !matches!(backend, DatabaseBackend::MySql) {
162 return;
163 }
164
165 let index_name = super::helpers::build_unique_constraint_name(table, unique_cols, name);
166 let mut idx = Index::create().name(&index_name).unique().to_owned();
167 for col in unique_cols {
168 idx.col(Alias::new(col.as_ref()));
169 }
170 stmt.index(&mut idx);
171}
172
173fn add_foreign_key_constraint(
174 stmt: &mut TableCreateStatement,
175 table: &str,
176 constraint: &TableConstraint,
177) {
178 if let TableConstraint::ForeignKey {
179 name,
180 columns: fk_cols,
181 ref_table,
182 ref_columns,
183 on_delete,
184 on_update,
185 ..
186 } = constraint
187 {
188 let fk_name = super::helpers::build_foreign_key_name(table, fk_cols, name.as_deref());
189 let mut fk = ForeignKey::create().name(&fk_name).to_owned();
190 fk.from_tbl(Alias::new(table));
191 for col in fk_cols {
192 fk.from_col(Alias::new(col));
193 }
194 fk.to_tbl(Alias::new(ref_table));
195 for col in ref_columns {
196 fk.to_col(Alias::new(col));
197 }
198 if let Some(action) = on_delete {
199 fk.on_delete(to_sea_fk_action(action));
200 }
201 if let Some(action) = on_update {
202 fk.on_update(to_sea_fk_action(action));
203 }
204 stmt.foreign_key(&mut fk);
205 }
206}
207
208pub fn build_create_table(
209 backend: DatabaseBackend,
210 table: &str,
211 columns: &[ColumnDef],
212 constraints: &[TableConstraint],
213) -> Result<Vec<BuiltQuery>, QueryError> {
214 let table_def = vespertide_core::TableDef {
217 description: None,
218 name: table.into(),
219 columns: columns.to_vec(),
220 constraints: constraints.to_vec(),
221 };
222 let normalized = table_def.normalize().map_err(|e| {
223 QueryError::SchemaError(format!("Failed to normalize table '{table}': {e}"))
224 })?;
225
226 let columns = &normalized.columns;
228 let constraints = &normalized.constraints;
229
230 let mut queries = Vec::with_capacity(1 + constraints.len());
235
236 let has_enum_columns = columns.iter().any(|c| {
242 matches!(
243 c.r#type,
244 ColumnType::Complex(ComplexColumnType::Enum { .. })
245 )
246 });
247 if has_enum_columns {
248 let mut created_enums: std::collections::HashSet<&str> = std::collections::HashSet::new();
249 for column in columns {
250 if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = &column.r#type
251 && created_enums.insert(name.as_str())
252 && let Some(create_type_sql) = build_create_enum_type_sql(table, &column.r#type)
253 {
254 queries.push(BuiltQuery::Raw(create_type_sql));
255 }
256 }
257 }
258
259 let has_unique_constraints = constraints
266 .iter()
267 .any(|c| matches!(c, TableConstraint::Unique { .. }));
268 let create_table_stmt = if matches!(backend, DatabaseBackend::MySql) || !has_unique_constraints
269 {
270 build_create_table_for_backend(backend, table, columns, constraints)
274 } else {
275 let table_constraints_owned: Vec<TableConstraint> = constraints
278 .iter()
279 .filter(|c| !matches!(c, TableConstraint::Unique { .. }))
280 .cloned()
281 .collect();
282 build_create_table_for_backend(backend, table, columns, &table_constraints_owned)
283 };
284
285 if matches!(backend, DatabaseBackend::Sqlite) {
287 let enum_check_clauses = collect_sqlite_enum_check_clauses(table, columns);
288 if enum_check_clauses.is_empty() {
289 queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
290 } else {
291 queries.push(build_create_with_checks(
294 backend,
295 &create_table_stmt,
296 &enum_check_clauses,
297 ));
298 }
299 } else {
300 queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
301 }
302
303 if matches!(backend, DatabaseBackend::Postgres | DatabaseBackend::Sqlite) {
305 for constraint in constraints {
306 if let TableConstraint::Unique {
307 name,
308 columns: unique_cols,
309 ..
310 } = constraint
311 {
312 let index_name = super::helpers::build_unique_constraint_name(
314 table,
315 unique_cols,
316 name.as_deref(),
317 );
318 let mut idx = Index::create()
319 .table(Alias::new(table))
320 .name(&index_name)
321 .unique()
322 .to_owned();
323 for col in unique_cols {
324 idx.col(Alias::new(col));
325 }
326 queries.push(BuiltQuery::CreateIndex(Box::new(idx)));
327 }
328 }
329 }
330
331 for constraint in constraints {
333 if let TableConstraint::Index {
334 name,
335 columns: index_cols,
336 } = constraint
337 {
338 let index_name = super::helpers::build_index_name(table, index_cols, name.as_deref());
340 let mut idx = Index::create()
341 .table(Alias::new(table))
342 .name(&index_name)
343 .to_owned();
344 for col in index_cols {
345 idx.col(Alias::new(col));
346 }
347 queries.push(BuiltQuery::CreateIndex(Box::new(idx)));
348 }
349 }
350
351 Ok(queries)
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::test_support::{col, joined_sql};
358 use insta::{assert_snapshot, with_settings};
359 use rstest::rstest;
360 use vespertide_core::{ColumnType, EnumValues, SimpleColumnType};
361
362 fn join_queries(queries: &[BuiltQuery], backend: DatabaseBackend, separator: &str) -> String {
363 let mut sql = String::new();
364 for (index, query) in queries.iter().enumerate() {
365 if index > 0 {
366 sql.push_str(separator);
367 }
368 sql.push_str(&query.build(backend));
369 }
370 sql
371 }
372
373 #[rstest]
374 #[case::create_table_postgres(
375 "create_table_postgres",
376 DatabaseBackend::Postgres,
377 &["CREATE TABLE \"users\" ( \"id\" integer )"]
378 )]
379 #[case::create_table_mysql(
380 "create_table_mysql",
381 DatabaseBackend::MySql,
382 &["CREATE TABLE `users` ( `id` int )"]
383 )]
384 #[case::create_table_sqlite(
385 "create_table_sqlite",
386 DatabaseBackend::Sqlite,
387 &["CREATE TABLE \"users\" ( \"id\" integer )"]
388 )]
389 fn test_create_table(
390 #[case] title: &str,
391 #[case] backend: DatabaseBackend,
392 #[case] expected: &[&str],
393 ) {
394 let result = build_create_table(
395 backend,
396 "users",
397 &[col("id", ColumnType::Simple(SimpleColumnType::Integer))],
398 &[],
399 )
400 .unwrap();
401 let sql = join_queries(&result, backend, "\n");
402 for exp in expected {
403 assert!(
404 sql.contains(exp),
405 "Expected SQL to contain '{exp}', got: {sql}"
406 );
407 }
408
409 with_settings!({ snapshot_suffix => format!("create_table_{}", title) }, {
410 assert_snapshot!(sql);
411 });
412 }
413
414 #[rstest]
415 #[case::inline_unique_postgres(DatabaseBackend::Postgres)]
416 #[case::inline_unique_mysql(DatabaseBackend::MySql)]
417 #[case::inline_unique_sqlite(DatabaseBackend::Sqlite)]
418 fn test_create_table_with_inline_unique(#[case] backend: DatabaseBackend) {
419 use vespertide_core::schema::str_or_bool::StrOrBoolOrArray;
422
423 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
424 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
425
426 let result = build_create_table(
427 backend,
428 "users",
429 &[
430 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
431 email_col,
432 ],
433 &[],
435 )
436 .unwrap();
437 let sql = join_queries(&result, backend, "\n");
438
439 assert!(
441 sql.contains("UNIQUE") || sql.to_uppercase().contains("UNIQUE"),
442 "Normalized unique constraint should be in SQL, but not found: {sql}"
443 );
444 with_settings!({ snapshot_suffix => format!("create_table_with_inline_unique_{:?}", backend) }, {
445 assert_snapshot!(sql);
446 });
447 }
448
449 #[rstest]
450 #[case::table_level_unique_postgres(DatabaseBackend::Postgres)]
451 #[case::table_level_unique_mysql(DatabaseBackend::MySql)]
452 #[case::table_level_unique_sqlite(DatabaseBackend::Sqlite)]
453 fn test_create_table_with_table_level_unique(#[case] backend: DatabaseBackend) {
454 let result = build_create_table(
456 backend,
457 "users",
458 &[
459 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
460 col("email", ColumnType::Simple(SimpleColumnType::Text)),
461 ],
462 &[TableConstraint::Unique {
463 name: Some("uq_email".into()),
464 columns: vec!["email".into()],
465 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
466 keep: vespertide_core::KeepPolicy::First,
467 },
468 }],
469 )
470 .unwrap();
471 let sql = join_queries(&result, backend, "\n");
472 assert!(sql.contains("CREATE TABLE"));
473 match backend {
475 DatabaseBackend::MySql => {
476 assert!(
477 sql.contains("UNIQUE"),
478 "MySQL should have UNIQUE in CREATE TABLE: {sql}"
479 );
480 }
481 _ => {
482 assert!(
484 sql.contains("CREATE UNIQUE INDEX"),
485 "Postgres/SQLite should have CREATE UNIQUE INDEX: {sql}"
486 );
487 }
488 }
489 with_settings!({ snapshot_suffix => format!("create_table_with_table_level_unique_{:?}", backend) }, {
490 assert_snapshot!(sql);
491 });
492 }
493
494 #[rstest]
500 #[case::unique_beside_pk_postgres(DatabaseBackend::Postgres)]
501 #[case::unique_beside_pk_mysql(DatabaseBackend::MySql)]
502 #[case::unique_beside_pk_sqlite(DatabaseBackend::Sqlite)]
503 fn test_create_table_unique_alongside_primary_key(#[case] backend: DatabaseBackend) {
504 let result = build_create_table(
505 backend,
506 "users",
507 &[
508 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
509 col("email", ColumnType::Simple(SimpleColumnType::Text)),
510 ],
511 &[
512 TableConstraint::PrimaryKey {
513 columns: vec!["id".into()],
514 auto_increment: false,
515 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
516 },
517 TableConstraint::Unique {
518 name: Some("uq_email".into()),
519 columns: vec!["email".into()],
520 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
521 keep: vespertide_core::KeepPolicy::First,
522 },
523 },
524 ],
525 )
526 .unwrap();
527 let sql = join_queries(&result, backend, "\n");
528
529 assert!(
530 sql.contains("PRIMARY KEY"),
531 "the PK must survive the unique partitioning: {sql}"
532 );
533 with_settings!({ snapshot_suffix => format!("create_table_unique_alongside_primary_key_{:?}", backend) }, {
534 assert_snapshot!(sql);
535 });
536 }
537
538 #[rstest]
539 #[case::table_level_unique_no_name_postgres(DatabaseBackend::Postgres)]
540 #[case::table_level_unique_no_name_mysql(DatabaseBackend::MySql)]
541 #[case::table_level_unique_no_name_sqlite(DatabaseBackend::Sqlite)]
542 fn test_create_table_with_table_level_unique_no_name(#[case] backend: DatabaseBackend) {
543 let result = build_create_table(
545 backend,
546 "users",
547 &[
548 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
549 col("email", ColumnType::Simple(SimpleColumnType::Text)),
550 ],
551 &[TableConstraint::Unique {
552 name: None,
553 columns: vec!["email".into()],
554 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
555 keep: vespertide_core::KeepPolicy::First,
556 },
557 }],
558 )
559 .unwrap();
560 let sql = join_queries(&result, backend, "\n");
561 assert!(sql.contains("CREATE TABLE"));
562 match backend {
564 DatabaseBackend::MySql => {
565 assert!(
566 sql.contains("UNIQUE"),
567 "MySQL should have UNIQUE in CREATE TABLE: {sql}"
568 );
569 }
570 _ => {
571 assert!(
573 sql.contains("CREATE UNIQUE INDEX"),
574 "Postgres/SQLite should have CREATE UNIQUE INDEX: {sql}"
575 );
576 }
577 }
578 with_settings!({ snapshot_suffix => format!("create_table_with_table_level_unique_no_name_{:?}", backend) }, {
579 assert_snapshot!(sql);
580 });
581 }
582
583 #[rstest]
584 #[case::postgres(DatabaseBackend::Postgres)]
585 #[case::mysql(DatabaseBackend::MySql)]
586 #[case::sqlite(DatabaseBackend::Sqlite)]
587 fn test_create_table_with_enum_column(#[case] backend: DatabaseBackend) {
588 let columns = vec![
590 ColumnDef {
591 name: "id".into(),
592 r#type: ColumnType::Simple(SimpleColumnType::Integer),
593 nullable: false,
594 default: None,
595 comment: None,
596 primary_key: None,
597 unique: None,
598 index: None,
599 foreign_key: None,
600 },
601 ColumnDef {
602 name: "status".into(),
603 r#type: ColumnType::Complex(ComplexColumnType::Enum {
604 name: "user_status".into(),
605 values: EnumValues::String(vec![
606 "active".into(),
607 "inactive".into(),
608 "pending".into(),
609 ]),
610 }),
611 nullable: false,
612 default: Some("'active'".into()),
613 comment: None,
614 primary_key: None,
615 unique: None,
616 index: None,
617 foreign_key: None,
618 },
619 ];
620 let constraints = vec![TableConstraint::PrimaryKey {
621 auto_increment: false,
622 columns: vec!["id".into()],
623 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
624 }];
625
626 let result = build_create_table(backend, "users", &columns, &constraints);
627 assert!(result.is_ok());
628 let queries = result.unwrap();
629 let sql = join_queries(&queries, backend, ";\n");
630
631 with_settings!({ snapshot_suffix => format!("create_table_with_enum_column_{:?}", backend) }, {
632 assert_snapshot!(sql);
633 });
634 }
635
636 #[rstest]
637 #[case::auto_increment_postgres(DatabaseBackend::Postgres)]
638 #[case::auto_increment_mysql(DatabaseBackend::MySql)]
639 #[case::auto_increment_sqlite(DatabaseBackend::Sqlite)]
640 fn test_create_table_with_auto_increment_primary_key(#[case] backend: DatabaseBackend) {
641 let columns = vec![ColumnDef {
643 name: "id".into(),
644 r#type: ColumnType::Simple(SimpleColumnType::Integer),
645 nullable: false,
646 default: None,
647 comment: None,
648 primary_key: None,
649 unique: None,
650 index: None,
651 foreign_key: None,
652 }];
653 let constraints = vec![TableConstraint::PrimaryKey {
654 auto_increment: true,
655 columns: vec!["id".into()],
656 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
657 }];
658
659 let result = build_create_table(backend, "users", &columns, &constraints);
660 assert!(result.is_ok());
661 let queries = result.unwrap();
662 let sql = join_queries(&queries, backend, ";\n");
663
664 match backend {
666 DatabaseBackend::Postgres => {
667 assert!(
672 sql.contains("SERIAL")
673 || sql.contains("serial")
674 || sql.contains("IDENTITY")
675 || sql.contains("identity"),
676 "PostgreSQL should use SERIAL or IDENTITY for auto_increment, got: {sql}"
677 );
678 }
679 DatabaseBackend::MySql => {
680 assert!(
681 sql.contains("AUTO_INCREMENT") || sql.contains("auto_increment"),
682 "MySQL should use AUTO_INCREMENT for auto_increment, got: {sql}"
683 );
684 }
685 DatabaseBackend::Sqlite => {
686 assert!(
687 sql.contains("AUTOINCREMENT") || sql.contains("autoincrement"),
688 "SQLite should use AUTOINCREMENT for auto_increment, got: {sql}"
689 );
690 }
691 }
692
693 with_settings!({ snapshot_suffix => format!("create_table_with_auto_increment_{:?}", backend) }, {
694 assert_snapshot!(sql);
695 });
696 }
697
698 #[rstest]
699 #[case::auto_increment_unsupported_type_postgres(DatabaseBackend::Postgres)]
700 #[case::auto_increment_unsupported_type_mysql(DatabaseBackend::MySql)]
701 #[case::auto_increment_unsupported_type_sqlite(DatabaseBackend::Sqlite)]
702 fn test_create_table_auto_increment_pk_on_non_integer_column(#[case] backend: DatabaseBackend) {
703 let columns = vec![col("id", ColumnType::Simple(SimpleColumnType::Text))];
709 let constraints = vec![TableConstraint::PrimaryKey {
710 auto_increment: true,
711 columns: vec!["id".into()],
712 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
713 }];
714
715 let queries = build_create_table(backend, "users", &columns, &constraints).unwrap();
716 let sql = join_queries(&queries, backend, ";\n");
717
718 assert!(
719 sql.to_uppercase().contains("PRIMARY KEY"),
720 "auto-increment PK on a type without auto-increment support must still \
721 emit an explicit PRIMARY KEY, got: {sql}"
722 );
723
724 with_settings!({ snapshot_suffix => format!("create_table_auto_increment_pk_on_non_integer_column_{:?}", backend) }, {
725 assert_snapshot!(sql);
726 });
727 }
728
729 #[rstest]
730 #[case::inline_auto_increment_postgres(DatabaseBackend::Postgres)]
731 #[case::inline_auto_increment_mysql(DatabaseBackend::MySql)]
732 #[case::inline_auto_increment_sqlite(DatabaseBackend::Sqlite)]
733 fn test_create_table_with_inline_auto_increment_primary_key(#[case] backend: DatabaseBackend) {
734 use vespertide_core::schema::primary_key::{PrimaryKeyDef, PrimaryKeySyntax};
736
737 let columns = vec![ColumnDef {
738 name: "id".into(),
739 r#type: ColumnType::Simple(SimpleColumnType::Integer),
740 nullable: false,
741 default: None,
742 comment: None,
743 primary_key: Some(PrimaryKeySyntax::Object(PrimaryKeyDef {
744 auto_increment: true,
745 })),
746 unique: None,
747 index: None,
748 foreign_key: None,
749 }];
750
751 let result = build_create_table(backend, "users", &columns, &[]);
752 assert!(result.is_ok());
753 let queries = result.unwrap();
754 let sql = join_queries(&queries, backend, ";\n");
755
756 match backend {
758 DatabaseBackend::Postgres => {
759 assert!(
764 sql.contains("SERIAL")
765 || sql.contains("serial")
766 || sql.contains("IDENTITY")
767 || sql.contains("identity"),
768 "PostgreSQL should use SERIAL or IDENTITY for auto_increment, got: {sql}"
769 );
770 }
771 DatabaseBackend::MySql => {
772 assert!(
773 sql.contains("AUTO_INCREMENT") || sql.contains("auto_increment"),
774 "MySQL should use AUTO_INCREMENT for auto_increment, got: {sql}"
775 );
776 }
777 DatabaseBackend::Sqlite => {
778 assert!(
779 sql.contains("AUTOINCREMENT") || sql.contains("autoincrement"),
780 "SQLite should use AUTOINCREMENT for auto_increment, got: {sql}"
781 );
782 }
783 }
784
785 with_settings!({ snapshot_suffix => format!("create_table_with_inline_auto_increment_{:?}", backend) }, {
786 assert_snapshot!(sql);
787 });
788 }
789
790 #[rstest]
793 #[case::timestamp_now_default_postgres(DatabaseBackend::Postgres)]
794 #[case::timestamp_now_default_mysql(DatabaseBackend::MySql)]
795 #[case::timestamp_now_default_sqlite(DatabaseBackend::Sqlite)]
796 #[rstest]
802 #[case::postgres(DatabaseBackend::Postgres)]
803 #[case::mysql(DatabaseBackend::MySql)]
804 #[case::sqlite(DatabaseBackend::Sqlite)]
805 fn test_create_table_check_and_index_constraints_are_skipped(#[case] backend: DatabaseBackend) {
806 let constraints = vec![
809 TableConstraint::PrimaryKey {
810 columns: vec!["id".into()],
811 auto_increment: false,
812 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
813 },
814 TableConstraint::Check {
815 name: "chk_age".into(),
816 expr: "age > 0".into(),
817 strategy: vespertide_core::CheckViolationStrategy::default(),
818 },
819 TableConstraint::Index {
820 name: Some("idx_age".into()),
821 columns: vec!["age".into()],
822 },
823 ];
824 let columns = vec![
825 ColumnDef {
826 name: "id".into(),
827 r#type: ColumnType::Simple(SimpleColumnType::Integer),
828 nullable: false,
829 default: None,
830 comment: None,
831 primary_key: None,
832 unique: None,
833 index: None,
834 foreign_key: None,
835 },
836 ColumnDef {
837 name: "age".into(),
838 r#type: ColumnType::Simple(SimpleColumnType::Integer),
839 nullable: false,
840 default: None,
841 comment: None,
842 primary_key: None,
843 unique: None,
844 index: None,
845 foreign_key: None,
846 },
847 ];
848 let queries = build_create_table(backend, "users", &columns, &constraints).unwrap();
849 let sql = joined_sql(backend, &queries);
850 assert!(sql.contains("PRIMARY KEY"));
855 assert!(!sql.contains("CHECK (age > 0)"));
856 assert!(sql.contains("CREATE INDEX") || sql.contains("idx_age"));
859 }
860
861 #[rstest]
862 #[case::postgres(DatabaseBackend::Postgres)]
863 #[case::mysql(DatabaseBackend::MySql)]
864 #[case::sqlite(DatabaseBackend::Sqlite)]
865 fn test_create_table_with_timestamp_now_default(#[case] backend: DatabaseBackend) {
866 let columns = vec![
867 ColumnDef {
868 name: "id".into(),
869 r#type: ColumnType::Simple(SimpleColumnType::BigInt),
870 nullable: false,
871 default: None,
872 comment: None,
873 primary_key: None,
874 unique: None,
875 index: None,
876 foreign_key: None,
877 },
878 ColumnDef {
879 name: "created_at".into(),
880 r#type: ColumnType::Simple(SimpleColumnType::Timestamptz),
881 nullable: false,
882 default: Some("NOW()".into()), comment: None,
884 primary_key: None,
885 unique: None,
886 index: None,
887 foreign_key: None,
888 },
889 ];
890
891 let result = build_create_table(backend, "events", &columns, &[]);
892 assert!(result.is_ok(), "build_create_table failed: {result:?}");
893 let queries = result.unwrap();
894 let sql = join_queries(&queries, backend, "\n");
895
896 if matches!(backend, DatabaseBackend::Sqlite) {
898 assert!(
899 !sql.contains("NOW()"),
900 "SQLite should not contain NOW(), got: {sql}"
901 );
902 assert!(
903 sql.contains("CURRENT_TIMESTAMP"),
904 "SQLite should use CURRENT_TIMESTAMP, got: {sql}"
905 );
906 }
907
908 with_settings!({ snapshot_suffix => format!("create_table_with_timestamp_now_default_{:?}", backend) }, {
909 assert_snapshot!(sql);
910 });
911 }
912}