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 if let Some((value, cast_type)) = parse_pg_type_cast(default) {
194 return convert_type_cast(&value, &cast_type, backend);
195 }
196
197 default.to_string()
198}
199
200fn parse_pg_type_cast(expr: &str) -> Option<(String, String)> {
203 let trimmed = expr.trim();
204
205 if let Some(after_open) = trimmed.strip_prefix('\'') {
207 let mut i = 0;
209 let bytes = after_open.as_bytes();
210 while i < bytes.len() {
211 if bytes[i] == b'\'' {
212 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
214 i += 2;
215 continue;
216 }
217 let value_end = i + 1; let rest = &after_open[value_end..];
220 if let Some(stripped) = rest.strip_prefix("::") {
221 let cast_type = stripped.trim().to_lowercase();
222 if !cast_type.is_empty() {
223 let value = format!("'{}'", &after_open[..i]);
224 return Some((value, cast_type));
225 }
226 }
227 return None;
228 }
229 i += 1;
230 }
231 return None;
232 }
233
234 if let Some(pos) = trimmed.find("::") {
236 let value = trimmed[..pos].trim().to_string();
237 let cast_type = trimmed[pos + 2..].trim().to_lowercase();
238 if !value.is_empty() && !cast_type.is_empty() {
239 return Some((value, cast_type));
240 }
241 }
242
243 None
244}
245
246fn pg_type_to_mysql_cast(pg_type: &str) -> &'static str {
248 match pg_type {
249 "json" | "jsonb" => "JSON",
250 "text" | "varchar" | "char" | "character varying" => "CHAR",
251 "integer" | "int" | "int4" | "smallint" | "int2" => "SIGNED",
252 "bigint" | "int8" => "SIGNED",
253 "real" | "float4" | "double precision" | "float8" => "DECIMAL",
254 "boolean" | "bool" => "UNSIGNED",
255 "date" => "DATE",
256 "time" => "TIME",
257 "timestamp"
258 | "timestamptz"
259 | "timestamp with time zone"
260 | "timestamp without time zone" => "DATETIME",
261 "numeric" | "decimal" => "DECIMAL",
262 "bytea" => "BINARY",
263 _ => "CHAR",
264 }
265}
266
267fn convert_type_cast(value: &str, cast_type: &str, backend: &DatabaseBackend) -> String {
269 match backend {
270 DatabaseBackend::Postgres => format!("{}::{}", value, cast_type),
272 DatabaseBackend::MySql => {
274 let mysql_type = pg_type_to_mysql_cast(cast_type);
275 format!("CAST({} AS {})", value, mysql_type)
276 }
277 DatabaseBackend::Sqlite => value.to_string(),
279 }
280}
281
282fn is_enum_type(column_type: &ColumnType) -> bool {
284 matches!(
285 column_type,
286 ColumnType::Complex(ComplexColumnType::Enum { .. })
287 )
288}
289
290pub fn normalize_enum_default(column_type: &ColumnType, value: &str) -> String {
293 if is_enum_type(column_type) && needs_quoting(value) {
294 format!("'{}'", value)
295 } else {
296 value.to_string()
297 }
298}
299
300fn needs_quoting(default_str: &str) -> bool {
302 let trimmed = default_str.trim();
303 if trimmed.is_empty() {
305 return true;
306 }
307 if trimmed.starts_with('\'') || trimmed.starts_with('"') {
309 return false;
310 }
311 if trimmed.contains('(') || trimmed.contains(')') {
313 return false;
314 }
315 if trimmed.eq_ignore_ascii_case("null") {
317 return false;
318 }
319 if trimmed.eq_ignore_ascii_case("current_timestamp")
321 || trimmed.eq_ignore_ascii_case("current_date")
322 || trimmed.eq_ignore_ascii_case("current_time")
323 {
324 return false;
325 }
326 true
327}
328
329pub fn build_sea_column_def_with_table(
331 backend: &DatabaseBackend,
332 table: &str,
333 column: &ColumnDef,
334) -> SeaColumnDef {
335 let mut col = SeaColumnDef::new(Alias::new(&column.name));
336 apply_column_type_with_table(&mut col, &column.r#type, table);
337
338 if !column.nullable {
339 col.not_null();
340 }
341
342 if let Some(default) = &column.default {
343 let default_str = default.to_sql();
344 let converted = convert_default_for_backend(&default_str, backend);
345
346 let final_default =
348 if is_enum_type(&column.r#type) && default.is_string() && needs_quoting(&converted) {
349 format!("'{}'", converted)
350 } else {
351 converted
352 };
353
354 let final_default = if *backend == DatabaseBackend::Sqlite
357 && final_default.contains('(')
358 && !final_default.starts_with('(')
359 {
360 format!("({})", final_default)
361 } else {
362 final_default
363 };
364
365 col.default(Into::<SimpleExpr>::into(sea_query::Expr::cust(
366 final_default,
367 )));
368 }
369
370 col
371}
372
373pub fn build_create_enum_type_sql(
379 table: &str,
380 column_type: &ColumnType,
381) -> Option<super::types::RawSql> {
382 if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = column_type {
383 if values.is_integer() {
385 return None;
386 }
387
388 let values_sql = values.to_sql_values().join(", ");
389
390 let type_name = build_enum_type_name(table, name);
392
393 let pg_sql = format!("CREATE TYPE \"{}\" AS ENUM ({})", type_name, values_sql);
395
396 Some(super::types::RawSql::per_backend(
399 pg_sql,
400 String::new(),
401 String::new(),
402 ))
403 } else {
404 None
405 }
406}
407
408pub fn build_drop_enum_type_sql(
413 table: &str,
414 column_type: &ColumnType,
415) -> Option<super::types::RawSql> {
416 if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
417 let type_name = build_enum_type_name(table, name);
419
420 let pg_sql = format!("DROP TYPE \"{}\"", type_name);
422
423 Some(super::types::RawSql::per_backend(
425 pg_sql,
426 String::new(),
427 String::new(),
428 ))
429 } else {
430 None
431 }
432}
433
434pub use vespertide_naming::{
436 build_check_constraint_name, build_enum_type_name, build_foreign_key_name, build_index_name,
437 build_unique_constraint_name,
438};
439
440pub fn build_sqlite_enum_check_name(table: &str, column: &str) -> String {
442 build_check_constraint_name(table, column)
443}
444
445pub fn build_sqlite_enum_check_clause(
448 table: &str,
449 column: &str,
450 column_type: &ColumnType,
451) -> Option<String> {
452 if let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = column_type {
453 let name = build_sqlite_enum_check_name(table, column);
454 let values_sql = values.to_sql_values().join(", ");
455 Some(format!(
456 "CONSTRAINT \"{}\" CHECK (\"{}\" IN ({}))",
457 name, column, values_sql
458 ))
459 } else {
460 None
461 }
462}
463
464pub fn collect_sqlite_enum_check_clauses(table: &str, columns: &[ColumnDef]) -> Vec<String> {
466 columns
467 .iter()
468 .filter_map(|col| build_sqlite_enum_check_clause(table, &col.name, &col.r#type))
469 .collect()
470}
471
472pub fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec<String> {
475 constraints
476 .iter()
477 .filter_map(|c| {
478 if let TableConstraint::Check { name, expr } = c {
479 Some(format!("CONSTRAINT \"{}\" CHECK ({})", name, expr))
480 } else {
481 None
482 }
483 })
484 .collect()
485}
486
487pub fn collect_all_check_clauses(
494 table: &str,
495 columns: &[ColumnDef],
496 constraints: &[TableConstraint],
497) -> Vec<String> {
498 let mut clauses = collect_sqlite_enum_check_clauses(table, columns);
499 let explicit = extract_check_clauses(constraints);
500 for clause in explicit {
501 if !clauses.contains(&clause) {
502 clauses.push(clause);
503 }
504 }
505 clauses
506}
507
508pub fn build_create_with_checks(
512 backend: &DatabaseBackend,
513 create_stmt: &sea_query::TableCreateStatement,
514 check_clauses: &[String],
515) -> BuiltQuery {
516 if check_clauses.is_empty() {
517 BuiltQuery::CreateTable(Box::new(create_stmt.clone()))
518 } else {
519 let base_sql = build_schema_statement(create_stmt, *backend);
520 let mut modified_sql = base_sql;
521 if let Some(pos) = modified_sql.rfind(')') {
522 let check_sql = check_clauses.join(", ");
523 modified_sql.insert_str(pos, &format!(", {}", check_sql));
524 }
525 BuiltQuery::Raw(RawSql::per_backend(
526 modified_sql.clone(),
527 modified_sql.clone(),
528 modified_sql,
529 ))
530 }
531}
532
533pub fn build_sqlite_temp_table_create(
539 backend: &DatabaseBackend,
540 temp_table: &str,
541 table: &str,
542 columns: &[ColumnDef],
543 constraints: &[TableConstraint],
544) -> BuiltQuery {
545 let create_stmt = build_create_table_for_backend(backend, temp_table, columns, constraints);
546 let check_clauses = collect_all_check_clauses(table, columns, constraints);
547 build_create_with_checks(backend, &create_stmt, &check_clauses)
548}
549
550pub fn recreate_indexes_after_rebuild(
557 table: &str,
558 constraints: &[TableConstraint],
559 pending_constraints: &[TableConstraint],
560) -> Vec<BuiltQuery> {
561 let mut queries = Vec::new();
562 for constraint in constraints {
563 if pending_constraints.contains(constraint) {
565 continue;
566 }
567 match constraint {
568 TableConstraint::Index { name, columns } => {
569 let index_name = build_index_name(table, columns, name.as_deref());
570 let cols_sql = columns
571 .iter()
572 .map(|c| format!("\"{}\"", c))
573 .collect::<Vec<_>>()
574 .join(", ");
575 let sql = format!(
576 "CREATE INDEX \"{}\" ON \"{}\" ({})",
577 index_name, table, cols_sql
578 );
579 queries.push(BuiltQuery::Raw(RawSql::per_backend(
580 sql.clone(),
581 sql.clone(),
582 sql,
583 )));
584 }
585 TableConstraint::Unique { name, columns } => {
586 let index_name = build_unique_constraint_name(table, columns, name.as_deref());
587 let cols_sql = columns
588 .iter()
589 .map(|c| format!("\"{}\"", c))
590 .collect::<Vec<_>>()
591 .join(", ");
592 let sql = format!(
593 "CREATE UNIQUE INDEX \"{}\" ON \"{}\" ({})",
594 index_name, table, cols_sql
595 );
596 queries.push(BuiltQuery::Raw(RawSql::per_backend(
597 sql.clone(),
598 sql.clone(),
599 sql,
600 )));
601 }
602 _ => {}
603 }
604 }
605 queries
606}
607
608pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
610 if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {
611 Some(name.as_str())
612 } else {
613 None
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use rstest::rstest;
621 use sea_query::{Alias, ColumnDef as SeaColumnDef, ForeignKeyAction};
622 use vespertide_core::EnumValues;
623
624 #[rstest]
625 #[case(ColumnType::Simple(SimpleColumnType::Integer))]
626 #[case(ColumnType::Simple(SimpleColumnType::BigInt))]
627 #[case(ColumnType::Simple(SimpleColumnType::Text))]
628 #[case(ColumnType::Simple(SimpleColumnType::Boolean))]
629 #[case(ColumnType::Simple(SimpleColumnType::Timestamp))]
630 #[case(ColumnType::Simple(SimpleColumnType::Uuid))]
631 #[case(ColumnType::Complex(ComplexColumnType::Varchar { length: 255 }))]
632 #[case(ColumnType::Complex(ComplexColumnType::Numeric { precision: 10, scale: 2 }))]
633 fn test_column_type_conversion(#[case] ty: ColumnType) {
634 let mut col = SeaColumnDef::new(Alias::new("test"));
636 apply_column_type_with_table(&mut col, &ty, "test_table");
637 }
638
639 #[rstest]
640 #[case(SimpleColumnType::SmallInt)]
641 #[case(SimpleColumnType::Integer)]
642 #[case(SimpleColumnType::BigInt)]
643 #[case(SimpleColumnType::Real)]
644 #[case(SimpleColumnType::DoublePrecision)]
645 #[case(SimpleColumnType::Text)]
646 #[case(SimpleColumnType::Boolean)]
647 #[case(SimpleColumnType::Date)]
648 #[case(SimpleColumnType::Time)]
649 #[case(SimpleColumnType::Timestamp)]
650 #[case(SimpleColumnType::Timestamptz)]
651 #[case(SimpleColumnType::Interval)]
652 #[case(SimpleColumnType::Bytea)]
653 #[case(SimpleColumnType::Uuid)]
654 #[case(SimpleColumnType::Json)]
655 #[case(SimpleColumnType::Inet)]
656 #[case(SimpleColumnType::Cidr)]
657 #[case(SimpleColumnType::Macaddr)]
658 #[case(SimpleColumnType::Xml)]
659 fn test_all_simple_types_cover_branches(#[case] ty: SimpleColumnType) {
660 let mut col = SeaColumnDef::new(Alias::new("t"));
661 apply_column_type_with_table(&mut col, &ColumnType::Simple(ty), "test_table");
662 }
663
664 #[rstest]
665 #[case(ComplexColumnType::Varchar { length: 42 })]
666 #[case(ComplexColumnType::Numeric { precision: 8, scale: 3 })]
667 #[case(ComplexColumnType::Char { length: 3 })]
668 #[case(ComplexColumnType::Custom { custom_type: "GEOGRAPHY".into() })]
669 #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) })]
670 fn test_all_complex_types_cover_branches(#[case] ty: ComplexColumnType) {
671 let mut col = SeaColumnDef::new(Alias::new("t"));
672 apply_column_type_with_table(&mut col, &ColumnType::Complex(ty), "test_table");
673 }
674
675 #[rstest]
676 #[case::cascade(ReferenceAction::Cascade, ForeignKeyAction::Cascade)]
677 #[case::restrict(ReferenceAction::Restrict, ForeignKeyAction::Restrict)]
678 #[case::set_null(ReferenceAction::SetNull, ForeignKeyAction::SetNull)]
679 #[case::set_default(ReferenceAction::SetDefault, ForeignKeyAction::SetDefault)]
680 #[case::no_action(ReferenceAction::NoAction, ForeignKeyAction::NoAction)]
681 fn test_reference_action_conversion(
682 #[case] action: ReferenceAction,
683 #[case] expected: ForeignKeyAction,
684 ) {
685 let result = to_sea_fk_action(&action);
687 assert!(
688 matches!(result, _expected),
689 "Expected {:?}, got {:?}",
690 expected,
691 result
692 );
693 }
694
695 #[rstest]
696 #[case(ReferenceAction::Cascade, "CASCADE")]
697 #[case(ReferenceAction::Restrict, "RESTRICT")]
698 #[case(ReferenceAction::SetNull, "SET NULL")]
699 #[case(ReferenceAction::SetDefault, "SET DEFAULT")]
700 #[case(ReferenceAction::NoAction, "NO ACTION")]
701 fn test_reference_action_sql_all_variants(
702 #[case] action: ReferenceAction,
703 #[case] expected: &str,
704 ) {
705 assert_eq!(reference_action_sql(&action), expected);
706 }
707
708 #[rstest]
709 #[case::gen_random_uuid_postgres(
710 "gen_random_uuid()",
711 DatabaseBackend::Postgres,
712 "gen_random_uuid()"
713 )]
714 #[case::gen_random_uuid_mysql("gen_random_uuid()", DatabaseBackend::MySql, "(UUID())")]
715 #[case::gen_random_uuid_sqlite(
716 "gen_random_uuid()",
717 DatabaseBackend::Sqlite,
718 "lower(hex(randomblob(16)))"
719 )]
720 #[case::current_timestamp_postgres(
721 "current_timestamp()",
722 DatabaseBackend::Postgres,
723 "CURRENT_TIMESTAMP"
724 )]
725 #[case::current_timestamp_mysql(
726 "current_timestamp()",
727 DatabaseBackend::MySql,
728 "CURRENT_TIMESTAMP"
729 )]
730 #[case::current_timestamp_sqlite(
731 "current_timestamp()",
732 DatabaseBackend::Sqlite,
733 "CURRENT_TIMESTAMP"
734 )]
735 #[case::now_postgres("now()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")]
736 #[case::now_mysql("now()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")]
737 #[case::now_sqlite("now()", DatabaseBackend::Sqlite, "CURRENT_TIMESTAMP")]
738 #[case::now_upper_postgres("NOW()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")]
739 #[case::now_upper_mysql("NOW()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")]
740 #[case::now_upper_sqlite("NOW()", DatabaseBackend::Sqlite, "CURRENT_TIMESTAMP")]
741 #[case::current_timestamp_upper_postgres(
742 "CURRENT_TIMESTAMP",
743 DatabaseBackend::Postgres,
744 "CURRENT_TIMESTAMP"
745 )]
746 #[case::current_timestamp_upper_mysql(
747 "CURRENT_TIMESTAMP",
748 DatabaseBackend::MySql,
749 "CURRENT_TIMESTAMP"
750 )]
751 #[case::current_timestamp_upper_sqlite(
752 "CURRENT_TIMESTAMP",
753 DatabaseBackend::Sqlite,
754 "CURRENT_TIMESTAMP"
755 )]
756 fn test_convert_default_for_backend(
757 #[case] default: &str,
758 #[case] backend: DatabaseBackend,
759 #[case] expected: &str,
760 ) {
761 let result = convert_default_for_backend(default, &backend);
762 assert_eq!(result, expected);
763 }
764
765 #[rstest]
768 #[case::json_cast_postgres("'[]'::json", DatabaseBackend::Postgres, "'[]'::json")]
770 #[case::json_cast_mysql("'[]'::json", DatabaseBackend::MySql, "CAST('[]' AS JSON)")]
771 #[case::json_cast_sqlite("'[]'::json", DatabaseBackend::Sqlite, "'[]'")]
772 #[case::jsonb_cast_postgres("'{}'::jsonb", DatabaseBackend::Postgres, "'{}'::jsonb")]
774 #[case::jsonb_cast_mysql("'{}'::jsonb", DatabaseBackend::MySql, "CAST('{}' AS JSON)")]
775 #[case::jsonb_cast_sqlite("'{}'::jsonb", DatabaseBackend::Sqlite, "'{}'")]
776 #[case::text_cast_postgres("'hello'::text", DatabaseBackend::Postgres, "'hello'::text")]
778 #[case::text_cast_mysql("'hello'::text", DatabaseBackend::MySql, "CAST('hello' AS CHAR)")]
779 #[case::text_cast_sqlite("'hello'::text", DatabaseBackend::Sqlite, "'hello'")]
780 #[case::int_cast_postgres("0::integer", DatabaseBackend::Postgres, "0::integer")]
782 #[case::int_cast_mysql("0::integer", DatabaseBackend::MySql, "CAST(0 AS SIGNED)")]
783 #[case::int_cast_sqlite("0::integer", DatabaseBackend::Sqlite, "0")]
784 #[case::bool_cast_postgres("0::boolean", DatabaseBackend::Postgres, "0::boolean")]
786 #[case::bool_cast_mysql("0::boolean", DatabaseBackend::MySql, "CAST(0 AS UNSIGNED)")]
787 #[case::bool_cast_sqlite("0::boolean", DatabaseBackend::Sqlite, "0")]
788 #[case::json_obj_cast_postgres(
790 "'{\"key\":\"value\"}'::json",
791 DatabaseBackend::Postgres,
792 "'{\"key\":\"value\"}'::json"
793 )]
794 #[case::json_obj_cast_mysql(
795 "'{\"key\":\"value\"}'::json",
796 DatabaseBackend::MySql,
797 "CAST('{\"key\":\"value\"}' AS JSON)"
798 )]
799 #[case::json_obj_cast_sqlite(
800 "'{\"key\":\"value\"}'::json",
801 DatabaseBackend::Sqlite,
802 "'{\"key\":\"value\"}'"
803 )]
804 #[case::timestamp_cast_postgres(
806 "'2024-01-01'::timestamp",
807 DatabaseBackend::Postgres,
808 "'2024-01-01'::timestamp"
809 )]
810 #[case::timestamp_cast_mysql(
811 "'2024-01-01'::timestamp",
812 DatabaseBackend::MySql,
813 "CAST('2024-01-01' AS DATETIME)"
814 )]
815 #[case::timestamp_cast_sqlite(
816 "'2024-01-01'::timestamp",
817 DatabaseBackend::Sqlite,
818 "'2024-01-01'"
819 )]
820 fn test_convert_default_for_backend_type_cast(
821 #[case] default: &str,
822 #[case] backend: DatabaseBackend,
823 #[case] expected: &str,
824 ) {
825 let result = convert_default_for_backend(default, &backend);
826 assert_eq!(result, expected);
827 }
828
829 #[test]
830 fn test_parse_pg_type_cast_no_cast() {
831 assert!(parse_pg_type_cast("'hello'").is_none());
833 assert!(parse_pg_type_cast("42").is_none());
834 assert!(parse_pg_type_cast("NOW()").is_none());
835 assert!(parse_pg_type_cast("CURRENT_TIMESTAMP").is_none());
836 }
837
838 #[test]
839 fn test_parse_pg_type_cast_valid() {
840 let (value, cast_type) = parse_pg_type_cast("'[]'::json").unwrap();
841 assert_eq!(value, "'[]'");
842 assert_eq!(cast_type, "json");
843
844 let (value, cast_type) = parse_pg_type_cast("0::boolean").unwrap();
845 assert_eq!(value, "0");
846 assert_eq!(cast_type, "boolean");
847 }
848
849 #[test]
850 fn test_parse_pg_type_cast_escaped_quotes() {
851 let (value, cast_type) = parse_pg_type_cast("'it''s'::text").unwrap();
853 assert_eq!(value, "'it''s'");
854 assert_eq!(cast_type, "text");
855 }
856
857 #[test]
858 fn test_parse_pg_type_cast_unterminated_quote() {
859 assert!(parse_pg_type_cast("'unclosed").is_none());
861 assert!(parse_pg_type_cast("'no close quote::json").is_none());
862 }
863
864 #[rstest]
865 #[case::numeric("'0.5'::numeric", DatabaseBackend::MySql, "CAST('0.5' AS DECIMAL)")]
866 #[case::decimal("'1.23'::decimal", DatabaseBackend::MySql, "CAST('1.23' AS DECIMAL)")]
867 #[case::bytea("'\\xDE'::bytea", DatabaseBackend::MySql, "CAST('\\xDE' AS BINARY)")]
868 #[case::unknown("'x'::citext", DatabaseBackend::MySql, "CAST('x' AS CHAR)")]
869 fn test_convert_default_for_backend_type_cast_extra(
870 #[case] default: &str,
871 #[case] backend: DatabaseBackend,
872 #[case] expected: &str,
873 ) {
874 let result = convert_default_for_backend(default, &backend);
875 assert_eq!(result, expected);
876 }
877
878 #[test]
879 fn test_is_enum_type_true() {
880 use vespertide_core::EnumValues;
881
882 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
883 name: "status".into(),
884 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
885 });
886 assert!(is_enum_type(&enum_type));
887 }
888
889 #[test]
890 fn test_is_enum_type_false() {
891 let text_type = ColumnType::Simple(SimpleColumnType::Text);
892 assert!(!is_enum_type(&text_type));
893 }
894
895 #[test]
896 fn test_get_enum_name_some() {
897 use vespertide_core::EnumValues;
898
899 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
900 name: "user_status".into(),
901 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
902 });
903 assert_eq!(get_enum_name(&enum_type), Some("user_status"));
904 }
905
906 #[test]
907 fn test_get_enum_name_none() {
908 let text_type = ColumnType::Simple(SimpleColumnType::Text);
909 assert_eq!(get_enum_name(&text_type), None);
910 }
911
912 #[test]
913 fn test_apply_column_type_integer_enum() {
914 use vespertide_core::{EnumValues, NumValue};
915 let integer_enum = ColumnType::Complex(ComplexColumnType::Enum {
916 name: "color".into(),
917 values: EnumValues::Integer(vec![
918 NumValue {
919 name: "Black".into(),
920 value: 0,
921 },
922 NumValue {
923 name: "White".into(),
924 value: 1,
925 },
926 ]),
927 });
928 let mut col = SeaColumnDef::new(Alias::new("color"));
929 apply_column_type_with_table(&mut col, &integer_enum, "test_table");
930 }
932
933 #[test]
934 fn test_build_create_enum_type_sql_integer_enum_returns_none() {
935 use vespertide_core::{EnumValues, NumValue};
936 let integer_enum = ColumnType::Complex(ComplexColumnType::Enum {
937 name: "priority".into(),
938 values: EnumValues::Integer(vec![
939 NumValue {
940 name: "Low".into(),
941 value: 0,
942 },
943 NumValue {
944 name: "High".into(),
945 value: 10,
946 },
947 ]),
948 });
949 assert!(build_create_enum_type_sql("test_table", &integer_enum).is_none());
951 }
952
953 #[rstest]
954 #[case::empty("", true)]
956 #[case::whitespace_only(" ", true)]
957 #[case::now_func("now()", false)]
959 #[case::coalesce_func("COALESCE(old_value, 'default')", false)]
960 #[case::uuid_func("gen_random_uuid()", false)]
961 #[case::null_upper("NULL", false)]
963 #[case::null_lower("null", false)]
964 #[case::null_mixed("Null", false)]
965 #[case::current_timestamp_upper("CURRENT_TIMESTAMP", false)]
967 #[case::current_timestamp_lower("current_timestamp", false)]
968 #[case::current_date_upper("CURRENT_DATE", false)]
969 #[case::current_date_lower("current_date", false)]
970 #[case::current_time_upper("CURRENT_TIME", false)]
971 #[case::current_time_lower("current_time", false)]
972 #[case::single_quoted("'active'", false)]
974 #[case::double_quoted("\"active\"", false)]
975 #[case::plain_active("active", true)]
977 #[case::plain_pending("pending", true)]
978 #[case::plain_underscore("some_value", true)]
979 fn test_needs_quoting(#[case] input: &str, #[case] expected: bool) {
980 assert_eq!(needs_quoting(input), expected);
981 }
982
983 #[test]
984 fn test_recreate_indexes_after_rebuild_skips_pending() {
985 use vespertide_core::TableConstraint;
986 let idx1 = TableConstraint::Index {
987 name: Some("idx_a".into()),
988 columns: vec!["a".into()],
989 };
990 let idx2 = TableConstraint::Index {
991 name: Some("idx_b".into()),
992 columns: vec!["b".into()],
993 };
994 let uq1 = TableConstraint::Unique {
995 name: Some("uq_c".into()),
996 columns: vec!["c".into()],
997 };
998
999 let constraints = vec![idx1.clone(), idx2.clone(), uq1.clone()];
1001 let pending = vec![idx1.clone(), uq1.clone()];
1002
1003 let queries = recreate_indexes_after_rebuild("t", &constraints, &pending);
1004 assert_eq!(queries.len(), 1);
1006 let sql = queries[0].build(DatabaseBackend::Sqlite);
1007 assert!(sql.contains("idx_b"));
1008 }
1009
1010 #[test]
1011 fn test_recreate_indexes_after_rebuild_no_pending() {
1012 use vespertide_core::TableConstraint;
1013 let idx = TableConstraint::Index {
1014 name: Some("idx_a".into()),
1015 columns: vec!["a".into()],
1016 };
1017 let uq = TableConstraint::Unique {
1018 name: Some("uq_b".into()),
1019 columns: vec!["b".into()],
1020 };
1021
1022 let queries = recreate_indexes_after_rebuild("t", &[idx, uq], &[]);
1023 assert_eq!(queries.len(), 2);
1024 }
1025
1026 #[test]
1027 fn test_recreate_indexes_after_rebuild_skips_non_index_constraints() {
1028 use vespertide_core::TableConstraint;
1029 let pk = TableConstraint::PrimaryKey {
1030 columns: vec!["id".into()],
1031 auto_increment: false,
1032 };
1033 let fk = TableConstraint::ForeignKey {
1034 name: None,
1035 columns: vec!["uid".into()],
1036 ref_table: "u".into(),
1037 ref_columns: vec!["id".into()],
1038 on_delete: None,
1039 on_update: None,
1040 };
1041 let chk = TableConstraint::Check {
1042 name: "chk".into(),
1043 expr: "id > 0".into(),
1044 };
1045
1046 let queries = recreate_indexes_after_rebuild("t", &[pk, fk, chk], &[]);
1047 assert_eq!(queries.len(), 0);
1048 }
1049}