Skip to main content

vespertide_core/schema/
mod.rs

1pub mod check_violation_strategy;
2pub mod column;
3pub mod constraint;
4pub mod fk_orphan_strategy;
5pub mod foreign_key;
6pub mod index;
7pub mod names;
8pub mod pk_addition_strategy;
9pub mod primary_key;
10pub mod reference;
11pub mod str_or_bool;
12pub mod table;
13pub mod unique_strategy;
14
15pub use check_violation_strategy::CheckViolationStrategy;
16pub use column::{
17    ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, SimpleColumnType,
18};
19pub use constraint::{ConstraintKind, TableConstraint};
20pub use fk_orphan_strategy::ForeignKeyOrphanStrategy;
21pub use index::IndexDef;
22pub use names::{ColumnName, IndexName, TableName};
23pub use pk_addition_strategy::PrimaryKeyAdditionStrategy;
24pub use primary_key::PrimaryKeyDef;
25pub use reference::ReferenceAction;
26pub use str_or_bool::{DefaultValue, StrOrBoolOrArray, StringOrBool};
27pub use table::{TableDef, TableValidationError};
28pub use unique_strategy::{KeepPolicy, UniqueConstraintStrategy};
29
30#[cfg(test)]
31mod tests {
32    mod column {
33        use crate::schema::column::*;
34        use crate::schema::primary_key::PrimaryKeySyntax;
35        use crate::schema::str_or_bool::StrOrBoolOrArray;
36        use rstest::rstest;
37
38        #[test]
39        fn new_creates_minimal_column() {
40            let c = ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false);
41            assert_eq!(c.name.as_str(), "id");
42            assert!(!c.nullable);
43            assert!(c.primary_key.is_none());
44            assert!(c.unique.is_none());
45            assert!(c.index.is_none());
46            assert!(c.foreign_key.is_none());
47            assert!(c.default.is_none());
48            assert!(c.comment.is_none());
49        }
50
51        #[test]
52        fn builder_chain_sets_fields() {
53            let c = ColumnDef::new("email", ColumnType::Simple(SimpleColumnType::Text), false)
54                .unique(StrOrBoolOrArray::Bool(true))
55                .index(StrOrBoolOrArray::Bool(true))
56                .comment("user email");
57            assert!(c.unique.is_some());
58            assert!(c.index.is_some());
59            assert_eq!(c.comment.as_deref(), Some("user email"));
60        }
61
62        #[test]
63        fn builder_preserves_round_trip_with_existing_literal() {
64            // Equivalence: new(...) + setters == literal { ... } construction
65            let from_builder =
66                ColumnDef::new("name", ColumnType::Simple(SimpleColumnType::Text), false);
67            let from_literal = ColumnDef {
68                name: "name".into(),
69                r#type: ColumnType::Simple(SimpleColumnType::Text),
70                nullable: false,
71                default: None,
72                comment: None,
73                primary_key: None,
74                unique: None,
75                index: None,
76                foreign_key: None,
77            };
78            assert_eq!(from_builder, from_literal);
79        }
80
81        #[test]
82        fn primary_key_builder_accepts_existing_syntax_type() {
83            let c = ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false)
84                .primary_key(PrimaryKeySyntax::Bool(true));
85            assert_eq!(c.primary_key, Some(PrimaryKeySyntax::Bool(true)));
86        }
87
88        #[rstest]
89        #[case(SimpleColumnType::SmallInt, "i16")]
90        #[case(SimpleColumnType::Integer, "i32")]
91        #[case(SimpleColumnType::BigInt, "i64")]
92        #[case(SimpleColumnType::Real, "f32")]
93        #[case(SimpleColumnType::DoublePrecision, "f64")]
94        #[case(SimpleColumnType::Text, "String")]
95        #[case(SimpleColumnType::Boolean, "bool")]
96        #[case(SimpleColumnType::Date, "Date")]
97        #[case(SimpleColumnType::Time, "Time")]
98        #[case(SimpleColumnType::Timestamp, "DateTime")]
99        #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")]
100        #[case(SimpleColumnType::Interval, "String")]
101        #[case(SimpleColumnType::Bytea, "Vec<u8>")]
102        #[case(SimpleColumnType::Uuid, "Uuid")]
103        #[case(SimpleColumnType::Json, "Json")]
104        // #[case(SimpleColumnType::Jsonb, "Json")]
105        #[case(SimpleColumnType::Inet, "String")]
106        #[case(SimpleColumnType::Cidr, "String")]
107        #[case(SimpleColumnType::Macaddr, "String")]
108        #[case(SimpleColumnType::Xml, "String")]
109        fn test_simple_column_type_to_rust_type_not_nullable(
110            #[case] column_type: SimpleColumnType,
111            #[case] expected: &str,
112        ) {
113            assert_eq!(
114                ColumnType::Simple(column_type).to_rust_type(false),
115                expected
116            );
117        }
118
119        #[rstest]
120        #[case(SimpleColumnType::SmallInt, "Option<i16>")]
121        #[case(SimpleColumnType::Integer, "Option<i32>")]
122        #[case(SimpleColumnType::BigInt, "Option<i64>")]
123        #[case(SimpleColumnType::Real, "Option<f32>")]
124        #[case(SimpleColumnType::DoublePrecision, "Option<f64>")]
125        #[case(SimpleColumnType::Text, "Option<String>")]
126        #[case(SimpleColumnType::Boolean, "Option<bool>")]
127        #[case(SimpleColumnType::Date, "Option<Date>")]
128        #[case(SimpleColumnType::Time, "Option<Time>")]
129        #[case(SimpleColumnType::Timestamp, "Option<DateTime>")]
130        #[case(SimpleColumnType::Timestamptz, "Option<DateTimeWithTimeZone>")]
131        #[case(SimpleColumnType::Interval, "Option<String>")]
132        #[case(SimpleColumnType::Bytea, "Option<Vec<u8>>")]
133        #[case(SimpleColumnType::Uuid, "Option<Uuid>")]
134        #[case(SimpleColumnType::Json, "Option<Json>")]
135        // #[case(SimpleColumnType::Jsonb, "Option<Json>")]
136        #[case(SimpleColumnType::Inet, "Option<String>")]
137        #[case(SimpleColumnType::Cidr, "Option<String>")]
138        #[case(SimpleColumnType::Macaddr, "Option<String>")]
139        #[case(SimpleColumnType::Xml, "Option<String>")]
140        fn test_simple_column_type_to_rust_type_nullable(
141            #[case] column_type: SimpleColumnType,
142            #[case] expected: &str,
143        ) {
144            assert_eq!(ColumnType::Simple(column_type).to_rust_type(true), expected);
145        }
146
147        #[rstest]
148        #[case(ComplexColumnType::Varchar { length: 255 }, false, "String")]
149        #[case(ComplexColumnType::Varchar { length: 50 }, false, "String")]
150        #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, false, "Decimal")]
151        #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, false, "Decimal")]
152        #[case(ComplexColumnType::Char { length: 10 }, false, "String")]
153        #[case(ComplexColumnType::Char { length: 1 }, false, "String")]
154        #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, false, "String")]
155        #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, false, "String")]
156        #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, false, "String")]
157        fn test_complex_column_type_to_rust_type_not_nullable(
158            #[case] column_type: ComplexColumnType,
159            #[case] nullable: bool,
160            #[case] expected: &str,
161        ) {
162            assert_eq!(
163                ColumnType::Complex(column_type).to_rust_type(nullable),
164                expected
165            );
166        }
167
168        #[rstest]
169        #[case(ComplexColumnType::Varchar { length: 255 }, "Option<String>")]
170        #[case(ComplexColumnType::Varchar { length: 50 }, "Option<String>")]
171        #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "Option<Decimal>")]
172        #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "Option<Decimal>")]
173        #[case(ComplexColumnType::Char { length: 10 }, "Option<String>")]
174        #[case(ComplexColumnType::Char { length: 1 }, "Option<String>")]
175        #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "Option<String>")]
176        #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "Option<String>")]
177        #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, "Option<String>")]
178        fn test_complex_column_type_to_rust_type_nullable(
179            #[case] column_type: ComplexColumnType,
180            #[case] expected: &str,
181        ) {
182            assert_eq!(
183                ColumnType::Complex(column_type).to_rust_type(true),
184                expected
185            );
186        }
187
188        #[rstest]
189        #[case(ComplexColumnType::Varchar { length: 255 })]
190        #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 })]
191        #[case(ComplexColumnType::Char { length: 1 })]
192        #[case(ComplexColumnType::Custom { custom_type: "SERIAL".into() })]
193        #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec![]) })]
194        fn test_complex_column_type_does_not_support_auto_increment(
195            #[case] column_type: ComplexColumnType,
196        ) {
197            // Complex types never support auto_increment
198            assert!(!ColumnType::Complex(column_type).supports_auto_increment());
199        }
200
201        #[test]
202        fn test_enum_values_is_string() {
203            let string_vals = EnumValues::String(vec!["active".into()]);
204            let int_vals = EnumValues::Integer(vec![NumValue {
205                name: "Active".into(),
206                value: 1,
207            }]);
208            assert!(string_vals.is_string());
209            assert!(!int_vals.is_string());
210        }
211
212        #[test]
213        fn test_enum_values_is_integer() {
214            let string_vals = EnumValues::String(vec!["active".into()]);
215            let int_vals = EnumValues::Integer(vec![NumValue {
216                name: "Active".into(),
217                value: 1,
218            }]);
219            assert!(!string_vals.is_integer());
220            assert!(int_vals.is_integer());
221        }
222
223        #[rstest]
224        // String match: exact-string check against each variant.
225        #[case::string_match(
226            EnumValues::String(vec!["active".into(), "pending".into()]),
227            "active",
228            true,
229        )]
230        #[case::string_miss(
231            EnumValues::String(vec!["active".into(), "pending".into()]),
232            "banned",
233            false,
234        )]
235        // Integer enum, numeric input: parses as i64 then matches `NumValue::value`.
236        #[case::integer_numeric_match(
237            EnumValues::Integer(vec![
238                NumValue { name: "Low".into(),  value: 0  },
239                NumValue { name: "High".into(), value: 10 },
240            ]),
241            "10",
242            true,
243        )]
244        #[case::integer_numeric_miss(
245            EnumValues::Integer(vec![
246                NumValue { name: "Low".into(),  value: 0  },
247                NumValue { name: "High".into(), value: 10 },
248            ]),
249            "99",
250            false,
251        )]
252        // Integer enum, value beyond i32::MAX: must still parse (i64) and match.
253        #[case::integer_numeric_beyond_i32(
254            EnumValues::Integer(vec![
255                NumValue { name: "small".into(), value: 1             },
256                NumValue { name: "big".into(),   value: 3_000_000_000 },
257            ]),
258            "3000000000",
259            true,
260        )]
261        // Integer enum, non-numeric input: falls back to matching `NumValue::name`.
262        #[case::integer_name_match(
263            EnumValues::Integer(vec![
264                NumValue { name: "Low".into(),  value: 0  },
265                NumValue { name: "High".into(), value: 10 },
266            ]),
267            "High",
268            true,
269        )]
270        #[case::integer_name_miss(
271            EnumValues::Integer(vec![
272                NumValue { name: "Low".into(),  value: 0  },
273                NumValue { name: "High".into(), value: 10 },
274            ]),
275            "Unknown",
276            false,
277        )]
278        fn contains_value_matches_variants_per_enum_kind(
279            #[case] values: EnumValues,
280            #[case] candidate: &str,
281            #[case] expected: bool,
282        ) {
283            assert_eq!(values.contains_value(candidate), expected);
284        }
285
286        #[test]
287        fn test_enum_values_len_and_is_empty() {
288            // String variant
289            let empty = EnumValues::String(vec![]);
290            let non_empty = EnumValues::String(vec!["a".into()]);
291            assert!(empty.is_empty());
292            assert_eq!(empty.len(), 0);
293            assert!(!non_empty.is_empty());
294            assert_eq!(non_empty.len(), 1);
295
296            // Integer variant
297            let empty_int = EnumValues::Integer(vec![]);
298            let non_empty_int = EnumValues::Integer(vec![
299                NumValue {
300                    name: "A".into(),
301                    value: 0,
302                },
303                NumValue {
304                    name: "B".into(),
305                    value: 1,
306                },
307            ]);
308            assert!(empty_int.is_empty());
309            assert_eq!(empty_int.len(), 0);
310            assert!(!non_empty_int.is_empty());
311            assert_eq!(non_empty_int.len(), 2);
312        }
313
314        #[rstest]
315        // Locks the buffer-push helper's expected output across every arm.
316        // Prior versions also cross-checked against a now-deleted
317        // `to_sql_values() -> Vec<String>` pipeline; the case data below is
318        // the actual contract.
319        #[case::string_two_variants(
320            EnumValues::String(vec!["active".into(), "pending".into()]),
321            ", ",
322            "'active', 'pending'",
323        )]
324        #[case::string_single_quote_escape(
325            EnumValues::String(vec!["O'Brien".into(), "'leading".into()]),
326            ", ",
327            "'O''Brien', '''leading'",
328        )]
329        #[case::integer_two_variants(
330            EnumValues::Integer(vec![
331                NumValue { name: "low".into(),  value: 0  },
332                NumValue { name: "high".into(), value: 10 },
333            ]),
334            ", ",
335            "0, 10",
336        )]
337        #[case::string_empty(EnumValues::String(vec![]), ", ", "")]
338        #[case::integer_empty(EnumValues::Integer(vec![]), ", ", "")]
339        #[case::string_single_variant(
340            EnumValues::String(vec!["only".into()]),
341            ", ",
342            "'only'",
343        )]
344        #[case::alt_separator(
345            EnumValues::String(vec!["a".into(), "b".into(), "c".into()]),
346            "_",
347            "'a'_'b'_'c'",
348        )]
349        fn sql_values_joined_locks_expected_output(
350            #[case] vals: EnumValues,
351            #[case] separator: &str,
352            #[case] expected: &str,
353        ) {
354            assert_eq!(vals.sql_values_joined(separator), expected);
355        }
356
357        #[test]
358        fn test_enum_values_from_vec_string() {
359            let vals: EnumValues = vec!["a".to_string(), "b".to_string()].into();
360            assert!(matches!(vals, EnumValues::String(_)));
361        }
362
363        #[test]
364        fn test_enum_values_from_vec_str() {
365            let vals: EnumValues = vec!["a", "b"].into();
366            assert!(matches!(vals, EnumValues::String(_)));
367        }
368
369        #[rstest]
370        #[case(SimpleColumnType::SmallInt, true)]
371        #[case(SimpleColumnType::Integer, true)]
372        #[case(SimpleColumnType::BigInt, true)]
373        #[case(SimpleColumnType::Text, false)]
374        #[case(SimpleColumnType::Boolean, false)]
375        fn test_simple_column_type_supports_auto_increment(
376            #[case] ty: SimpleColumnType,
377            #[case] expected: bool,
378        ) {
379            assert_eq!(ty.supports_auto_increment(), expected);
380        }
381
382        #[rstest]
383        #[case(SimpleColumnType::Integer, true)]
384        #[case(SimpleColumnType::Text, false)]
385        fn test_column_type_simple_supports_auto_increment(
386            #[case] ty: SimpleColumnType,
387            #[case] expected: bool,
388        ) {
389            assert_eq!(ColumnType::Simple(ty).supports_auto_increment(), expected);
390        }
391
392        #[test]
393        fn test_requires_migration_integer_enum_values_changed() {
394            // Integer enum values changed - should NOT require migration
395            let from = ColumnType::Complex(ComplexColumnType::Enum {
396                name: "status".into(),
397                values: EnumValues::Integer(vec![
398                    NumValue {
399                        name: "Pending".into(),
400                        value: 0,
401                    },
402                    NumValue {
403                        name: "Active".into(),
404                        value: 1,
405                    },
406                ]),
407            });
408            let to = ColumnType::Complex(ComplexColumnType::Enum {
409                name: "status".into(),
410                values: EnumValues::Integer(vec![
411                    NumValue {
412                        name: "Pending".into(),
413                        value: 0,
414                    },
415                    NumValue {
416                        name: "Active".into(),
417                        value: 1,
418                    },
419                    NumValue {
420                        name: "Completed".into(),
421                        value: 100,
422                    },
423                ]),
424            });
425            assert!(!from.requires_migration(&to));
426        }
427
428        #[test]
429        fn requires_migration_integer_enum_name_change_is_false() {
430            let e1 = ColumnType::Complex(ComplexColumnType::Enum {
431                name: "old_status".into(),
432                values: EnumValues::Integer(vec![
433                    NumValue {
434                        name: "active".into(),
435                        value: 0,
436                    },
437                    NumValue {
438                        name: "inactive".into(),
439                        value: 1,
440                    },
441                ]),
442            });
443            let e2 = ColumnType::Complex(ComplexColumnType::Enum {
444                name: "new_status".into(),
445                values: EnumValues::Integer(vec![
446                    NumValue {
447                        name: "active".into(),
448                        value: 0,
449                    },
450                    NumValue {
451                        name: "inactive".into(),
452                        value: 1,
453                    },
454                ]),
455            });
456
457            assert!(
458                !e1.requires_migration(&e2),
459                "integer enum name change should NOT require migration (stored as INTEGER, no DB schema change)"
460            );
461        }
462
463        #[test]
464        fn test_requires_migration_integer_enum_name_changed() {
465            // Integer enum name changed - should NOT require migration (DB type is always INTEGER)
466            let from = ColumnType::Complex(ComplexColumnType::Enum {
467                name: "old_status".into(),
468                values: EnumValues::Integer(vec![NumValue {
469                    name: "Pending".into(),
470                    value: 0,
471                }]),
472            });
473            let to = ColumnType::Complex(ComplexColumnType::Enum {
474                name: "new_status".into(),
475                values: EnumValues::Integer(vec![NumValue {
476                    name: "Pending".into(),
477                    value: 0,
478                }]),
479            });
480            assert!(!from.requires_migration(&to));
481        }
482
483        #[test]
484        fn test_requires_migration_string_enum_values_changed() {
485            // String enum values changed - SHOULD require migration
486            let from = ColumnType::Complex(ComplexColumnType::Enum {
487                name: "status".into(),
488                values: EnumValues::String(vec!["pending".into(), "active".into()]),
489            });
490            let to = ColumnType::Complex(ComplexColumnType::Enum {
491                name: "status".into(),
492                values: EnumValues::String(vec![
493                    "pending".into(),
494                    "active".into(),
495                    "completed".into(),
496                ]),
497            });
498            assert!(from.requires_migration(&to));
499        }
500
501        #[test]
502        fn test_requires_migration_simple_types() {
503            let int = ColumnType::Simple(SimpleColumnType::Integer);
504            let text = ColumnType::Simple(SimpleColumnType::Text);
505            assert!(int.requires_migration(&text));
506            assert!(!int.requires_migration(&int));
507        }
508
509        #[test]
510        fn test_requires_migration_mixed_enum_types() {
511            // String enum to integer enum - SHOULD require migration
512            let string_enum = ColumnType::Complex(ComplexColumnType::Enum {
513                name: "status".into(),
514                values: EnumValues::String(vec!["pending".into()]),
515            });
516            let int_enum = ColumnType::Complex(ComplexColumnType::Enum {
517                name: "status".into(),
518                values: EnumValues::Integer(vec![NumValue {
519                    name: "Pending".into(),
520                    value: 0,
521                }]),
522            });
523            assert!(string_enum.requires_migration(&int_enum));
524        }
525
526        // Tests for to_display_string
527        #[rstest]
528        #[case(SimpleColumnType::SmallInt, "smallint")]
529        #[case(SimpleColumnType::Integer, "integer")]
530        #[case(SimpleColumnType::BigInt, "bigint")]
531        #[case(SimpleColumnType::Real, "real")]
532        #[case(SimpleColumnType::DoublePrecision, "double precision")]
533        #[case(SimpleColumnType::Text, "text")]
534        #[case(SimpleColumnType::Boolean, "boolean")]
535        #[case(SimpleColumnType::Date, "date")]
536        #[case(SimpleColumnType::Time, "time")]
537        #[case(SimpleColumnType::Timestamp, "timestamp")]
538        #[case(SimpleColumnType::Timestamptz, "timestamptz")]
539        #[case(SimpleColumnType::Interval, "interval")]
540        #[case(SimpleColumnType::Bytea, "bytea")]
541        #[case(SimpleColumnType::Uuid, "uuid")]
542        #[case(SimpleColumnType::Json, "json")]
543        #[case(SimpleColumnType::Inet, "inet")]
544        #[case(SimpleColumnType::Cidr, "cidr")]
545        #[case(SimpleColumnType::Macaddr, "macaddr")]
546        #[case(SimpleColumnType::Xml, "xml")]
547        fn test_simple_column_type_to_display_string(
548            #[case] column_type: SimpleColumnType,
549            #[case] expected: &str,
550        ) {
551            assert_eq!(column_type.to_display_string(), expected);
552        }
553
554        #[rstest]
555        #[case(SimpleColumnType::SmallInt, "SMALLINT")]
556        #[case(SimpleColumnType::Integer, "INTEGER")]
557        #[case(SimpleColumnType::BigInt, "BIGINT")]
558        #[case(SimpleColumnType::Real, "REAL")]
559        #[case(SimpleColumnType::DoublePrecision, "DOUBLE PRECISION")]
560        #[case(SimpleColumnType::Text, "TEXT")]
561        #[case(SimpleColumnType::Boolean, "BOOLEAN")]
562        #[case(SimpleColumnType::Date, "DATE")]
563        #[case(SimpleColumnType::Time, "TIME")]
564        #[case(SimpleColumnType::Timestamp, "TIMESTAMP")]
565        #[case(SimpleColumnType::Timestamptz, "TIMESTAMPTZ")]
566        #[case(SimpleColumnType::Interval, "INTERVAL")]
567        #[case(SimpleColumnType::Bytea, "BYTEA")]
568        #[case(SimpleColumnType::Uuid, "UUID")]
569        #[case(SimpleColumnType::Json, "JSON")]
570        #[case(SimpleColumnType::Inet, "INET")]
571        #[case(SimpleColumnType::Cidr, "CIDR")]
572        #[case(SimpleColumnType::Macaddr, "MACADDR")]
573        #[case(SimpleColumnType::Xml, "XML")]
574        fn test_simple_column_type_sql_type(
575            #[case] column_type: SimpleColumnType,
576            #[case] expected: &str,
577        ) {
578            assert_eq!(column_type.sql_type(), expected);
579        }
580
581        #[rstest]
582        #[case(SimpleColumnType::SmallInt, "small_int")]
583        #[case(SimpleColumnType::Integer, "integer")]
584        #[case(SimpleColumnType::BigInt, "big_int")]
585        #[case(SimpleColumnType::Real, "real")]
586        #[case(SimpleColumnType::DoublePrecision, "double_precision")]
587        #[case(SimpleColumnType::Text, "text")]
588        #[case(SimpleColumnType::Boolean, "boolean")]
589        #[case(SimpleColumnType::Date, "date")]
590        #[case(SimpleColumnType::Time, "time")]
591        #[case(SimpleColumnType::Timestamp, "timestamp")]
592        #[case(SimpleColumnType::Timestamptz, "timestamptz")]
593        #[case(SimpleColumnType::Interval, "interval")]
594        #[case(SimpleColumnType::Bytea, "bytea")]
595        #[case(SimpleColumnType::Uuid, "uuid")]
596        #[case(SimpleColumnType::Json, "json")]
597        #[case(SimpleColumnType::Inet, "inet")]
598        #[case(SimpleColumnType::Cidr, "cidr")]
599        #[case(SimpleColumnType::Macaddr, "macaddr")]
600        #[case(SimpleColumnType::Xml, "xml")]
601        fn test_simple_column_type_model_name_matches_serde_wire_format(
602            #[case] column_type: SimpleColumnType,
603            #[case] expected: &str,
604        ) {
605            assert_eq!(column_type.model_name(), expected);
606            // model_name() must agree with the serde wire format byte-for-byte.
607            assert_eq!(
608                serde_json::to_value(column_type).unwrap(),
609                serde_json::Value::String(expected.to_string())
610            );
611        }
612
613        #[rstest]
614        #[case(ComplexColumnType::Varchar { length: 255 }, "VARCHAR")]
615        #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "NUMERIC")]
616        #[case(ComplexColumnType::Char { length: 5 }, "CHAR")]
617        #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "CUSTOM")]
618        #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into()]) }, "ENUM")]
619        fn test_complex_column_type_sql_type(
620            #[case] column_type: ComplexColumnType,
621            #[case] expected: &str,
622        ) {
623            assert_eq!(column_type.sql_type(), expected);
624        }
625
626        #[test]
627        fn test_complex_column_type_to_display_string_varchar() {
628            let ty = ComplexColumnType::Varchar { length: 255 };
629            assert_eq!(ty.to_display_string(), "varchar(255)");
630        }
631
632        #[test]
633        fn test_complex_column_type_to_display_string_numeric() {
634            let ty = ComplexColumnType::Numeric {
635                precision: 10,
636                scale: 2,
637            };
638            assert_eq!(ty.to_display_string(), "numeric(10,2)");
639        }
640
641        #[test]
642        fn test_complex_column_type_to_display_string_char() {
643            let ty = ComplexColumnType::Char { length: 5 };
644            assert_eq!(ty.to_display_string(), "char(5)");
645        }
646
647        #[test]
648        fn test_complex_column_type_to_display_string_custom() {
649            let ty = ComplexColumnType::Custom {
650                custom_type: "TSVECTOR".into(),
651            };
652            assert_eq!(ty.to_display_string(), "tsvector");
653        }
654
655        #[test]
656        fn test_complex_column_type_to_display_string_string_enum() {
657            let ty = ComplexColumnType::Enum {
658                name: "user_status".into(),
659                values: EnumValues::String(vec!["active".into(), "inactive".into()]),
660            };
661            assert_eq!(ty.to_display_string(), "enum<user_status>");
662        }
663
664        #[test]
665        fn test_complex_column_type_to_display_string_integer_enum() {
666            let ty = ComplexColumnType::Enum {
667                name: "priority".into(),
668                values: EnumValues::Integer(vec![
669                    NumValue {
670                        name: "Low".into(),
671                        value: 0,
672                    },
673                    NumValue {
674                        name: "High".into(),
675                        value: 10,
676                    },
677                ]),
678            };
679            assert_eq!(ty.to_display_string(), "enum<priority> (integer)");
680        }
681
682        #[test]
683        fn test_column_type_to_display_string_simple() {
684            let ty = ColumnType::Simple(SimpleColumnType::Integer);
685            assert_eq!(ty.to_display_string(), "integer");
686        }
687
688        #[test]
689        fn test_column_type_to_display_string_complex() {
690            let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 100 });
691            assert_eq!(ty.to_display_string(), "varchar(100)");
692        }
693
694        // Tests for default_fill_value
695        #[rstest]
696        #[case(SimpleColumnType::SmallInt, "0")]
697        #[case(SimpleColumnType::Integer, "0")]
698        #[case(SimpleColumnType::BigInt, "0")]
699        #[case(SimpleColumnType::Real, "0.0")]
700        #[case(SimpleColumnType::DoublePrecision, "0.0")]
701        #[case(SimpleColumnType::Boolean, "false")]
702        #[case(SimpleColumnType::Text, "''")]
703        #[case(SimpleColumnType::Date, "'1970-01-01'")]
704        #[case(SimpleColumnType::Time, "'00:00:00'")]
705        #[case(SimpleColumnType::Timestamp, "CURRENT_TIMESTAMP")]
706        #[case(SimpleColumnType::Timestamptz, "CURRENT_TIMESTAMP")]
707        #[case(SimpleColumnType::Interval, "'0'")]
708        #[case(SimpleColumnType::Bytea, "''")]
709        #[case(SimpleColumnType::Uuid, "'00000000-0000-0000-0000-000000000000'")]
710        #[case(SimpleColumnType::Json, "'{}'")]
711        #[case(SimpleColumnType::Inet, "'0.0.0.0'")]
712        #[case(SimpleColumnType::Cidr, "'0.0.0.0'")]
713        #[case(SimpleColumnType::Macaddr, "'00:00:00:00:00:00'")]
714        #[case(SimpleColumnType::Xml, "'<xml/>'")]
715        fn test_simple_column_type_default_fill_value(
716            #[case] column_type: SimpleColumnType,
717            #[case] expected: &str,
718        ) {
719            assert_eq!(column_type.default_fill_value(), expected);
720        }
721
722        #[test]
723        fn test_complex_column_type_default_fill_value_varchar() {
724            let ty = ComplexColumnType::Varchar { length: 255 };
725            assert_eq!(ty.default_fill_value(), "''");
726        }
727
728        #[test]
729        fn test_complex_column_type_default_fill_value_char() {
730            let ty = ComplexColumnType::Char { length: 1 };
731            assert_eq!(ty.default_fill_value(), "''");
732        }
733
734        #[test]
735        fn test_complex_column_type_default_fill_value_numeric() {
736            let ty = ComplexColumnType::Numeric {
737                precision: 10,
738                scale: 2,
739            };
740            assert_eq!(ty.default_fill_value(), "0");
741        }
742
743        #[test]
744        fn test_complex_column_type_default_fill_value_custom() {
745            let ty = ComplexColumnType::Custom {
746                custom_type: "MONEY".into(),
747            };
748            assert_eq!(ty.default_fill_value(), "''");
749        }
750
751        #[test]
752        fn test_complex_column_type_default_fill_value_enum() {
753            let ty = ComplexColumnType::Enum {
754                name: "status".into(),
755                values: EnumValues::String(vec!["active".into()]),
756            };
757            assert_eq!(ty.default_fill_value(), "''");
758        }
759
760        #[test]
761        fn test_column_type_default_fill_value_simple() {
762            let ty = ColumnType::Simple(SimpleColumnType::Integer);
763            assert_eq!(ty.default_fill_value(), "0");
764        }
765
766        #[test]
767        fn test_column_type_default_fill_value_complex() {
768            let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 100 });
769            assert_eq!(ty.default_fill_value(), "''");
770        }
771
772        // Tests for enum_variant_names
773        #[test]
774        fn test_enum_variant_names_simple_type_returns_none() {
775            let ty = ColumnType::Simple(SimpleColumnType::Integer);
776            assert_eq!(ty.enum_variant_names(), None);
777        }
778
779        #[test]
780        fn test_enum_variant_names_complex_non_enum_returns_none() {
781            let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 255 });
782            assert_eq!(ty.enum_variant_names(), None);
783        }
784
785        #[test]
786        fn test_enum_variant_names_complex_numeric_returns_none() {
787            let ty = ColumnType::Complex(ComplexColumnType::Numeric {
788                precision: 10,
789                scale: 2,
790            });
791            assert_eq!(ty.enum_variant_names(), None);
792        }
793
794        #[test]
795        fn test_enum_variant_names_complex_char_returns_none() {
796            let ty = ColumnType::Complex(ComplexColumnType::Char { length: 1 });
797            assert_eq!(ty.enum_variant_names(), None);
798        }
799
800        #[test]
801        fn test_enum_variant_names_complex_custom_returns_none() {
802            let ty = ColumnType::Complex(ComplexColumnType::Custom {
803                custom_type: "TSVECTOR".into(),
804            });
805            assert_eq!(ty.enum_variant_names(), None);
806        }
807
808        #[test]
809        fn test_enum_variant_names_string_enum() {
810            let ty = ColumnType::Complex(ComplexColumnType::Enum {
811                name: "status".into(),
812                values: EnumValues::String(vec![
813                    "active".into(),
814                    "inactive".into(),
815                    "pending".into(),
816                ]),
817            });
818            assert_eq!(
819                ty.enum_variant_names(),
820                Some(vec![
821                    "active".to_string(),
822                    "inactive".to_string(),
823                    "pending".to_string()
824                ])
825            );
826        }
827
828        #[test]
829        fn test_enum_variant_names_integer_enum() {
830            let ty = ColumnType::Complex(ComplexColumnType::Enum {
831                name: "priority".into(),
832                values: EnumValues::Integer(vec![
833                    NumValue {
834                        name: "Low".into(),
835                        value: 0,
836                    },
837                    NumValue {
838                        name: "Medium".into(),
839                        value: 5,
840                    },
841                    NumValue {
842                        name: "High".into(),
843                        value: 10,
844                    },
845                ]),
846            });
847            assert_eq!(
848                ty.enum_variant_names(),
849                Some(vec![
850                    "Low".to_string(),
851                    "Medium".to_string(),
852                    "High".to_string()
853                ])
854            );
855        }
856
857        #[test]
858        fn test_enum_variant_names_empty_string_enum() {
859            let ty = ColumnType::Complex(ComplexColumnType::Enum {
860                name: "empty".into(),
861                values: EnumValues::String(vec![]),
862            });
863            assert_eq!(ty.enum_variant_names(), Some(vec![]));
864        }
865
866        #[test]
867        fn test_enum_variant_names_empty_integer_enum() {
868            let ty = ColumnType::Complex(ComplexColumnType::Enum {
869                name: "empty".into(),
870                values: EnumValues::Integer(vec![]),
871            });
872            assert_eq!(ty.enum_variant_names(), Some(vec![]));
873        }
874    }
875}