1use sea_query::{
2 Alias, ColumnDef as SeaColumnDef, ForeignKeyAction, MysqlQueryBuilder, PostgresQueryBuilder,
3 QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder,
4};
5
6use vespertide_core::{
7 ColumnDef, ColumnType, ComplexColumnType, ReferenceAction, SimpleColumnType, TableConstraint,
8};
9
10use super::create_table::build_create_table_for_backend;
11use super::types::{BuiltQuery, DatabaseBackend, RawSql};
12
13pub fn normalize_fill_with(fill_with: Option<&str>) -> Option<String> {
15 fill_with.map(|s| {
16 if s.is_empty() {
17 "''".to_string()
18 } else {
19 s.to_string()
20 }
21 })
22}
23
24pub fn build_schema_statement<T: SchemaStatementBuilder>(
26 stmt: &T,
27 backend: DatabaseBackend,
28) -> String {
29 match backend {
30 DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
31 DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
32 DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
33 }
34}
35
36pub fn build_query_statement<T: QueryStatementWriter>(
38 stmt: &T,
39 backend: DatabaseBackend,
40) -> String {
41 match backend {
42 DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
43 DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
44 DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
45 }
46}
47
48pub fn apply_column_type_with_table(col: &mut SeaColumnDef, ty: &ColumnType, table: &str) {
50 match ty {
51 ColumnType::Simple(simple) => match simple {
52 SimpleColumnType::SmallInt => {
53 col.small_integer();
54 }
55 SimpleColumnType::Integer => {
56 col.integer();
57 }
58 SimpleColumnType::BigInt => {
59 col.big_integer();
60 }
61 SimpleColumnType::Real => {
62 col.float();
63 }
64 SimpleColumnType::DoublePrecision => {
65 col.double();
66 }
67 SimpleColumnType::Text => {
68 col.text();
69 }
70 SimpleColumnType::Boolean => {
71 col.boolean();
72 }
73 SimpleColumnType::Date => {
74 col.date();
75 }
76 SimpleColumnType::Time => {
77 col.time();
78 }
79 SimpleColumnType::Timestamp => {
80 col.timestamp();
81 }
82 SimpleColumnType::Timestamptz => {
83 col.timestamp_with_time_zone();
84 }
85 SimpleColumnType::Interval => {
86 col.interval(None, None);
87 }
88 SimpleColumnType::Bytea => {
89 col.binary();
90 }
91 SimpleColumnType::Uuid => {
92 col.uuid();
93 }
94 SimpleColumnType::Json => {
95 col.json();
96 }
97 SimpleColumnType::Inet => {
98 col.custom(Alias::new("INET"));
99 }
100 SimpleColumnType::Cidr => {
101 col.custom(Alias::new("CIDR"));
102 }
103 SimpleColumnType::Macaddr => {
104 col.custom(Alias::new("MACADDR"));
105 }
106 SimpleColumnType::Xml => {
107 col.custom(Alias::new("XML"));
108 }
109 },
110 ColumnType::Complex(complex) => match complex {
111 ComplexColumnType::Varchar { length } => {
112 col.string_len(*length);
113 }
114 ComplexColumnType::Numeric { precision, scale } => {
115 col.decimal_len(*precision, *scale);
116 }
117 ComplexColumnType::Char { length } => {
118 col.char_len(*length);
119 }
120 ComplexColumnType::Custom { custom_type } => {
121 col.custom(Alias::new(custom_type));
122 }
123 ComplexColumnType::Enum { name, values } => {
124 if values.is_integer() {
126 col.integer();
127 } else {
128 let type_name = build_enum_type_name(table, name);
130 col.enumeration(
131 Alias::new(&type_name),
132 values
133 .variant_names()
134 .into_iter()
135 .map(Alias::new)
136 .collect::<Vec<Alias>>(),
137 );
138 }
139 }
140 },
141 }
142}
143
144pub fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction {
146 match action {
147 ReferenceAction::Cascade => ForeignKeyAction::Cascade,
148 ReferenceAction::Restrict => ForeignKeyAction::Restrict,
149 ReferenceAction::SetNull => ForeignKeyAction::SetNull,
150 ReferenceAction::SetDefault => ForeignKeyAction::SetDefault,
151 ReferenceAction::NoAction => ForeignKeyAction::NoAction,
152 }
153}
154
155pub fn reference_action_sql(action: &ReferenceAction) -> &'static str {
157 match action {
158 ReferenceAction::Cascade => "CASCADE",
159 ReferenceAction::Restrict => "RESTRICT",
160 ReferenceAction::SetNull => "SET NULL",
161 ReferenceAction::SetDefault => "SET DEFAULT",
162 ReferenceAction::NoAction => "NO ACTION",
163 }
164}
165
166pub fn convert_default_for_backend(default: &str, backend: &DatabaseBackend) -> String {
168 let lower = default.to_lowercase();
169
170 if lower == "gen_random_uuid()" || lower == "uuid()" || lower == "lower(hex(randomblob(16)))" {
172 return match backend {
173 DatabaseBackend::Postgres => "gen_random_uuid()".to_string(),
174 DatabaseBackend::MySql => "(UUID())".to_string(),
175 DatabaseBackend::Sqlite => "lower(hex(randomblob(16)))".to_string(),
176 };
177 }
178
179 if lower == "current_timestamp()"
181 || lower == "now()"
182 || lower == "current_timestamp"
183 || lower == "getdate()"
184 {
185 return match backend {
186 DatabaseBackend::Postgres => "CURRENT_TIMESTAMP".to_string(),
187 DatabaseBackend::MySql => "CURRENT_TIMESTAMP".to_string(),
188 DatabaseBackend::Sqlite => "CURRENT_TIMESTAMP".to_string(),
189 };
190 }
191
192 default.to_string()
193}
194
195fn is_enum_type(column_type: &ColumnType) -> bool {
197 matches!(
198 column_type,
199 ColumnType::Complex(ComplexColumnType::Enum { .. })
200 )
201}
202
203pub fn normalize_enum_default(column_type: &ColumnType, value: &str) -> String {
206 if is_enum_type(column_type) && needs_quoting(value) {
207 format!("'{}'", value)
208 } else {
209 value.to_string()
210 }
211}
212
213fn needs_quoting(default_str: &str) -> bool {
215 let trimmed = default_str.trim();
216 if trimmed.is_empty() {
218 return true;
219 }
220 if trimmed.starts_with('\'') || trimmed.starts_with('"') {
222 return false;
223 }
224 if trimmed.contains('(') || trimmed.contains(')') {
226 return false;
227 }
228 if trimmed.eq_ignore_ascii_case("null") {
230 return false;
231 }
232 if trimmed.eq_ignore_ascii_case("current_timestamp")
234 || trimmed.eq_ignore_ascii_case("current_date")
235 || trimmed.eq_ignore_ascii_case("current_time")
236 {
237 return false;
238 }
239 true
240}
241
242pub fn build_sea_column_def_with_table(
244 backend: &DatabaseBackend,
245 table: &str,
246 column: &ColumnDef,
247) -> SeaColumnDef {
248 let mut col = SeaColumnDef::new(Alias::new(&column.name));
249 apply_column_type_with_table(&mut col, &column.r#type, table);
250
251 if !column.nullable {
252 col.not_null();
253 }
254
255 if let Some(default) = &column.default {
256 let default_str = default.to_sql();
257 let converted = convert_default_for_backend(&default_str, backend);
258
259 let final_default =
261 if is_enum_type(&column.r#type) && default.is_string() && needs_quoting(&converted) {
262 format!("'{}'", converted)
263 } else {
264 converted
265 };
266
267 let final_default = if *backend == DatabaseBackend::Sqlite
270 && final_default.contains('(')
271 && !final_default.starts_with('(')
272 {
273 format!("({})", final_default)
274 } else {
275 final_default
276 };
277
278 col.default(Into::<SimpleExpr>::into(sea_query::Expr::cust(
279 final_default,
280 )));
281 }
282
283 col
284}
285
286pub fn build_create_enum_type_sql(
292 table: &str,
293 column_type: &ColumnType,
294) -> Option<super::types::RawSql> {
295 if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = column_type {
296 if values.is_integer() {
298 return None;
299 }
300
301 let values_sql = values.to_sql_values().join(", ");
302
303 let type_name = build_enum_type_name(table, name);
305
306 let pg_sql = format!("CREATE TYPE \"{}\" AS ENUM ({})", type_name, values_sql);
308
309 Some(super::types::RawSql::per_backend(
312 pg_sql,
313 String::new(),
314 String::new(),
315 ))
316 } else {
317 None
318 }
319}
320
321pub fn build_drop_enum_type_sql(
326 table: &str,
327 column_type: &ColumnType,
328) -> Option<super::types::RawSql> {
329 if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
330 let type_name = build_enum_type_name(table, name);
332
333 let pg_sql = format!("DROP TYPE \"{}\"", type_name);
335
336 Some(super::types::RawSql::per_backend(
338 pg_sql,
339 String::new(),
340 String::new(),
341 ))
342 } else {
343 None
344 }
345}
346
347pub use vespertide_naming::{
349 build_check_constraint_name, build_enum_type_name, build_foreign_key_name, build_index_name,
350 build_unique_constraint_name,
351};
352
353pub fn build_sqlite_enum_check_name(table: &str, column: &str) -> String {
355 build_check_constraint_name(table, column)
356}
357
358pub fn build_sqlite_enum_check_clause(
361 table: &str,
362 column: &str,
363 column_type: &ColumnType,
364) -> Option<String> {
365 if let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = column_type {
366 let name = build_sqlite_enum_check_name(table, column);
367 let values_sql = values.to_sql_values().join(", ");
368 Some(format!(
369 "CONSTRAINT \"{}\" CHECK (\"{}\" IN ({}))",
370 name, column, values_sql
371 ))
372 } else {
373 None
374 }
375}
376
377pub fn collect_sqlite_enum_check_clauses(table: &str, columns: &[ColumnDef]) -> Vec<String> {
379 columns
380 .iter()
381 .filter_map(|col| build_sqlite_enum_check_clause(table, &col.name, &col.r#type))
382 .collect()
383}
384
385pub fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec<String> {
388 constraints
389 .iter()
390 .filter_map(|c| {
391 if let TableConstraint::Check { name, expr } = c {
392 Some(format!("CONSTRAINT \"{}\" CHECK ({})", name, expr))
393 } else {
394 None
395 }
396 })
397 .collect()
398}
399
400pub fn collect_all_check_clauses(
407 table: &str,
408 columns: &[ColumnDef],
409 constraints: &[TableConstraint],
410) -> Vec<String> {
411 let mut clauses = collect_sqlite_enum_check_clauses(table, columns);
412 let explicit = extract_check_clauses(constraints);
413 for clause in explicit {
414 if !clauses.contains(&clause) {
415 clauses.push(clause);
416 }
417 }
418 clauses
419}
420
421pub fn build_create_with_checks(
425 backend: &DatabaseBackend,
426 create_stmt: &sea_query::TableCreateStatement,
427 check_clauses: &[String],
428) -> BuiltQuery {
429 if check_clauses.is_empty() {
430 BuiltQuery::CreateTable(Box::new(create_stmt.clone()))
431 } else {
432 let base_sql = build_schema_statement(create_stmt, *backend);
433 let mut modified_sql = base_sql;
434 if let Some(pos) = modified_sql.rfind(')') {
435 let check_sql = check_clauses.join(", ");
436 modified_sql.insert_str(pos, &format!(", {}", check_sql));
437 }
438 BuiltQuery::Raw(RawSql::per_backend(
439 modified_sql.clone(),
440 modified_sql.clone(),
441 modified_sql,
442 ))
443 }
444}
445
446pub fn build_sqlite_temp_table_create(
452 backend: &DatabaseBackend,
453 temp_table: &str,
454 table: &str,
455 columns: &[ColumnDef],
456 constraints: &[TableConstraint],
457) -> BuiltQuery {
458 let create_stmt = build_create_table_for_backend(backend, temp_table, columns, constraints);
459 let check_clauses = collect_all_check_clauses(table, columns, constraints);
460 build_create_with_checks(backend, &create_stmt, &check_clauses)
461}
462
463pub fn recreate_indexes_after_rebuild(
470 table: &str,
471 constraints: &[TableConstraint],
472 pending_constraints: &[TableConstraint],
473) -> Vec<BuiltQuery> {
474 let mut queries = Vec::new();
475 for constraint in constraints {
476 if pending_constraints.contains(constraint) {
478 continue;
479 }
480 match constraint {
481 TableConstraint::Index { name, columns } => {
482 let index_name = build_index_name(table, columns, name.as_deref());
483 let cols_sql = columns
484 .iter()
485 .map(|c| format!("\"{}\"", c))
486 .collect::<Vec<_>>()
487 .join(", ");
488 let sql = format!(
489 "CREATE INDEX \"{}\" ON \"{}\" ({})",
490 index_name, table, cols_sql
491 );
492 queries.push(BuiltQuery::Raw(RawSql::per_backend(
493 sql.clone(),
494 sql.clone(),
495 sql,
496 )));
497 }
498 TableConstraint::Unique { name, columns } => {
499 let index_name = build_unique_constraint_name(table, columns, name.as_deref());
500 let cols_sql = columns
501 .iter()
502 .map(|c| format!("\"{}\"", c))
503 .collect::<Vec<_>>()
504 .join(", ");
505 let sql = format!(
506 "CREATE UNIQUE INDEX \"{}\" ON \"{}\" ({})",
507 index_name, table, cols_sql
508 );
509 queries.push(BuiltQuery::Raw(RawSql::per_backend(
510 sql.clone(),
511 sql.clone(),
512 sql,
513 )));
514 }
515 _ => {}
516 }
517 }
518 queries
519}
520
521pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
523 if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
524 Some(name.as_str())
525 } else {
526 None
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use rstest::rstest;
534 use sea_query::{Alias, ColumnDef as SeaColumnDef, ForeignKeyAction};
535 use vespertide_core::EnumValues;
536
537 #[rstest]
538 #[case(ColumnType::Simple(SimpleColumnType::Integer))]
539 #[case(ColumnType::Simple(SimpleColumnType::BigInt))]
540 #[case(ColumnType::Simple(SimpleColumnType::Text))]
541 #[case(ColumnType::Simple(SimpleColumnType::Boolean))]
542 #[case(ColumnType::Simple(SimpleColumnType::Timestamp))]
543 #[case(ColumnType::Simple(SimpleColumnType::Uuid))]
544 #[case(ColumnType::Complex(ComplexColumnType::Varchar { length: 255 }))]
545 #[case(ColumnType::Complex(ComplexColumnType::Numeric { precision: 10, scale: 2 }))]
546 fn test_column_type_conversion(#[case] ty: ColumnType) {
547 let mut col = SeaColumnDef::new(Alias::new("test"));
549 apply_column_type_with_table(&mut col, &ty, "test_table");
550 }
551
552 #[rstest]
553 #[case(SimpleColumnType::SmallInt)]
554 #[case(SimpleColumnType::Integer)]
555 #[case(SimpleColumnType::BigInt)]
556 #[case(SimpleColumnType::Real)]
557 #[case(SimpleColumnType::DoublePrecision)]
558 #[case(SimpleColumnType::Text)]
559 #[case(SimpleColumnType::Boolean)]
560 #[case(SimpleColumnType::Date)]
561 #[case(SimpleColumnType::Time)]
562 #[case(SimpleColumnType::Timestamp)]
563 #[case(SimpleColumnType::Timestamptz)]
564 #[case(SimpleColumnType::Interval)]
565 #[case(SimpleColumnType::Bytea)]
566 #[case(SimpleColumnType::Uuid)]
567 #[case(SimpleColumnType::Json)]
568 #[case(SimpleColumnType::Inet)]
569 #[case(SimpleColumnType::Cidr)]
570 #[case(SimpleColumnType::Macaddr)]
571 #[case(SimpleColumnType::Xml)]
572 fn test_all_simple_types_cover_branches(#[case] ty: SimpleColumnType) {
573 let mut col = SeaColumnDef::new(Alias::new("t"));
574 apply_column_type_with_table(&mut col, &ColumnType::Simple(ty), "test_table");
575 }
576
577 #[rstest]
578 #[case(ComplexColumnType::Varchar { length: 42 })]
579 #[case(ComplexColumnType::Numeric { precision: 8, scale: 3 })]
580 #[case(ComplexColumnType::Char { length: 3 })]
581 #[case(ComplexColumnType::Custom { custom_type: "GEOGRAPHY".into() })]
582 #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) })]
583 fn test_all_complex_types_cover_branches(#[case] ty: ComplexColumnType) {
584 let mut col = SeaColumnDef::new(Alias::new("t"));
585 apply_column_type_with_table(&mut col, &ColumnType::Complex(ty), "test_table");
586 }
587
588 #[rstest]
589 #[case::cascade(ReferenceAction::Cascade, ForeignKeyAction::Cascade)]
590 #[case::restrict(ReferenceAction::Restrict, ForeignKeyAction::Restrict)]
591 #[case::set_null(ReferenceAction::SetNull, ForeignKeyAction::SetNull)]
592 #[case::set_default(ReferenceAction::SetDefault, ForeignKeyAction::SetDefault)]
593 #[case::no_action(ReferenceAction::NoAction, ForeignKeyAction::NoAction)]
594 fn test_reference_action_conversion(
595 #[case] action: ReferenceAction,
596 #[case] expected: ForeignKeyAction,
597 ) {
598 let result = to_sea_fk_action(&action);
600 assert!(
601 matches!(result, _expected),
602 "Expected {:?}, got {:?}",
603 expected,
604 result
605 );
606 }
607
608 #[rstest]
609 #[case(ReferenceAction::Cascade, "CASCADE")]
610 #[case(ReferenceAction::Restrict, "RESTRICT")]
611 #[case(ReferenceAction::SetNull, "SET NULL")]
612 #[case(ReferenceAction::SetDefault, "SET DEFAULT")]
613 #[case(ReferenceAction::NoAction, "NO ACTION")]
614 fn test_reference_action_sql_all_variants(
615 #[case] action: ReferenceAction,
616 #[case] expected: &str,
617 ) {
618 assert_eq!(reference_action_sql(&action), expected);
619 }
620
621 #[rstest]
622 #[case::gen_random_uuid_postgres(
623 "gen_random_uuid()",
624 DatabaseBackend::Postgres,
625 "gen_random_uuid()"
626 )]
627 #[case::gen_random_uuid_mysql("gen_random_uuid()", DatabaseBackend::MySql, "(UUID())")]
628 #[case::gen_random_uuid_sqlite(
629 "gen_random_uuid()",
630 DatabaseBackend::Sqlite,
631 "lower(hex(randomblob(16)))"
632 )]
633 #[case::current_timestamp_postgres(
634 "current_timestamp()",
635 DatabaseBackend::Postgres,
636 "CURRENT_TIMESTAMP"
637 )]
638 #[case::current_timestamp_mysql(
639 "current_timestamp()",
640 DatabaseBackend::MySql,
641 "CURRENT_TIMESTAMP"
642 )]
643 #[case::current_timestamp_sqlite(
644 "current_timestamp()",
645 DatabaseBackend::Sqlite,
646 "CURRENT_TIMESTAMP"
647 )]
648 #[case::now_postgres("now()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")]
649 #[case::now_mysql("now()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")]
650 #[case::now_sqlite("now()", DatabaseBackend::Sqlite, "CURRENT_TIMESTAMP")]
651 #[case::now_upper_postgres("NOW()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")]
652 #[case::now_upper_mysql("NOW()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")]
653 #[case::now_upper_sqlite("NOW()", DatabaseBackend::Sqlite, "CURRENT_TIMESTAMP")]
654 #[case::current_timestamp_upper_postgres(
655 "CURRENT_TIMESTAMP",
656 DatabaseBackend::Postgres,
657 "CURRENT_TIMESTAMP"
658 )]
659 #[case::current_timestamp_upper_mysql(
660 "CURRENT_TIMESTAMP",
661 DatabaseBackend::MySql,
662 "CURRENT_TIMESTAMP"
663 )]
664 #[case::current_timestamp_upper_sqlite(
665 "CURRENT_TIMESTAMP",
666 DatabaseBackend::Sqlite,
667 "CURRENT_TIMESTAMP"
668 )]
669 fn test_convert_default_for_backend(
670 #[case] default: &str,
671 #[case] backend: DatabaseBackend,
672 #[case] expected: &str,
673 ) {
674 let result = convert_default_for_backend(default, &backend);
675 assert_eq!(result, expected);
676 }
677
678 #[test]
679 fn test_is_enum_type_true() {
680 use vespertide_core::EnumValues;
681
682 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
683 name: "status".into(),
684 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
685 });
686 assert!(is_enum_type(&enum_type));
687 }
688
689 #[test]
690 fn test_is_enum_type_false() {
691 let text_type = ColumnType::Simple(SimpleColumnType::Text);
692 assert!(!is_enum_type(&text_type));
693 }
694
695 #[test]
696 fn test_get_enum_name_some() {
697 use vespertide_core::EnumValues;
698
699 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
700 name: "user_status".into(),
701 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
702 });
703 assert_eq!(get_enum_name(&enum_type), Some("user_status"));
704 }
705
706 #[test]
707 fn test_get_enum_name_none() {
708 let text_type = ColumnType::Simple(SimpleColumnType::Text);
709 assert_eq!(get_enum_name(&text_type), None);
710 }
711
712 #[test]
713 fn test_apply_column_type_integer_enum() {
714 use vespertide_core::{EnumValues, NumValue};
715 let integer_enum = ColumnType::Complex(ComplexColumnType::Enum {
716 name: "color".into(),
717 values: EnumValues::Integer(vec![
718 NumValue {
719 name: "Black".into(),
720 value: 0,
721 },
722 NumValue {
723 name: "White".into(),
724 value: 1,
725 },
726 ]),
727 });
728 let mut col = SeaColumnDef::new(Alias::new("color"));
729 apply_column_type_with_table(&mut col, &integer_enum, "test_table");
730 }
732
733 #[test]
734 fn test_build_create_enum_type_sql_integer_enum_returns_none() {
735 use vespertide_core::{EnumValues, NumValue};
736 let integer_enum = ColumnType::Complex(ComplexColumnType::Enum {
737 name: "priority".into(),
738 values: EnumValues::Integer(vec![
739 NumValue {
740 name: "Low".into(),
741 value: 0,
742 },
743 NumValue {
744 name: "High".into(),
745 value: 10,
746 },
747 ]),
748 });
749 assert!(build_create_enum_type_sql("test_table", &integer_enum).is_none());
751 }
752
753 #[rstest]
754 #[case::empty("", true)]
756 #[case::whitespace_only(" ", true)]
757 #[case::now_func("now()", false)]
759 #[case::coalesce_func("COALESCE(old_value, 'default')", false)]
760 #[case::uuid_func("gen_random_uuid()", false)]
761 #[case::null_upper("NULL", false)]
763 #[case::null_lower("null", false)]
764 #[case::null_mixed("Null", false)]
765 #[case::current_timestamp_upper("CURRENT_TIMESTAMP", false)]
767 #[case::current_timestamp_lower("current_timestamp", false)]
768 #[case::current_date_upper("CURRENT_DATE", false)]
769 #[case::current_date_lower("current_date", false)]
770 #[case::current_time_upper("CURRENT_TIME", false)]
771 #[case::current_time_lower("current_time", false)]
772 #[case::single_quoted("'active'", false)]
774 #[case::double_quoted("\"active\"", false)]
775 #[case::plain_active("active", true)]
777 #[case::plain_pending("pending", true)]
778 #[case::plain_underscore("some_value", true)]
779 fn test_needs_quoting(#[case] input: &str, #[case] expected: bool) {
780 assert_eq!(needs_quoting(input), expected);
781 }
782
783 #[test]
784 fn test_recreate_indexes_after_rebuild_skips_pending() {
785 use vespertide_core::TableConstraint;
786 let idx1 = TableConstraint::Index {
787 name: Some("idx_a".into()),
788 columns: vec!["a".into()],
789 };
790 let idx2 = TableConstraint::Index {
791 name: Some("idx_b".into()),
792 columns: vec!["b".into()],
793 };
794 let uq1 = TableConstraint::Unique {
795 name: Some("uq_c".into()),
796 columns: vec!["c".into()],
797 };
798
799 let constraints = vec![idx1.clone(), idx2.clone(), uq1.clone()];
801 let pending = vec![idx1.clone(), uq1.clone()];
802
803 let queries = recreate_indexes_after_rebuild("t", &constraints, &pending);
804 assert_eq!(queries.len(), 1);
806 let sql = queries[0].build(DatabaseBackend::Sqlite);
807 assert!(sql.contains("idx_b"));
808 }
809
810 #[test]
811 fn test_recreate_indexes_after_rebuild_no_pending() {
812 use vespertide_core::TableConstraint;
813 let idx = TableConstraint::Index {
814 name: Some("idx_a".into()),
815 columns: vec!["a".into()],
816 };
817 let uq = TableConstraint::Unique {
818 name: Some("uq_b".into()),
819 columns: vec!["b".into()],
820 };
821
822 let queries = recreate_indexes_after_rebuild("t", &[idx, uq], &[]);
823 assert_eq!(queries.len(), 2);
824 }
825
826 #[test]
827 fn test_recreate_indexes_after_rebuild_skips_non_index_constraints() {
828 use vespertide_core::TableConstraint;
829 let pk = TableConstraint::PrimaryKey {
830 columns: vec!["id".into()],
831 auto_increment: false,
832 };
833 let fk = TableConstraint::ForeignKey {
834 name: None,
835 columns: vec!["uid".into()],
836 ref_table: "u".into(),
837 ref_columns: vec!["id".into()],
838 on_delete: None,
839 on_update: None,
840 };
841 let chk = TableConstraint::Check {
842 name: "chk".into(),
843 expr: "id > 0".into(),
844 };
845
846 let queries = recreate_indexes_after_rebuild("t", &[pk, fk, chk], &[]);
847 assert_eq!(queries.len(), 0);
848 }
849}