Skip to main content

vespertide_core/schema/
column.rs

1use serde::{Deserialize, Serialize};
2
3use crate::schema::{
4    foreign_key::ForeignKeySyntax,
5    names::ColumnName,
6    primary_key::PrimaryKeySyntax,
7    str_or_bool::{StrOrBoolOrArray, StringOrBool},
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(rename_all = "snake_case")]
13pub struct ColumnDef {
14    pub name: ColumnName,
15    pub r#type: ColumnType,
16    pub nullable: bool,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub default: Option<StringOrBool>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub comment: Option<String>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub primary_key: Option<PrimaryKeySyntax>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub unique: Option<StrOrBoolOrArray>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub index: Option<StrOrBoolOrArray>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub foreign_key: Option<ForeignKeySyntax>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[serde(rename_all = "snake_case", untagged)]
34pub enum ColumnType {
35    Simple(SimpleColumnType),
36    Complex(ComplexColumnType),
37}
38
39impl ColumnType {
40    /// Returns true if this type supports auto_increment (integer types only)
41    pub fn supports_auto_increment(&self) -> bool {
42        match self {
43            ColumnType::Simple(ty) => ty.supports_auto_increment(),
44            ColumnType::Complex(_) => false,
45        }
46    }
47
48    /// Check if two column types require a migration.
49    /// For integer enums, no migration is ever needed because the underlying DB type is always INTEGER.
50    /// The enum name and values only affect code generation (SeaORM entities), not the database schema.
51    pub fn requires_migration(&self, other: &ColumnType) -> bool {
52        match (self, other) {
53            (
54                ColumnType::Complex(ComplexColumnType::Enum {
55                    values: values1, ..
56                }),
57                ColumnType::Complex(ComplexColumnType::Enum {
58                    values: values2, ..
59                }),
60            ) => {
61                // Both are integer enums - never require migration (DB type is always INTEGER)
62                if values1.is_integer() && values2.is_integer() {
63                    false
64                } else {
65                    // String enums: compare only values, not name.
66                    // The enum name is a user-facing label; the actual DB type name
67                    // is auto-generated with a table prefix at SQL generation time.
68                    // Different labels with identical values don't require a migration.
69                    values1 != values2
70                }
71            }
72            _ => self != other,
73        }
74    }
75
76    /// Convert column type to Rust type string (for SeaORM entity generation)
77    pub fn to_rust_type(&self, nullable: bool) -> String {
78        let base = match self {
79            ColumnType::Simple(ty) => match ty {
80                SimpleColumnType::SmallInt => "i16".to_string(),
81                SimpleColumnType::Integer => "i32".to_string(),
82                SimpleColumnType::BigInt => "i64".to_string(),
83                SimpleColumnType::Real => "f32".to_string(),
84                SimpleColumnType::DoublePrecision => "f64".to_string(),
85                SimpleColumnType::Text => "String".to_string(),
86                SimpleColumnType::Boolean => "bool".to_string(),
87                SimpleColumnType::Date => "Date".to_string(),
88                SimpleColumnType::Time => "Time".to_string(),
89                SimpleColumnType::Timestamp => "DateTime".to_string(),
90                SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(),
91                SimpleColumnType::Interval => "String".to_string(),
92                SimpleColumnType::Bytea => "Vec<u8>".to_string(),
93                SimpleColumnType::Uuid => "Uuid".to_string(),
94                SimpleColumnType::Json => "Json".to_string(),
95                // SimpleColumnType::Jsonb => "Json".to_string(),
96                SimpleColumnType::Inet | SimpleColumnType::Cidr => "String".to_string(),
97                SimpleColumnType::Macaddr => "String".to_string(),
98                SimpleColumnType::Xml => "String".to_string(),
99            },
100            ColumnType::Complex(ty) => match ty {
101                ComplexColumnType::Varchar { .. } => "String".to_string(),
102                ComplexColumnType::Numeric { .. } => "Decimal".to_string(),
103                ComplexColumnType::Char { .. } => "String".to_string(),
104                ComplexColumnType::Custom { .. } => "String".to_string(), // Default for custom types
105                ComplexColumnType::Enum { .. } => "String".to_string(),
106            },
107        };
108
109        if nullable {
110            format!("Option<{}>", base)
111        } else {
112            base
113        }
114    }
115
116    /// Convert column type to human-readable display string (for CLI prompts)
117    /// Examples: "integer", "text", "varchar(255)", "numeric(10,2)"
118    pub fn to_display_string(&self) -> String {
119        match self {
120            ColumnType::Simple(ty) => ty.to_display_string(),
121            ColumnType::Complex(ty) => ty.to_display_string(),
122        }
123    }
124
125    /// Get the default fill value for this column type (for CLI prompts)
126    /// Returns None if no sensible default exists for the type
127    pub fn default_fill_value(&self) -> &'static str {
128        match self {
129            ColumnType::Simple(ty) => ty.default_fill_value(),
130            ColumnType::Complex(ty) => ty.default_fill_value(),
131        }
132    }
133
134    /// Get enum variant names if this is an enum type
135    /// Returns None if not an enum, Some(names) otherwise
136    pub fn enum_variant_names(&self) -> Option<Vec<String>> {
137        match self {
138            ColumnType::Complex(ComplexColumnType::Enum { values, .. }) => Some(
139                values
140                    .variant_names()
141                    .into_iter()
142                    .map(String::from)
143                    .collect(),
144            ),
145            _ => None,
146        }
147    }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[serde(rename_all = "snake_case")]
153pub enum SimpleColumnType {
154    SmallInt,
155    Integer,
156    BigInt,
157    Real,
158    DoublePrecision,
159
160    // Text types
161    Text,
162
163    // Boolean type
164    Boolean,
165
166    // Date/Time types
167    Date,
168    Time,
169    Timestamp,
170    Timestamptz,
171    Interval,
172
173    // Binary type
174    Bytea,
175
176    // UUID type
177    Uuid,
178
179    // JSON types
180    Json,
181    // Jsonb,
182
183    // Network types
184    Inet,
185    Cidr,
186    Macaddr,
187
188    // XML type
189    Xml,
190}
191
192impl SimpleColumnType {
193    /// Returns true if this type supports auto_increment (integer types only)
194    pub fn supports_auto_increment(&self) -> bool {
195        matches!(
196            self,
197            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt
198        )
199    }
200
201    /// Convert to human-readable display string
202    pub fn to_display_string(&self) -> String {
203        match self {
204            SimpleColumnType::SmallInt => "smallint".to_string(),
205            SimpleColumnType::Integer => "integer".to_string(),
206            SimpleColumnType::BigInt => "bigint".to_string(),
207            SimpleColumnType::Real => "real".to_string(),
208            SimpleColumnType::DoublePrecision => "double precision".to_string(),
209            SimpleColumnType::Text => "text".to_string(),
210            SimpleColumnType::Boolean => "boolean".to_string(),
211            SimpleColumnType::Date => "date".to_string(),
212            SimpleColumnType::Time => "time".to_string(),
213            SimpleColumnType::Timestamp => "timestamp".to_string(),
214            SimpleColumnType::Timestamptz => "timestamptz".to_string(),
215            SimpleColumnType::Interval => "interval".to_string(),
216            SimpleColumnType::Bytea => "bytea".to_string(),
217            SimpleColumnType::Uuid => "uuid".to_string(),
218            SimpleColumnType::Json => "json".to_string(),
219            SimpleColumnType::Inet => "inet".to_string(),
220            SimpleColumnType::Cidr => "cidr".to_string(),
221            SimpleColumnType::Macaddr => "macaddr".to_string(),
222            SimpleColumnType::Xml => "xml".to_string(),
223        }
224    }
225
226    /// Get the default fill value for this type
227    /// Returns None if no sensible default exists
228    pub fn default_fill_value(&self) -> &'static str {
229        match self {
230            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
231                "0"
232            }
233            SimpleColumnType::Real | SimpleColumnType::DoublePrecision => "0.0",
234            SimpleColumnType::Boolean => "false",
235            SimpleColumnType::Text => "''",
236            SimpleColumnType::Date => "'1970-01-01'",
237            SimpleColumnType::Time => "'00:00:00'",
238            SimpleColumnType::Timestamp | SimpleColumnType::Timestamptz => "CURRENT_TIMESTAMP",
239            SimpleColumnType::Interval => "'0'",
240            SimpleColumnType::Uuid => "'00000000-0000-0000-0000-000000000000'",
241            SimpleColumnType::Json => "'{}'",
242            SimpleColumnType::Bytea => "''",
243            SimpleColumnType::Inet | SimpleColumnType::Cidr => "'0.0.0.0'",
244            SimpleColumnType::Macaddr => "'00:00:00:00:00:00'",
245            SimpleColumnType::Xml => "'<xml/>'",
246        }
247    }
248}
249
250/// Integer enum variant with name and numeric value
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253pub struct NumValue {
254    pub name: String,
255    pub value: i32,
256}
257
258/// Enum values definition - either all string or all integer
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261#[serde(untagged)]
262pub enum EnumValues {
263    String(Vec<String>),
264    Integer(Vec<NumValue>),
265}
266
267impl EnumValues {
268    /// Check if this is a string enum
269    pub fn is_string(&self) -> bool {
270        matches!(self, EnumValues::String(_))
271    }
272
273    /// Check if this is an integer enum
274    pub fn is_integer(&self) -> bool {
275        matches!(self, EnumValues::Integer(_))
276    }
277
278    /// Get all variant names
279    pub fn variant_names(&self) -> Vec<&str> {
280        match self {
281            EnumValues::String(values) => values.iter().map(|s| s.as_str()).collect(),
282            EnumValues::Integer(values) => values.iter().map(|v| v.name.as_str()).collect(),
283        }
284    }
285
286    /// Get the number of variants
287    pub fn len(&self) -> usize {
288        match self {
289            EnumValues::String(values) => values.len(),
290            EnumValues::Integer(values) => values.len(),
291        }
292    }
293
294    /// Check if there are no variants
295    pub fn is_empty(&self) -> bool {
296        self.len() == 0
297    }
298
299    /// Get SQL values for CREATE TYPE ENUM (only for string enums)
300    /// Returns quoted strings like 'value1', 'value2'
301    pub fn to_sql_values(&self) -> Vec<String> {
302        match self {
303            EnumValues::String(values) => values
304                .iter()
305                .map(|s| format!("'{}'", s.replace('\'', "''")))
306                .collect(),
307            EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(),
308        }
309    }
310}
311
312impl From<Vec<String>> for EnumValues {
313    fn from(values: Vec<String>) -> Self {
314        EnumValues::String(values)
315    }
316}
317
318impl From<Vec<&str>> for EnumValues {
319    fn from(values: Vec<&str>) -> Self {
320        EnumValues::String(values.into_iter().map(|s| s.to_string()).collect())
321    }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
326#[serde(rename_all = "snake_case", tag = "kind")]
327pub enum ComplexColumnType {
328    Varchar { length: u32 },
329    Numeric { precision: u32, scale: u32 },
330    Char { length: u32 },
331    Custom { custom_type: String },
332    Enum { name: String, values: EnumValues },
333}
334
335impl ComplexColumnType {
336    /// Convert to human-readable display string
337    pub fn to_display_string(&self) -> String {
338        match self {
339            ComplexColumnType::Varchar { length } => format!("varchar({})", length),
340            ComplexColumnType::Numeric { precision, scale } => {
341                format!("numeric({},{})", precision, scale)
342            }
343            ComplexColumnType::Char { length } => format!("char({})", length),
344            ComplexColumnType::Custom { custom_type } => custom_type.to_lowercase(),
345            ComplexColumnType::Enum { name, values } => {
346                if values.is_integer() {
347                    format!("enum<{}> (integer)", name)
348                } else {
349                    format!("enum<{}>", name)
350                }
351            }
352        }
353    }
354
355    /// Get the default fill value for this type.
356    pub fn default_fill_value(&self) -> &'static str {
357        match self {
358            ComplexColumnType::Varchar { .. } | ComplexColumnType::Char { .. } => "''",
359            ComplexColumnType::Numeric { .. } => "0",
360            ComplexColumnType::Custom { .. } => "''",
361            ComplexColumnType::Enum { .. } => "''",
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use rstest::rstest;
370
371    #[rstest]
372    #[case(SimpleColumnType::SmallInt, "i16")]
373    #[case(SimpleColumnType::Integer, "i32")]
374    #[case(SimpleColumnType::BigInt, "i64")]
375    #[case(SimpleColumnType::Real, "f32")]
376    #[case(SimpleColumnType::DoublePrecision, "f64")]
377    #[case(SimpleColumnType::Text, "String")]
378    #[case(SimpleColumnType::Boolean, "bool")]
379    #[case(SimpleColumnType::Date, "Date")]
380    #[case(SimpleColumnType::Time, "Time")]
381    #[case(SimpleColumnType::Timestamp, "DateTime")]
382    #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")]
383    #[case(SimpleColumnType::Interval, "String")]
384    #[case(SimpleColumnType::Bytea, "Vec<u8>")]
385    #[case(SimpleColumnType::Uuid, "Uuid")]
386    #[case(SimpleColumnType::Json, "Json")]
387    // #[case(SimpleColumnType::Jsonb, "Json")]
388    #[case(SimpleColumnType::Inet, "String")]
389    #[case(SimpleColumnType::Cidr, "String")]
390    #[case(SimpleColumnType::Macaddr, "String")]
391    #[case(SimpleColumnType::Xml, "String")]
392    fn test_simple_column_type_to_rust_type_not_nullable(
393        #[case] column_type: SimpleColumnType,
394        #[case] expected: &str,
395    ) {
396        assert_eq!(
397            ColumnType::Simple(column_type).to_rust_type(false),
398            expected
399        );
400    }
401
402    #[rstest]
403    #[case(SimpleColumnType::SmallInt, "Option<i16>")]
404    #[case(SimpleColumnType::Integer, "Option<i32>")]
405    #[case(SimpleColumnType::BigInt, "Option<i64>")]
406    #[case(SimpleColumnType::Real, "Option<f32>")]
407    #[case(SimpleColumnType::DoublePrecision, "Option<f64>")]
408    #[case(SimpleColumnType::Text, "Option<String>")]
409    #[case(SimpleColumnType::Boolean, "Option<bool>")]
410    #[case(SimpleColumnType::Date, "Option<Date>")]
411    #[case(SimpleColumnType::Time, "Option<Time>")]
412    #[case(SimpleColumnType::Timestamp, "Option<DateTime>")]
413    #[case(SimpleColumnType::Timestamptz, "Option<DateTimeWithTimeZone>")]
414    #[case(SimpleColumnType::Interval, "Option<String>")]
415    #[case(SimpleColumnType::Bytea, "Option<Vec<u8>>")]
416    #[case(SimpleColumnType::Uuid, "Option<Uuid>")]
417    #[case(SimpleColumnType::Json, "Option<Json>")]
418    // #[case(SimpleColumnType::Jsonb, "Option<Json>")]
419    #[case(SimpleColumnType::Inet, "Option<String>")]
420    #[case(SimpleColumnType::Cidr, "Option<String>")]
421    #[case(SimpleColumnType::Macaddr, "Option<String>")]
422    #[case(SimpleColumnType::Xml, "Option<String>")]
423    fn test_simple_column_type_to_rust_type_nullable(
424        #[case] column_type: SimpleColumnType,
425        #[case] expected: &str,
426    ) {
427        assert_eq!(ColumnType::Simple(column_type).to_rust_type(true), expected);
428    }
429
430    #[rstest]
431    #[case(ComplexColumnType::Varchar { length: 255 }, false, "String")]
432    #[case(ComplexColumnType::Varchar { length: 50 }, false, "String")]
433    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, false, "Decimal")]
434    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, false, "Decimal")]
435    #[case(ComplexColumnType::Char { length: 10 }, false, "String")]
436    #[case(ComplexColumnType::Char { length: 1 }, false, "String")]
437    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, false, "String")]
438    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, false, "String")]
439    #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, false, "String")]
440    fn test_complex_column_type_to_rust_type_not_nullable(
441        #[case] column_type: ComplexColumnType,
442        #[case] nullable: bool,
443        #[case] expected: &str,
444    ) {
445        assert_eq!(
446            ColumnType::Complex(column_type).to_rust_type(nullable),
447            expected
448        );
449    }
450
451    #[rstest]
452    #[case(ComplexColumnType::Varchar { length: 255 }, "Option<String>")]
453    #[case(ComplexColumnType::Varchar { length: 50 }, "Option<String>")]
454    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "Option<Decimal>")]
455    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "Option<Decimal>")]
456    #[case(ComplexColumnType::Char { length: 10 }, "Option<String>")]
457    #[case(ComplexColumnType::Char { length: 1 }, "Option<String>")]
458    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "Option<String>")]
459    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "Option<String>")]
460    #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, "Option<String>")]
461    fn test_complex_column_type_to_rust_type_nullable(
462        #[case] column_type: ComplexColumnType,
463        #[case] expected: &str,
464    ) {
465        assert_eq!(
466            ColumnType::Complex(column_type).to_rust_type(true),
467            expected
468        );
469    }
470
471    #[rstest]
472    #[case(ComplexColumnType::Varchar { length: 255 })]
473    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 })]
474    #[case(ComplexColumnType::Char { length: 1 })]
475    #[case(ComplexColumnType::Custom { custom_type: "SERIAL".into() })]
476    #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec![]) })]
477    fn test_complex_column_type_does_not_support_auto_increment(
478        #[case] column_type: ComplexColumnType,
479    ) {
480        // Complex types never support auto_increment
481        assert!(!ColumnType::Complex(column_type).supports_auto_increment());
482    }
483
484    #[test]
485    fn test_enum_values_is_string() {
486        let string_vals = EnumValues::String(vec!["active".into()]);
487        let int_vals = EnumValues::Integer(vec![NumValue {
488            name: "Active".into(),
489            value: 1,
490        }]);
491        assert!(string_vals.is_string());
492        assert!(!int_vals.is_string());
493    }
494
495    #[test]
496    fn test_enum_values_is_integer() {
497        let string_vals = EnumValues::String(vec!["active".into()]);
498        let int_vals = EnumValues::Integer(vec![NumValue {
499            name: "Active".into(),
500            value: 1,
501        }]);
502        assert!(!string_vals.is_integer());
503        assert!(int_vals.is_integer());
504    }
505
506    #[test]
507    fn test_enum_values_variant_names_string() {
508        let vals = EnumValues::String(vec!["pending".into(), "active".into()]);
509        assert_eq!(vals.variant_names(), vec!["pending", "active"]);
510    }
511
512    #[test]
513    fn test_enum_values_variant_names_integer() {
514        let vals = EnumValues::Integer(vec![
515            NumValue {
516                name: "Low".into(),
517                value: 0,
518            },
519            NumValue {
520                name: "High".into(),
521                value: 10,
522            },
523        ]);
524        assert_eq!(vals.variant_names(), vec!["Low", "High"]);
525    }
526
527    #[test]
528    fn test_enum_values_len_and_is_empty() {
529        // String variant
530        let empty = EnumValues::String(vec![]);
531        let non_empty = EnumValues::String(vec!["a".into()]);
532        assert!(empty.is_empty());
533        assert_eq!(empty.len(), 0);
534        assert!(!non_empty.is_empty());
535        assert_eq!(non_empty.len(), 1);
536
537        // Integer variant
538        let empty_int = EnumValues::Integer(vec![]);
539        let non_empty_int = EnumValues::Integer(vec![
540            NumValue {
541                name: "A".into(),
542                value: 0,
543            },
544            NumValue {
545                name: "B".into(),
546                value: 1,
547            },
548        ]);
549        assert!(empty_int.is_empty());
550        assert_eq!(empty_int.len(), 0);
551        assert!(!non_empty_int.is_empty());
552        assert_eq!(non_empty_int.len(), 2);
553    }
554
555    #[test]
556    fn test_enum_values_to_sql_values_string() {
557        let vals = EnumValues::String(vec!["active".into(), "pending".into()]);
558        assert_eq!(vals.to_sql_values(), vec!["'active'", "'pending'"]);
559    }
560
561    #[test]
562    fn test_enum_values_to_sql_values_integer() {
563        let vals = EnumValues::Integer(vec![
564            NumValue {
565                name: "Low".into(),
566                value: 0,
567            },
568            NumValue {
569                name: "High".into(),
570                value: 10,
571            },
572        ]);
573        assert_eq!(vals.to_sql_values(), vec!["0", "10"]);
574    }
575
576    #[test]
577    fn test_enum_values_from_vec_string() {
578        let vals: EnumValues = vec!["a".to_string(), "b".to_string()].into();
579        assert!(matches!(vals, EnumValues::String(_)));
580    }
581
582    #[test]
583    fn test_enum_values_from_vec_str() {
584        let vals: EnumValues = vec!["a", "b"].into();
585        assert!(matches!(vals, EnumValues::String(_)));
586    }
587
588    #[rstest]
589    #[case(SimpleColumnType::SmallInt, true)]
590    #[case(SimpleColumnType::Integer, true)]
591    #[case(SimpleColumnType::BigInt, true)]
592    #[case(SimpleColumnType::Text, false)]
593    #[case(SimpleColumnType::Boolean, false)]
594    fn test_simple_column_type_supports_auto_increment(
595        #[case] ty: SimpleColumnType,
596        #[case] expected: bool,
597    ) {
598        assert_eq!(ty.supports_auto_increment(), expected);
599    }
600
601    #[rstest]
602    #[case(SimpleColumnType::Integer, true)]
603    #[case(SimpleColumnType::Text, false)]
604    fn test_column_type_simple_supports_auto_increment(
605        #[case] ty: SimpleColumnType,
606        #[case] expected: bool,
607    ) {
608        assert_eq!(ColumnType::Simple(ty).supports_auto_increment(), expected);
609    }
610
611    #[test]
612    fn test_requires_migration_integer_enum_values_changed() {
613        // Integer enum values changed - should NOT require migration
614        let from = ColumnType::Complex(ComplexColumnType::Enum {
615            name: "status".into(),
616            values: EnumValues::Integer(vec![
617                NumValue {
618                    name: "Pending".into(),
619                    value: 0,
620                },
621                NumValue {
622                    name: "Active".into(),
623                    value: 1,
624                },
625            ]),
626        });
627        let to = ColumnType::Complex(ComplexColumnType::Enum {
628            name: "status".into(),
629            values: EnumValues::Integer(vec![
630                NumValue {
631                    name: "Pending".into(),
632                    value: 0,
633                },
634                NumValue {
635                    name: "Active".into(),
636                    value: 1,
637                },
638                NumValue {
639                    name: "Completed".into(),
640                    value: 100,
641                },
642            ]),
643        });
644        assert!(!from.requires_migration(&to));
645    }
646
647    #[test]
648    fn test_requires_migration_integer_enum_name_changed() {
649        // Integer enum name changed - should NOT require migration (DB type is always INTEGER)
650        let from = ColumnType::Complex(ComplexColumnType::Enum {
651            name: "old_status".into(),
652            values: EnumValues::Integer(vec![NumValue {
653                name: "Pending".into(),
654                value: 0,
655            }]),
656        });
657        let to = ColumnType::Complex(ComplexColumnType::Enum {
658            name: "new_status".into(),
659            values: EnumValues::Integer(vec![NumValue {
660                name: "Pending".into(),
661                value: 0,
662            }]),
663        });
664        assert!(!from.requires_migration(&to));
665    }
666
667    #[test]
668    fn test_requires_migration_string_enum_values_changed() {
669        // String enum values changed - SHOULD require migration
670        let from = ColumnType::Complex(ComplexColumnType::Enum {
671            name: "status".into(),
672            values: EnumValues::String(vec!["pending".into(), "active".into()]),
673        });
674        let to = ColumnType::Complex(ComplexColumnType::Enum {
675            name: "status".into(),
676            values: EnumValues::String(vec!["pending".into(), "active".into(), "completed".into()]),
677        });
678        assert!(from.requires_migration(&to));
679    }
680
681    #[test]
682    fn test_requires_migration_simple_types() {
683        let int = ColumnType::Simple(SimpleColumnType::Integer);
684        let text = ColumnType::Simple(SimpleColumnType::Text);
685        assert!(int.requires_migration(&text));
686        assert!(!int.requires_migration(&int));
687    }
688
689    #[test]
690    fn test_requires_migration_mixed_enum_types() {
691        // String enum to integer enum - SHOULD require migration
692        let string_enum = ColumnType::Complex(ComplexColumnType::Enum {
693            name: "status".into(),
694            values: EnumValues::String(vec!["pending".into()]),
695        });
696        let int_enum = ColumnType::Complex(ComplexColumnType::Enum {
697            name: "status".into(),
698            values: EnumValues::Integer(vec![NumValue {
699                name: "Pending".into(),
700                value: 0,
701            }]),
702        });
703        assert!(string_enum.requires_migration(&int_enum));
704    }
705
706    // Tests for to_display_string
707    #[rstest]
708    #[case(SimpleColumnType::SmallInt, "smallint")]
709    #[case(SimpleColumnType::Integer, "integer")]
710    #[case(SimpleColumnType::BigInt, "bigint")]
711    #[case(SimpleColumnType::Real, "real")]
712    #[case(SimpleColumnType::DoublePrecision, "double precision")]
713    #[case(SimpleColumnType::Text, "text")]
714    #[case(SimpleColumnType::Boolean, "boolean")]
715    #[case(SimpleColumnType::Date, "date")]
716    #[case(SimpleColumnType::Time, "time")]
717    #[case(SimpleColumnType::Timestamp, "timestamp")]
718    #[case(SimpleColumnType::Timestamptz, "timestamptz")]
719    #[case(SimpleColumnType::Interval, "interval")]
720    #[case(SimpleColumnType::Bytea, "bytea")]
721    #[case(SimpleColumnType::Uuid, "uuid")]
722    #[case(SimpleColumnType::Json, "json")]
723    #[case(SimpleColumnType::Inet, "inet")]
724    #[case(SimpleColumnType::Cidr, "cidr")]
725    #[case(SimpleColumnType::Macaddr, "macaddr")]
726    #[case(SimpleColumnType::Xml, "xml")]
727    fn test_simple_column_type_to_display_string(
728        #[case] column_type: SimpleColumnType,
729        #[case] expected: &str,
730    ) {
731        assert_eq!(column_type.to_display_string(), expected);
732    }
733
734    #[test]
735    fn test_complex_column_type_to_display_string_varchar() {
736        let ty = ComplexColumnType::Varchar { length: 255 };
737        assert_eq!(ty.to_display_string(), "varchar(255)");
738    }
739
740    #[test]
741    fn test_complex_column_type_to_display_string_numeric() {
742        let ty = ComplexColumnType::Numeric {
743            precision: 10,
744            scale: 2,
745        };
746        assert_eq!(ty.to_display_string(), "numeric(10,2)");
747    }
748
749    #[test]
750    fn test_complex_column_type_to_display_string_char() {
751        let ty = ComplexColumnType::Char { length: 5 };
752        assert_eq!(ty.to_display_string(), "char(5)");
753    }
754
755    #[test]
756    fn test_complex_column_type_to_display_string_custom() {
757        let ty = ComplexColumnType::Custom {
758            custom_type: "TSVECTOR".into(),
759        };
760        assert_eq!(ty.to_display_string(), "tsvector");
761    }
762
763    #[test]
764    fn test_complex_column_type_to_display_string_string_enum() {
765        let ty = ComplexColumnType::Enum {
766            name: "user_status".into(),
767            values: EnumValues::String(vec!["active".into(), "inactive".into()]),
768        };
769        assert_eq!(ty.to_display_string(), "enum<user_status>");
770    }
771
772    #[test]
773    fn test_complex_column_type_to_display_string_integer_enum() {
774        let ty = ComplexColumnType::Enum {
775            name: "priority".into(),
776            values: EnumValues::Integer(vec![
777                NumValue {
778                    name: "Low".into(),
779                    value: 0,
780                },
781                NumValue {
782                    name: "High".into(),
783                    value: 10,
784                },
785            ]),
786        };
787        assert_eq!(ty.to_display_string(), "enum<priority> (integer)");
788    }
789
790    #[test]
791    fn test_column_type_to_display_string_simple() {
792        let ty = ColumnType::Simple(SimpleColumnType::Integer);
793        assert_eq!(ty.to_display_string(), "integer");
794    }
795
796    #[test]
797    fn test_column_type_to_display_string_complex() {
798        let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 100 });
799        assert_eq!(ty.to_display_string(), "varchar(100)");
800    }
801
802    // Tests for default_fill_value
803    #[rstest]
804    #[case(SimpleColumnType::SmallInt, "0")]
805    #[case(SimpleColumnType::Integer, "0")]
806    #[case(SimpleColumnType::BigInt, "0")]
807    #[case(SimpleColumnType::Real, "0.0")]
808    #[case(SimpleColumnType::DoublePrecision, "0.0")]
809    #[case(SimpleColumnType::Boolean, "false")]
810    #[case(SimpleColumnType::Text, "''")]
811    #[case(SimpleColumnType::Date, "'1970-01-01'")]
812    #[case(SimpleColumnType::Time, "'00:00:00'")]
813    #[case(SimpleColumnType::Timestamp, "CURRENT_TIMESTAMP")]
814    #[case(SimpleColumnType::Timestamptz, "CURRENT_TIMESTAMP")]
815    #[case(SimpleColumnType::Interval, "'0'")]
816    #[case(SimpleColumnType::Bytea, "''")]
817    #[case(SimpleColumnType::Uuid, "'00000000-0000-0000-0000-000000000000'")]
818    #[case(SimpleColumnType::Json, "'{}'")]
819    #[case(SimpleColumnType::Inet, "'0.0.0.0'")]
820    #[case(SimpleColumnType::Cidr, "'0.0.0.0'")]
821    #[case(SimpleColumnType::Macaddr, "'00:00:00:00:00:00'")]
822    #[case(SimpleColumnType::Xml, "'<xml/>'")]
823    fn test_simple_column_type_default_fill_value(
824        #[case] column_type: SimpleColumnType,
825        #[case] expected: &str,
826    ) {
827        assert_eq!(column_type.default_fill_value(), expected);
828    }
829
830    #[test]
831    fn test_complex_column_type_default_fill_value_varchar() {
832        let ty = ComplexColumnType::Varchar { length: 255 };
833        assert_eq!(ty.default_fill_value(), "''");
834    }
835
836    #[test]
837    fn test_complex_column_type_default_fill_value_char() {
838        let ty = ComplexColumnType::Char { length: 1 };
839        assert_eq!(ty.default_fill_value(), "''");
840    }
841
842    #[test]
843    fn test_complex_column_type_default_fill_value_numeric() {
844        let ty = ComplexColumnType::Numeric {
845            precision: 10,
846            scale: 2,
847        };
848        assert_eq!(ty.default_fill_value(), "0");
849    }
850
851    #[test]
852    fn test_complex_column_type_default_fill_value_custom() {
853        let ty = ComplexColumnType::Custom {
854            custom_type: "MONEY".into(),
855        };
856        assert_eq!(ty.default_fill_value(), "''");
857    }
858
859    #[test]
860    fn test_complex_column_type_default_fill_value_enum() {
861        let ty = ComplexColumnType::Enum {
862            name: "status".into(),
863            values: EnumValues::String(vec!["active".into()]),
864        };
865        assert_eq!(ty.default_fill_value(), "''");
866    }
867
868    #[test]
869    fn test_column_type_default_fill_value_simple() {
870        let ty = ColumnType::Simple(SimpleColumnType::Integer);
871        assert_eq!(ty.default_fill_value(), "0");
872    }
873
874    #[test]
875    fn test_column_type_default_fill_value_complex() {
876        let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 100 });
877        assert_eq!(ty.default_fill_value(), "''");
878    }
879
880    // Tests for enum_variant_names
881    #[test]
882    fn test_enum_variant_names_simple_type_returns_none() {
883        let ty = ColumnType::Simple(SimpleColumnType::Integer);
884        assert_eq!(ty.enum_variant_names(), None);
885    }
886
887    #[test]
888    fn test_enum_variant_names_complex_non_enum_returns_none() {
889        let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 255 });
890        assert_eq!(ty.enum_variant_names(), None);
891    }
892
893    #[test]
894    fn test_enum_variant_names_complex_numeric_returns_none() {
895        let ty = ColumnType::Complex(ComplexColumnType::Numeric {
896            precision: 10,
897            scale: 2,
898        });
899        assert_eq!(ty.enum_variant_names(), None);
900    }
901
902    #[test]
903    fn test_enum_variant_names_complex_char_returns_none() {
904        let ty = ColumnType::Complex(ComplexColumnType::Char { length: 1 });
905        assert_eq!(ty.enum_variant_names(), None);
906    }
907
908    #[test]
909    fn test_enum_variant_names_complex_custom_returns_none() {
910        let ty = ColumnType::Complex(ComplexColumnType::Custom {
911            custom_type: "TSVECTOR".into(),
912        });
913        assert_eq!(ty.enum_variant_names(), None);
914    }
915
916    #[test]
917    fn test_enum_variant_names_string_enum() {
918        let ty = ColumnType::Complex(ComplexColumnType::Enum {
919            name: "status".into(),
920            values: EnumValues::String(vec!["active".into(), "inactive".into(), "pending".into()]),
921        });
922        assert_eq!(
923            ty.enum_variant_names(),
924            Some(vec![
925                "active".to_string(),
926                "inactive".to_string(),
927                "pending".to_string()
928            ])
929        );
930    }
931
932    #[test]
933    fn test_enum_variant_names_integer_enum() {
934        let ty = ColumnType::Complex(ComplexColumnType::Enum {
935            name: "priority".into(),
936            values: EnumValues::Integer(vec![
937                NumValue {
938                    name: "Low".into(),
939                    value: 0,
940                },
941                NumValue {
942                    name: "Medium".into(),
943                    value: 5,
944                },
945                NumValue {
946                    name: "High".into(),
947                    value: 10,
948                },
949            ]),
950        });
951        assert_eq!(
952            ty.enum_variant_names(),
953            Some(vec![
954                "Low".to_string(),
955                "Medium".to_string(),
956                "High".to_string()
957            ])
958        );
959    }
960
961    #[test]
962    fn test_enum_variant_names_empty_string_enum() {
963        let ty = ColumnType::Complex(ComplexColumnType::Enum {
964            name: "empty".into(),
965            values: EnumValues::String(vec![]),
966        });
967        assert_eq!(ty.enum_variant_names(), Some(vec![]));
968    }
969
970    #[test]
971    fn test_enum_variant_names_empty_integer_enum() {
972        let ty = ColumnType::Complex(ComplexColumnType::Enum {
973            name: "empty".into(),
974            values: EnumValues::Integer(vec![]),
975        });
976        assert_eq!(ty.enum_variant_names(), Some(vec![]));
977    }
978}