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
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
132#[serde(rename_all = "snake_case")]
133pub enum SimpleColumnType {
134    SmallInt,
135    Integer,
136    BigInt,
137    Real,
138    DoublePrecision,
139
140    // Text types
141    Text,
142
143    // Boolean type
144    Boolean,
145
146    // Date/Time types
147    Date,
148    Time,
149    Timestamp,
150    Timestamptz,
151    Interval,
152
153    // Binary type
154    Bytea,
155
156    // UUID type
157    Uuid,
158
159    // JSON types
160    Json,
161    // Jsonb,
162
163    // Network types
164    Inet,
165    Cidr,
166    Macaddr,
167
168    // XML type
169    Xml,
170}
171
172impl SimpleColumnType {
173    /// Returns true if this type supports auto_increment (integer types only)
174    pub fn supports_auto_increment(&self) -> bool {
175        matches!(
176            self,
177            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt
178        )
179    }
180
181    /// Convert to human-readable display string
182    pub fn to_display_string(&self) -> String {
183        match self {
184            SimpleColumnType::SmallInt => "smallint".to_string(),
185            SimpleColumnType::Integer => "integer".to_string(),
186            SimpleColumnType::BigInt => "bigint".to_string(),
187            SimpleColumnType::Real => "real".to_string(),
188            SimpleColumnType::DoublePrecision => "double precision".to_string(),
189            SimpleColumnType::Text => "text".to_string(),
190            SimpleColumnType::Boolean => "boolean".to_string(),
191            SimpleColumnType::Date => "date".to_string(),
192            SimpleColumnType::Time => "time".to_string(),
193            SimpleColumnType::Timestamp => "timestamp".to_string(),
194            SimpleColumnType::Timestamptz => "timestamptz".to_string(),
195            SimpleColumnType::Interval => "interval".to_string(),
196            SimpleColumnType::Bytea => "bytea".to_string(),
197            SimpleColumnType::Uuid => "uuid".to_string(),
198            SimpleColumnType::Json => "json".to_string(),
199            SimpleColumnType::Inet => "inet".to_string(),
200            SimpleColumnType::Cidr => "cidr".to_string(),
201            SimpleColumnType::Macaddr => "macaddr".to_string(),
202            SimpleColumnType::Xml => "xml".to_string(),
203        }
204    }
205
206    /// Get the default fill value for this type
207    /// Returns None if no sensible default exists
208    pub fn default_fill_value(&self) -> Option<&'static str> {
209        match self {
210            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
211                Some("0")
212            }
213            SimpleColumnType::Real | SimpleColumnType::DoublePrecision => Some("0.0"),
214            SimpleColumnType::Boolean => Some("false"),
215            SimpleColumnType::Text => Some("''"),
216            _ => None,
217        }
218    }
219}
220
221/// Integer enum variant with name and numeric value
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
223pub struct NumValue {
224    pub name: String,
225    pub value: i32,
226}
227
228/// Enum values definition - either all string or all integer
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
230#[serde(untagged)]
231pub enum EnumValues {
232    String(Vec<String>),
233    Integer(Vec<NumValue>),
234}
235
236impl EnumValues {
237    /// Check if this is a string enum
238    pub fn is_string(&self) -> bool {
239        matches!(self, EnumValues::String(_))
240    }
241
242    /// Check if this is an integer enum
243    pub fn is_integer(&self) -> bool {
244        matches!(self, EnumValues::Integer(_))
245    }
246
247    /// Get all variant names
248    pub fn variant_names(&self) -> Vec<&str> {
249        match self {
250            EnumValues::String(values) => values.iter().map(|s| s.as_str()).collect(),
251            EnumValues::Integer(values) => values.iter().map(|v| v.name.as_str()).collect(),
252        }
253    }
254
255    /// Get the number of variants
256    pub fn len(&self) -> usize {
257        match self {
258            EnumValues::String(values) => values.len(),
259            EnumValues::Integer(values) => values.len(),
260        }
261    }
262
263    /// Check if there are no variants
264    pub fn is_empty(&self) -> bool {
265        self.len() == 0
266    }
267
268    /// Get SQL values for CREATE TYPE ENUM (only for string enums)
269    /// Returns quoted strings like 'value1', 'value2'
270    pub fn to_sql_values(&self) -> Vec<String> {
271        match self {
272            EnumValues::String(values) => values
273                .iter()
274                .map(|s| format!("'{}'", s.replace('\'', "''")))
275                .collect(),
276            EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(),
277        }
278    }
279}
280
281impl From<Vec<String>> for EnumValues {
282    fn from(values: Vec<String>) -> Self {
283        EnumValues::String(values)
284    }
285}
286
287impl From<Vec<&str>> for EnumValues {
288    fn from(values: Vec<&str>) -> Self {
289        EnumValues::String(values.into_iter().map(|s| s.to_string()).collect())
290    }
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
294#[serde(rename_all = "snake_case", tag = "kind")]
295pub enum ComplexColumnType {
296    Varchar { length: u32 },
297    Numeric { precision: u32, scale: u32 },
298    Char { length: u32 },
299    Custom { custom_type: String },
300    Enum { name: String, values: EnumValues },
301}
302
303impl ComplexColumnType {
304    /// Convert to human-readable display string
305    pub fn to_display_string(&self) -> String {
306        match self {
307            ComplexColumnType::Varchar { length } => format!("varchar({})", length),
308            ComplexColumnType::Numeric { precision, scale } => {
309                format!("numeric({},{})", precision, scale)
310            }
311            ComplexColumnType::Char { length } => format!("char({})", length),
312            ComplexColumnType::Custom { custom_type } => custom_type.to_lowercase(),
313            ComplexColumnType::Enum { name, values } => {
314                if values.is_integer() {
315                    format!("enum<{}> (integer)", name)
316                } else {
317                    format!("enum<{}>", name)
318                }
319            }
320        }
321    }
322
323    /// Get the default fill value for this type
324    /// Returns None if no sensible default exists
325    pub fn default_fill_value(&self) -> Option<&'static str> {
326        match self {
327            ComplexColumnType::Varchar { .. } | ComplexColumnType::Char { .. } => Some("''"),
328            ComplexColumnType::Numeric { .. } => Some("0"),
329            _ => None,
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use rstest::rstest;
338
339    #[rstest]
340    #[case(SimpleColumnType::SmallInt, "i16")]
341    #[case(SimpleColumnType::Integer, "i32")]
342    #[case(SimpleColumnType::BigInt, "i64")]
343    #[case(SimpleColumnType::Real, "f32")]
344    #[case(SimpleColumnType::DoublePrecision, "f64")]
345    #[case(SimpleColumnType::Text, "String")]
346    #[case(SimpleColumnType::Boolean, "bool")]
347    #[case(SimpleColumnType::Date, "Date")]
348    #[case(SimpleColumnType::Time, "Time")]
349    #[case(SimpleColumnType::Timestamp, "DateTime")]
350    #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")]
351    #[case(SimpleColumnType::Interval, "String")]
352    #[case(SimpleColumnType::Bytea, "Vec<u8>")]
353    #[case(SimpleColumnType::Uuid, "Uuid")]
354    #[case(SimpleColumnType::Json, "Json")]
355    // #[case(SimpleColumnType::Jsonb, "Json")]
356    #[case(SimpleColumnType::Inet, "String")]
357    #[case(SimpleColumnType::Cidr, "String")]
358    #[case(SimpleColumnType::Macaddr, "String")]
359    #[case(SimpleColumnType::Xml, "String")]
360    fn test_simple_column_type_to_rust_type_not_nullable(
361        #[case] column_type: SimpleColumnType,
362        #[case] expected: &str,
363    ) {
364        assert_eq!(
365            ColumnType::Simple(column_type).to_rust_type(false),
366            expected
367        );
368    }
369
370    #[rstest]
371    #[case(SimpleColumnType::SmallInt, "Option<i16>")]
372    #[case(SimpleColumnType::Integer, "Option<i32>")]
373    #[case(SimpleColumnType::BigInt, "Option<i64>")]
374    #[case(SimpleColumnType::Real, "Option<f32>")]
375    #[case(SimpleColumnType::DoublePrecision, "Option<f64>")]
376    #[case(SimpleColumnType::Text, "Option<String>")]
377    #[case(SimpleColumnType::Boolean, "Option<bool>")]
378    #[case(SimpleColumnType::Date, "Option<Date>")]
379    #[case(SimpleColumnType::Time, "Option<Time>")]
380    #[case(SimpleColumnType::Timestamp, "Option<DateTime>")]
381    #[case(SimpleColumnType::Timestamptz, "Option<DateTimeWithTimeZone>")]
382    #[case(SimpleColumnType::Interval, "Option<String>")]
383    #[case(SimpleColumnType::Bytea, "Option<Vec<u8>>")]
384    #[case(SimpleColumnType::Uuid, "Option<Uuid>")]
385    #[case(SimpleColumnType::Json, "Option<Json>")]
386    // #[case(SimpleColumnType::Jsonb, "Option<Json>")]
387    #[case(SimpleColumnType::Inet, "Option<String>")]
388    #[case(SimpleColumnType::Cidr, "Option<String>")]
389    #[case(SimpleColumnType::Macaddr, "Option<String>")]
390    #[case(SimpleColumnType::Xml, "Option<String>")]
391    fn test_simple_column_type_to_rust_type_nullable(
392        #[case] column_type: SimpleColumnType,
393        #[case] expected: &str,
394    ) {
395        assert_eq!(ColumnType::Simple(column_type).to_rust_type(true), expected);
396    }
397
398    #[rstest]
399    #[case(ComplexColumnType::Varchar { length: 255 }, false, "String")]
400    #[case(ComplexColumnType::Varchar { length: 50 }, false, "String")]
401    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, false, "Decimal")]
402    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, false, "Decimal")]
403    #[case(ComplexColumnType::Char { length: 10 }, false, "String")]
404    #[case(ComplexColumnType::Char { length: 1 }, false, "String")]
405    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, false, "String")]
406    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, false, "String")]
407    #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, false, "String")]
408    fn test_complex_column_type_to_rust_type_not_nullable(
409        #[case] column_type: ComplexColumnType,
410        #[case] nullable: bool,
411        #[case] expected: &str,
412    ) {
413        assert_eq!(
414            ColumnType::Complex(column_type).to_rust_type(nullable),
415            expected
416        );
417    }
418
419    #[rstest]
420    #[case(ComplexColumnType::Varchar { length: 255 }, "Option<String>")]
421    #[case(ComplexColumnType::Varchar { length: 50 }, "Option<String>")]
422    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "Option<Decimal>")]
423    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "Option<Decimal>")]
424    #[case(ComplexColumnType::Char { length: 10 }, "Option<String>")]
425    #[case(ComplexColumnType::Char { length: 1 }, "Option<String>")]
426    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "Option<String>")]
427    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "Option<String>")]
428    #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, "Option<String>")]
429    fn test_complex_column_type_to_rust_type_nullable(
430        #[case] column_type: ComplexColumnType,
431        #[case] expected: &str,
432    ) {
433        assert_eq!(
434            ColumnType::Complex(column_type).to_rust_type(true),
435            expected
436        );
437    }
438
439    #[rstest]
440    #[case(ComplexColumnType::Varchar { length: 255 })]
441    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 })]
442    #[case(ComplexColumnType::Char { length: 1 })]
443    #[case(ComplexColumnType::Custom { custom_type: "SERIAL".into() })]
444    #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec![]) })]
445    fn test_complex_column_type_does_not_support_auto_increment(
446        #[case] column_type: ComplexColumnType,
447    ) {
448        // Complex types never support auto_increment
449        assert!(!ColumnType::Complex(column_type).supports_auto_increment());
450    }
451
452    #[test]
453    fn test_enum_values_is_string() {
454        let string_vals = EnumValues::String(vec!["active".into()]);
455        let int_vals = EnumValues::Integer(vec![NumValue {
456            name: "Active".into(),
457            value: 1,
458        }]);
459        assert!(string_vals.is_string());
460        assert!(!int_vals.is_string());
461    }
462
463    #[test]
464    fn test_enum_values_is_integer() {
465        let string_vals = EnumValues::String(vec!["active".into()]);
466        let int_vals = EnumValues::Integer(vec![NumValue {
467            name: "Active".into(),
468            value: 1,
469        }]);
470        assert!(!string_vals.is_integer());
471        assert!(int_vals.is_integer());
472    }
473
474    #[test]
475    fn test_enum_values_variant_names_string() {
476        let vals = EnumValues::String(vec!["pending".into(), "active".into()]);
477        assert_eq!(vals.variant_names(), vec!["pending", "active"]);
478    }
479
480    #[test]
481    fn test_enum_values_variant_names_integer() {
482        let vals = EnumValues::Integer(vec![
483            NumValue {
484                name: "Low".into(),
485                value: 0,
486            },
487            NumValue {
488                name: "High".into(),
489                value: 10,
490            },
491        ]);
492        assert_eq!(vals.variant_names(), vec!["Low", "High"]);
493    }
494
495    #[test]
496    fn test_enum_values_len_and_is_empty() {
497        // String variant
498        let empty = EnumValues::String(vec![]);
499        let non_empty = EnumValues::String(vec!["a".into()]);
500        assert!(empty.is_empty());
501        assert_eq!(empty.len(), 0);
502        assert!(!non_empty.is_empty());
503        assert_eq!(non_empty.len(), 1);
504
505        // Integer variant
506        let empty_int = EnumValues::Integer(vec![]);
507        let non_empty_int = EnumValues::Integer(vec![
508            NumValue {
509                name: "A".into(),
510                value: 0,
511            },
512            NumValue {
513                name: "B".into(),
514                value: 1,
515            },
516        ]);
517        assert!(empty_int.is_empty());
518        assert_eq!(empty_int.len(), 0);
519        assert!(!non_empty_int.is_empty());
520        assert_eq!(non_empty_int.len(), 2);
521    }
522
523    #[test]
524    fn test_enum_values_to_sql_values_string() {
525        let vals = EnumValues::String(vec!["active".into(), "pending".into()]);
526        assert_eq!(vals.to_sql_values(), vec!["'active'", "'pending'"]);
527    }
528
529    #[test]
530    fn test_enum_values_to_sql_values_integer() {
531        let vals = EnumValues::Integer(vec![
532            NumValue {
533                name: "Low".into(),
534                value: 0,
535            },
536            NumValue {
537                name: "High".into(),
538                value: 10,
539            },
540        ]);
541        assert_eq!(vals.to_sql_values(), vec!["0", "10"]);
542    }
543
544    #[test]
545    fn test_enum_values_from_vec_string() {
546        let vals: EnumValues = vec!["a".to_string(), "b".to_string()].into();
547        assert!(matches!(vals, EnumValues::String(_)));
548    }
549
550    #[test]
551    fn test_enum_values_from_vec_str() {
552        let vals: EnumValues = vec!["a", "b"].into();
553        assert!(matches!(vals, EnumValues::String(_)));
554    }
555
556    #[rstest]
557    #[case(SimpleColumnType::SmallInt, true)]
558    #[case(SimpleColumnType::Integer, true)]
559    #[case(SimpleColumnType::BigInt, true)]
560    #[case(SimpleColumnType::Text, false)]
561    #[case(SimpleColumnType::Boolean, false)]
562    fn test_simple_column_type_supports_auto_increment(
563        #[case] ty: SimpleColumnType,
564        #[case] expected: bool,
565    ) {
566        assert_eq!(ty.supports_auto_increment(), expected);
567    }
568
569    #[rstest]
570    #[case(SimpleColumnType::Integer, true)]
571    #[case(SimpleColumnType::Text, false)]
572    fn test_column_type_simple_supports_auto_increment(
573        #[case] ty: SimpleColumnType,
574        #[case] expected: bool,
575    ) {
576        assert_eq!(ColumnType::Simple(ty).supports_auto_increment(), expected);
577    }
578
579    #[test]
580    fn test_requires_migration_integer_enum_values_changed() {
581        // Integer enum values changed - should NOT require migration
582        let from = ColumnType::Complex(ComplexColumnType::Enum {
583            name: "status".into(),
584            values: EnumValues::Integer(vec![
585                NumValue {
586                    name: "Pending".into(),
587                    value: 0,
588                },
589                NumValue {
590                    name: "Active".into(),
591                    value: 1,
592                },
593            ]),
594        });
595        let to = ColumnType::Complex(ComplexColumnType::Enum {
596            name: "status".into(),
597            values: EnumValues::Integer(vec![
598                NumValue {
599                    name: "Pending".into(),
600                    value: 0,
601                },
602                NumValue {
603                    name: "Active".into(),
604                    value: 1,
605                },
606                NumValue {
607                    name: "Completed".into(),
608                    value: 100,
609                },
610            ]),
611        });
612        assert!(!from.requires_migration(&to));
613    }
614
615    #[test]
616    fn test_requires_migration_integer_enum_name_changed() {
617        // Integer enum name changed - should NOT require migration (DB type is always INTEGER)
618        let from = ColumnType::Complex(ComplexColumnType::Enum {
619            name: "old_status".into(),
620            values: EnumValues::Integer(vec![NumValue {
621                name: "Pending".into(),
622                value: 0,
623            }]),
624        });
625        let to = ColumnType::Complex(ComplexColumnType::Enum {
626            name: "new_status".into(),
627            values: EnumValues::Integer(vec![NumValue {
628                name: "Pending".into(),
629                value: 0,
630            }]),
631        });
632        assert!(!from.requires_migration(&to));
633    }
634
635    #[test]
636    fn test_requires_migration_string_enum_values_changed() {
637        // String enum values changed - SHOULD require migration
638        let from = ColumnType::Complex(ComplexColumnType::Enum {
639            name: "status".into(),
640            values: EnumValues::String(vec!["pending".into(), "active".into()]),
641        });
642        let to = ColumnType::Complex(ComplexColumnType::Enum {
643            name: "status".into(),
644            values: EnumValues::String(vec!["pending".into(), "active".into(), "completed".into()]),
645        });
646        assert!(from.requires_migration(&to));
647    }
648
649    #[test]
650    fn test_requires_migration_simple_types() {
651        let int = ColumnType::Simple(SimpleColumnType::Integer);
652        let text = ColumnType::Simple(SimpleColumnType::Text);
653        assert!(int.requires_migration(&text));
654        assert!(!int.requires_migration(&int));
655    }
656
657    #[test]
658    fn test_requires_migration_mixed_enum_types() {
659        // String enum to integer enum - SHOULD require migration
660        let string_enum = ColumnType::Complex(ComplexColumnType::Enum {
661            name: "status".into(),
662            values: EnumValues::String(vec!["pending".into()]),
663        });
664        let int_enum = ColumnType::Complex(ComplexColumnType::Enum {
665            name: "status".into(),
666            values: EnumValues::Integer(vec![NumValue {
667                name: "Pending".into(),
668                value: 0,
669            }]),
670        });
671        assert!(string_enum.requires_migration(&int_enum));
672    }
673
674    // Tests for to_display_string
675    #[rstest]
676    #[case(SimpleColumnType::SmallInt, "smallint")]
677    #[case(SimpleColumnType::Integer, "integer")]
678    #[case(SimpleColumnType::BigInt, "bigint")]
679    #[case(SimpleColumnType::Real, "real")]
680    #[case(SimpleColumnType::DoublePrecision, "double precision")]
681    #[case(SimpleColumnType::Text, "text")]
682    #[case(SimpleColumnType::Boolean, "boolean")]
683    #[case(SimpleColumnType::Date, "date")]
684    #[case(SimpleColumnType::Time, "time")]
685    #[case(SimpleColumnType::Timestamp, "timestamp")]
686    #[case(SimpleColumnType::Timestamptz, "timestamptz")]
687    #[case(SimpleColumnType::Interval, "interval")]
688    #[case(SimpleColumnType::Bytea, "bytea")]
689    #[case(SimpleColumnType::Uuid, "uuid")]
690    #[case(SimpleColumnType::Json, "json")]
691    #[case(SimpleColumnType::Inet, "inet")]
692    #[case(SimpleColumnType::Cidr, "cidr")]
693    #[case(SimpleColumnType::Macaddr, "macaddr")]
694    #[case(SimpleColumnType::Xml, "xml")]
695    fn test_simple_column_type_to_display_string(
696        #[case] column_type: SimpleColumnType,
697        #[case] expected: &str,
698    ) {
699        assert_eq!(column_type.to_display_string(), expected);
700    }
701
702    #[test]
703    fn test_complex_column_type_to_display_string_varchar() {
704        let ty = ComplexColumnType::Varchar { length: 255 };
705        assert_eq!(ty.to_display_string(), "varchar(255)");
706    }
707
708    #[test]
709    fn test_complex_column_type_to_display_string_numeric() {
710        let ty = ComplexColumnType::Numeric {
711            precision: 10,
712            scale: 2,
713        };
714        assert_eq!(ty.to_display_string(), "numeric(10,2)");
715    }
716
717    #[test]
718    fn test_complex_column_type_to_display_string_char() {
719        let ty = ComplexColumnType::Char { length: 5 };
720        assert_eq!(ty.to_display_string(), "char(5)");
721    }
722
723    #[test]
724    fn test_complex_column_type_to_display_string_custom() {
725        let ty = ComplexColumnType::Custom {
726            custom_type: "TSVECTOR".into(),
727        };
728        assert_eq!(ty.to_display_string(), "tsvector");
729    }
730
731    #[test]
732    fn test_complex_column_type_to_display_string_string_enum() {
733        let ty = ComplexColumnType::Enum {
734            name: "user_status".into(),
735            values: EnumValues::String(vec!["active".into(), "inactive".into()]),
736        };
737        assert_eq!(ty.to_display_string(), "enum<user_status>");
738    }
739
740    #[test]
741    fn test_complex_column_type_to_display_string_integer_enum() {
742        let ty = ComplexColumnType::Enum {
743            name: "priority".into(),
744            values: EnumValues::Integer(vec![
745                NumValue {
746                    name: "Low".into(),
747                    value: 0,
748                },
749                NumValue {
750                    name: "High".into(),
751                    value: 10,
752                },
753            ]),
754        };
755        assert_eq!(ty.to_display_string(), "enum<priority> (integer)");
756    }
757
758    #[test]
759    fn test_column_type_to_display_string_simple() {
760        let ty = ColumnType::Simple(SimpleColumnType::Integer);
761        assert_eq!(ty.to_display_string(), "integer");
762    }
763
764    #[test]
765    fn test_column_type_to_display_string_complex() {
766        let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 100 });
767        assert_eq!(ty.to_display_string(), "varchar(100)");
768    }
769
770    // Tests for default_fill_value
771    #[rstest]
772    #[case(SimpleColumnType::SmallInt, Some("0"))]
773    #[case(SimpleColumnType::Integer, Some("0"))]
774    #[case(SimpleColumnType::BigInt, Some("0"))]
775    #[case(SimpleColumnType::Real, Some("0.0"))]
776    #[case(SimpleColumnType::DoublePrecision, Some("0.0"))]
777    #[case(SimpleColumnType::Boolean, Some("false"))]
778    #[case(SimpleColumnType::Text, Some("''"))]
779    #[case(SimpleColumnType::Date, None)]
780    #[case(SimpleColumnType::Time, None)]
781    #[case(SimpleColumnType::Timestamp, None)]
782    #[case(SimpleColumnType::Uuid, None)]
783    fn test_simple_column_type_default_fill_value(
784        #[case] column_type: SimpleColumnType,
785        #[case] expected: Option<&str>,
786    ) {
787        assert_eq!(column_type.default_fill_value(), expected);
788    }
789
790    #[test]
791    fn test_complex_column_type_default_fill_value_varchar() {
792        let ty = ComplexColumnType::Varchar { length: 255 };
793        assert_eq!(ty.default_fill_value(), Some("''"));
794    }
795
796    #[test]
797    fn test_complex_column_type_default_fill_value_char() {
798        let ty = ComplexColumnType::Char { length: 1 };
799        assert_eq!(ty.default_fill_value(), Some("''"));
800    }
801
802    #[test]
803    fn test_complex_column_type_default_fill_value_numeric() {
804        let ty = ComplexColumnType::Numeric {
805            precision: 10,
806            scale: 2,
807        };
808        assert_eq!(ty.default_fill_value(), Some("0"));
809    }
810
811    #[test]
812    fn test_complex_column_type_default_fill_value_custom() {
813        let ty = ComplexColumnType::Custom {
814            custom_type: "MONEY".into(),
815        };
816        assert_eq!(ty.default_fill_value(), None);
817    }
818
819    #[test]
820    fn test_complex_column_type_default_fill_value_enum() {
821        let ty = ComplexColumnType::Enum {
822            name: "status".into(),
823            values: EnumValues::String(vec!["active".into()]),
824        };
825        assert_eq!(ty.default_fill_value(), None);
826    }
827
828    #[test]
829    fn test_column_type_default_fill_value_simple() {
830        let ty = ColumnType::Simple(SimpleColumnType::Integer);
831        assert_eq!(ty.default_fill_value(), Some("0"));
832    }
833
834    #[test]
835    fn test_column_type_default_fill_value_complex() {
836        let ty = ColumnType::Complex(ComplexColumnType::Varchar { length: 100 });
837        assert_eq!(ty.default_fill_value(), Some("''"));
838    }
839}