vespertide_core/schema/
column.rs

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