vespertide_core/schema/
column.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use crate::schema::{
5    foreign_key::ForeignKeySyntax, names::ColumnName, primary_key::PrimaryKeySyntax,
6    str_or_bool::StrOrBoolOrArray,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
10#[serde(rename_all = "snake_case")]
11pub struct ColumnDef {
12    pub name: ColumnName,
13    pub r#type: ColumnType,
14    pub nullable: bool,
15    pub default: Option<String>,
16    pub comment: Option<String>,
17    pub primary_key: Option<PrimaryKeySyntax>,
18    pub unique: Option<StrOrBoolOrArray>,
19    pub index: Option<StrOrBoolOrArray>,
20    pub foreign_key: Option<ForeignKeySyntax>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "snake_case", untagged)]
25pub enum ColumnType {
26    Simple(SimpleColumnType),
27    Complex(ComplexColumnType),
28}
29
30impl ColumnType {
31    /// Convert column type to SQL type string
32    pub fn to_sql(&self) -> String {
33        match self {
34            ColumnType::Simple(ty) => match ty {
35                SimpleColumnType::SmallInt => "SMALLINT".into(),
36                SimpleColumnType::Integer => "INTEGER".into(),
37                SimpleColumnType::BigInt => "BIGINT".into(),
38                SimpleColumnType::Real => "REAL".into(),
39                SimpleColumnType::DoublePrecision => "DOUBLE PRECISION".into(),
40                SimpleColumnType::Text => "TEXT".into(),
41                SimpleColumnType::Boolean => "BOOLEAN".into(),
42                SimpleColumnType::Date => "DATE".into(),
43                SimpleColumnType::Time => "TIME".into(),
44                SimpleColumnType::Timestamp => "TIMESTAMP".into(),
45                SimpleColumnType::Timestamptz => "TIMESTAMPTZ".into(),
46                SimpleColumnType::Interval => "INTERVAL".into(),
47                SimpleColumnType::Bytea => "BYTEA".into(),
48                SimpleColumnType::Uuid => "UUID".into(),
49                SimpleColumnType::Json => "JSON".into(),
50                SimpleColumnType::Jsonb => "JSONB".into(),
51                SimpleColumnType::Inet => "INET".into(),
52                SimpleColumnType::Cidr => "CIDR".into(),
53                SimpleColumnType::Macaddr => "MACADDR".into(),
54                SimpleColumnType::Xml => "XML".into(),
55            },
56            ColumnType::Complex(ty) => match ty {
57                ComplexColumnType::Varchar { length } => format!("VARCHAR({})", length),
58                ComplexColumnType::Numeric { precision, scale } => {
59                    format!("NUMERIC({}, {})", precision, scale)
60                }
61                ComplexColumnType::Char { length } => format!("CHAR({})", length),
62                ComplexColumnType::Custom { custom_type } => custom_type.clone(),
63            },
64        }
65    }
66
67    /// Convert column type to Rust type string (for SeaORM entity generation)
68    pub fn to_rust_type(&self, nullable: bool) -> String {
69        let base = match self {
70            ColumnType::Simple(ty) => match ty {
71                SimpleColumnType::SmallInt => "i16".to_string(),
72                SimpleColumnType::Integer => "i32".to_string(),
73                SimpleColumnType::BigInt => "i64".to_string(),
74                SimpleColumnType::Real => "f32".to_string(),
75                SimpleColumnType::DoublePrecision => "f64".to_string(),
76                SimpleColumnType::Text => "String".to_string(),
77                SimpleColumnType::Boolean => "bool".to_string(),
78                SimpleColumnType::Date => "Date".to_string(),
79                SimpleColumnType::Time => "Time".to_string(),
80                SimpleColumnType::Timestamp => "DateTime".to_string(),
81                SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(),
82                SimpleColumnType::Interval => "String".to_string(),
83                SimpleColumnType::Bytea => "Vec<u8>".to_string(),
84                SimpleColumnType::Uuid => "Uuid".to_string(),
85                SimpleColumnType::Json | SimpleColumnType::Jsonb => "Json".to_string(),
86                SimpleColumnType::Inet | SimpleColumnType::Cidr => "String".to_string(),
87                SimpleColumnType::Macaddr => "String".to_string(),
88                SimpleColumnType::Xml => "String".to_string(),
89            },
90            ColumnType::Complex(ty) => match ty {
91                ComplexColumnType::Varchar { .. } => "String".to_string(),
92                ComplexColumnType::Numeric { .. } => "Decimal".to_string(),
93                ComplexColumnType::Char { .. } => "String".to_string(),
94                ComplexColumnType::Custom { .. } => "String".to_string(), // Default for custom types
95            },
96        };
97
98        if nullable {
99            format!("Option<{}>", base)
100        } else {
101            base
102        }
103    }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
107#[serde(rename_all = "snake_case")]
108pub enum SimpleColumnType {
109    SmallInt,
110    Integer,
111    BigInt,
112    Real,
113    DoublePrecision,
114
115    // Text types
116    Text,
117
118    // Boolean type
119    Boolean,
120
121    // Date/Time types
122    Date,
123    Time,
124    Timestamp,
125    Timestamptz,
126    Interval,
127
128    // Binary type
129    Bytea,
130
131    // UUID type
132    Uuid,
133
134    // JSON types
135    Json,
136    Jsonb,
137
138    // Network types
139    Inet,
140    Cidr,
141    Macaddr,
142
143    // XML type
144    Xml,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
148#[serde(rename_all = "snake_case", tag = "kind")]
149pub enum ComplexColumnType {
150    Varchar { length: u32 },
151    Numeric { precision: u32, scale: u32 },
152    Char { length: u32 },
153    Custom { custom_type: String },
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use rstest::rstest;
160
161    #[rstest]
162    #[case(SimpleColumnType::SmallInt, "SMALLINT")]
163    #[case(SimpleColumnType::Integer, "INTEGER")]
164    #[case(SimpleColumnType::BigInt, "BIGINT")]
165    #[case(SimpleColumnType::Real, "REAL")]
166    #[case(SimpleColumnType::DoublePrecision, "DOUBLE PRECISION")]
167    #[case(SimpleColumnType::Text, "TEXT")]
168    #[case(SimpleColumnType::Boolean, "BOOLEAN")]
169    #[case(SimpleColumnType::Date, "DATE")]
170    #[case(SimpleColumnType::Time, "TIME")]
171    #[case(SimpleColumnType::Timestamp, "TIMESTAMP")]
172    #[case(SimpleColumnType::Timestamptz, "TIMESTAMPTZ")]
173    #[case(SimpleColumnType::Interval, "INTERVAL")]
174    #[case(SimpleColumnType::Bytea, "BYTEA")]
175    #[case(SimpleColumnType::Uuid, "UUID")]
176    #[case(SimpleColumnType::Json, "JSON")]
177    #[case(SimpleColumnType::Jsonb, "JSONB")]
178    #[case(SimpleColumnType::Inet, "INET")]
179    #[case(SimpleColumnType::Cidr, "CIDR")]
180    #[case(SimpleColumnType::Macaddr, "MACADDR")]
181    #[case(SimpleColumnType::Xml, "XML")]
182    fn test_simple_column_type_to_sql(
183        #[case] column_type: SimpleColumnType,
184        #[case] expected: &str,
185    ) {
186        assert_eq!(ColumnType::Simple(column_type).to_sql(), expected);
187    }
188
189    #[rstest]
190    #[case(ComplexColumnType::Varchar { length: 255 }, "VARCHAR(255)")]
191    #[case(ComplexColumnType::Varchar { length: 50 }, "VARCHAR(50)")]
192    #[case(ComplexColumnType::Varchar { length: 1 }, "VARCHAR(1)")]
193    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "NUMERIC(10, 2)")]
194    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "NUMERIC(5, 0)")]
195    #[case(ComplexColumnType::Numeric { precision: 18, scale: 4 }, "NUMERIC(18, 4)")]
196    #[case(ComplexColumnType::Char { length: 10 }, "CHAR(10)")]
197    #[case(ComplexColumnType::Char { length: 1 }, "CHAR(1)")]
198    #[case(ComplexColumnType::Char { length: 255 }, "CHAR(255)")]
199    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "MONEY")]
200    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "JSONB")]
201    #[case(ComplexColumnType::Custom { custom_type: "CUSTOM_TYPE".into() }, "CUSTOM_TYPE")]
202    fn test_complex_column_type_to_sql(
203        #[case] column_type: ComplexColumnType,
204        #[case] expected: &str,
205    ) {
206        assert_eq!(ColumnType::Complex(column_type).to_sql(), expected);
207    }
208
209    #[rstest]
210    #[case(SimpleColumnType::SmallInt, "i16")]
211    #[case(SimpleColumnType::Integer, "i32")]
212    #[case(SimpleColumnType::BigInt, "i64")]
213    #[case(SimpleColumnType::Real, "f32")]
214    #[case(SimpleColumnType::DoublePrecision, "f64")]
215    #[case(SimpleColumnType::Text, "String")]
216    #[case(SimpleColumnType::Boolean, "bool")]
217    #[case(SimpleColumnType::Date, "Date")]
218    #[case(SimpleColumnType::Time, "Time")]
219    #[case(SimpleColumnType::Timestamp, "DateTime")]
220    #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")]
221    #[case(SimpleColumnType::Interval, "String")]
222    #[case(SimpleColumnType::Bytea, "Vec<u8>")]
223    #[case(SimpleColumnType::Uuid, "Uuid")]
224    #[case(SimpleColumnType::Json, "Json")]
225    #[case(SimpleColumnType::Jsonb, "Json")]
226    #[case(SimpleColumnType::Inet, "String")]
227    #[case(SimpleColumnType::Cidr, "String")]
228    #[case(SimpleColumnType::Macaddr, "String")]
229    #[case(SimpleColumnType::Xml, "String")]
230    fn test_simple_column_type_to_rust_type_not_nullable(
231        #[case] column_type: SimpleColumnType,
232        #[case] expected: &str,
233    ) {
234        assert_eq!(
235            ColumnType::Simple(column_type).to_rust_type(false),
236            expected
237        );
238    }
239
240    #[rstest]
241    #[case(SimpleColumnType::SmallInt, "Option<i16>")]
242    #[case(SimpleColumnType::Integer, "Option<i32>")]
243    #[case(SimpleColumnType::BigInt, "Option<i64>")]
244    #[case(SimpleColumnType::Real, "Option<f32>")]
245    #[case(SimpleColumnType::DoublePrecision, "Option<f64>")]
246    #[case(SimpleColumnType::Text, "Option<String>")]
247    #[case(SimpleColumnType::Boolean, "Option<bool>")]
248    #[case(SimpleColumnType::Date, "Option<Date>")]
249    #[case(SimpleColumnType::Time, "Option<Time>")]
250    #[case(SimpleColumnType::Timestamp, "Option<DateTime>")]
251    #[case(SimpleColumnType::Timestamptz, "Option<DateTimeWithTimeZone>")]
252    #[case(SimpleColumnType::Interval, "Option<String>")]
253    #[case(SimpleColumnType::Bytea, "Option<Vec<u8>>")]
254    #[case(SimpleColumnType::Uuid, "Option<Uuid>")]
255    #[case(SimpleColumnType::Json, "Option<Json>")]
256    #[case(SimpleColumnType::Jsonb, "Option<Json>")]
257    #[case(SimpleColumnType::Inet, "Option<String>")]
258    #[case(SimpleColumnType::Cidr, "Option<String>")]
259    #[case(SimpleColumnType::Macaddr, "Option<String>")]
260    #[case(SimpleColumnType::Xml, "Option<String>")]
261    fn test_simple_column_type_to_rust_type_nullable(
262        #[case] column_type: SimpleColumnType,
263        #[case] expected: &str,
264    ) {
265        assert_eq!(ColumnType::Simple(column_type).to_rust_type(true), expected);
266    }
267
268    #[rstest]
269    #[case(ComplexColumnType::Varchar { length: 255 }, false, "String")]
270    #[case(ComplexColumnType::Varchar { length: 50 }, false, "String")]
271    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, false, "Decimal")]
272    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, false, "Decimal")]
273    #[case(ComplexColumnType::Char { length: 10 }, false, "String")]
274    #[case(ComplexColumnType::Char { length: 1 }, false, "String")]
275    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, false, "String")]
276    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, false, "String")]
277    fn test_complex_column_type_to_rust_type_not_nullable(
278        #[case] column_type: ComplexColumnType,
279        #[case] nullable: bool,
280        #[case] expected: &str,
281    ) {
282        assert_eq!(
283            ColumnType::Complex(column_type).to_rust_type(nullable),
284            expected
285        );
286    }
287
288    #[rstest]
289    #[case(ComplexColumnType::Varchar { length: 255 }, "Option<String>")]
290    #[case(ComplexColumnType::Varchar { length: 50 }, "Option<String>")]
291    #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "Option<Decimal>")]
292    #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "Option<Decimal>")]
293    #[case(ComplexColumnType::Char { length: 10 }, "Option<String>")]
294    #[case(ComplexColumnType::Char { length: 1 }, "Option<String>")]
295    #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "Option<String>")]
296    #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "Option<String>")]
297    fn test_complex_column_type_to_rust_type_nullable(
298        #[case] column_type: ComplexColumnType,
299        #[case] expected: &str,
300    ) {
301        assert_eq!(
302            ColumnType::Complex(column_type).to_rust_type(true),
303            expected
304        );
305    }
306}