Skip to main content

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