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 pub fn supports_auto_increment(&self) -> bool {
33 match self {
34 ColumnType::Simple(ty) => ty.supports_auto_increment(),
35 ColumnType::Complex(_) => false,
36 }
37 }
38
39 pub fn requires_migration(&self, other: &ColumnType) -> bool {
43 match (self, other) {
44 (
45 ColumnType::Complex(ComplexColumnType::Enum {
46 values: values1, ..
47 }),
48 ColumnType::Complex(ComplexColumnType::Enum {
49 values: values2, ..
50 }),
51 ) => {
52 if values1.is_integer() && values2.is_integer() {
54 false
55 } else {
56 self != other
58 }
59 }
60 _ => self != other,
61 }
62 }
63
64 pub fn to_rust_type(&self, nullable: bool) -> String {
66 let base = match self {
67 ColumnType::Simple(ty) => match ty {
68 SimpleColumnType::SmallInt => "i16".to_string(),
69 SimpleColumnType::Integer => "i32".to_string(),
70 SimpleColumnType::BigInt => "i64".to_string(),
71 SimpleColumnType::Real => "f32".to_string(),
72 SimpleColumnType::DoublePrecision => "f64".to_string(),
73 SimpleColumnType::Text => "String".to_string(),
74 SimpleColumnType::Boolean => "bool".to_string(),
75 SimpleColumnType::Date => "Date".to_string(),
76 SimpleColumnType::Time => "Time".to_string(),
77 SimpleColumnType::Timestamp => "DateTime".to_string(),
78 SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(),
79 SimpleColumnType::Interval => "String".to_string(),
80 SimpleColumnType::Bytea => "Vec<u8>".to_string(),
81 SimpleColumnType::Uuid => "Uuid".to_string(),
82 SimpleColumnType::Json | SimpleColumnType::Jsonb => "Json".to_string(),
83 SimpleColumnType::Inet | SimpleColumnType::Cidr => "String".to_string(),
84 SimpleColumnType::Macaddr => "String".to_string(),
85 SimpleColumnType::Xml => "String".to_string(),
86 },
87 ColumnType::Complex(ty) => match ty {
88 ComplexColumnType::Varchar { .. } => "String".to_string(),
89 ComplexColumnType::Numeric { .. } => "Decimal".to_string(),
90 ComplexColumnType::Char { .. } => "String".to_string(),
91 ComplexColumnType::Custom { .. } => "String".to_string(), ComplexColumnType::Enum { .. } => "String".to_string(),
93 },
94 };
95
96 if nullable {
97 format!("Option<{}>", base)
98 } else {
99 base
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105#[serde(rename_all = "snake_case")]
106pub enum SimpleColumnType {
107 SmallInt,
108 Integer,
109 BigInt,
110 Real,
111 DoublePrecision,
112
113 Text,
115
116 Boolean,
118
119 Date,
121 Time,
122 Timestamp,
123 Timestamptz,
124 Interval,
125
126 Bytea,
128
129 Uuid,
131
132 Json,
134 Jsonb,
135
136 Inet,
138 Cidr,
139 Macaddr,
140
141 Xml,
143}
144
145impl SimpleColumnType {
146 pub fn supports_auto_increment(&self) -> bool {
148 matches!(
149 self,
150 SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt
151 )
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
157pub struct NumValue {
158 pub name: String,
159 pub value: i32,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
164#[serde(untagged)]
165pub enum EnumValues {
166 String(Vec<String>),
167 Integer(Vec<NumValue>),
168}
169
170impl EnumValues {
171 pub fn is_string(&self) -> bool {
173 matches!(self, EnumValues::String(_))
174 }
175
176 pub fn is_integer(&self) -> bool {
178 matches!(self, EnumValues::Integer(_))
179 }
180
181 pub fn variant_names(&self) -> Vec<&str> {
183 match self {
184 EnumValues::String(values) => values.iter().map(|s| s.as_str()).collect(),
185 EnumValues::Integer(values) => values.iter().map(|v| v.name.as_str()).collect(),
186 }
187 }
188
189 pub fn len(&self) -> usize {
191 match self {
192 EnumValues::String(values) => values.len(),
193 EnumValues::Integer(values) => values.len(),
194 }
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.len() == 0
200 }
201
202 pub fn to_sql_values(&self) -> Vec<String> {
205 match self {
206 EnumValues::String(values) => values
207 .iter()
208 .map(|s| format!("'{}'", s.replace('\'', "''")))
209 .collect(),
210 EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(),
211 }
212 }
213}
214
215impl From<Vec<String>> for EnumValues {
216 fn from(values: Vec<String>) -> Self {
217 EnumValues::String(values)
218 }
219}
220
221impl From<Vec<&str>> for EnumValues {
222 fn from(values: Vec<&str>) -> Self {
223 EnumValues::String(values.into_iter().map(|s| s.to_string()).collect())
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
228#[serde(rename_all = "snake_case", tag = "kind")]
229pub enum ComplexColumnType {
230 Varchar { length: u32 },
231 Numeric { precision: u32, scale: u32 },
232 Char { length: u32 },
233 Custom { custom_type: String },
234 Enum { name: String, values: EnumValues },
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use rstest::rstest;
241
242 #[rstest]
243 #[case(SimpleColumnType::SmallInt, "i16")]
244 #[case(SimpleColumnType::Integer, "i32")]
245 #[case(SimpleColumnType::BigInt, "i64")]
246 #[case(SimpleColumnType::Real, "f32")]
247 #[case(SimpleColumnType::DoublePrecision, "f64")]
248 #[case(SimpleColumnType::Text, "String")]
249 #[case(SimpleColumnType::Boolean, "bool")]
250 #[case(SimpleColumnType::Date, "Date")]
251 #[case(SimpleColumnType::Time, "Time")]
252 #[case(SimpleColumnType::Timestamp, "DateTime")]
253 #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")]
254 #[case(SimpleColumnType::Interval, "String")]
255 #[case(SimpleColumnType::Bytea, "Vec<u8>")]
256 #[case(SimpleColumnType::Uuid, "Uuid")]
257 #[case(SimpleColumnType::Json, "Json")]
258 #[case(SimpleColumnType::Jsonb, "Json")]
259 #[case(SimpleColumnType::Inet, "String")]
260 #[case(SimpleColumnType::Cidr, "String")]
261 #[case(SimpleColumnType::Macaddr, "String")]
262 #[case(SimpleColumnType::Xml, "String")]
263 fn test_simple_column_type_to_rust_type_not_nullable(
264 #[case] column_type: SimpleColumnType,
265 #[case] expected: &str,
266 ) {
267 assert_eq!(
268 ColumnType::Simple(column_type).to_rust_type(false),
269 expected
270 );
271 }
272
273 #[rstest]
274 #[case(SimpleColumnType::SmallInt, "Option<i16>")]
275 #[case(SimpleColumnType::Integer, "Option<i32>")]
276 #[case(SimpleColumnType::BigInt, "Option<i64>")]
277 #[case(SimpleColumnType::Real, "Option<f32>")]
278 #[case(SimpleColumnType::DoublePrecision, "Option<f64>")]
279 #[case(SimpleColumnType::Text, "Option<String>")]
280 #[case(SimpleColumnType::Boolean, "Option<bool>")]
281 #[case(SimpleColumnType::Date, "Option<Date>")]
282 #[case(SimpleColumnType::Time, "Option<Time>")]
283 #[case(SimpleColumnType::Timestamp, "Option<DateTime>")]
284 #[case(SimpleColumnType::Timestamptz, "Option<DateTimeWithTimeZone>")]
285 #[case(SimpleColumnType::Interval, "Option<String>")]
286 #[case(SimpleColumnType::Bytea, "Option<Vec<u8>>")]
287 #[case(SimpleColumnType::Uuid, "Option<Uuid>")]
288 #[case(SimpleColumnType::Json, "Option<Json>")]
289 #[case(SimpleColumnType::Jsonb, "Option<Json>")]
290 #[case(SimpleColumnType::Inet, "Option<String>")]
291 #[case(SimpleColumnType::Cidr, "Option<String>")]
292 #[case(SimpleColumnType::Macaddr, "Option<String>")]
293 #[case(SimpleColumnType::Xml, "Option<String>")]
294 fn test_simple_column_type_to_rust_type_nullable(
295 #[case] column_type: SimpleColumnType,
296 #[case] expected: &str,
297 ) {
298 assert_eq!(ColumnType::Simple(column_type).to_rust_type(true), expected);
299 }
300
301 #[rstest]
302 #[case(ComplexColumnType::Varchar { length: 255 }, false, "String")]
303 #[case(ComplexColumnType::Varchar { length: 50 }, false, "String")]
304 #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, false, "Decimal")]
305 #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, false, "Decimal")]
306 #[case(ComplexColumnType::Char { length: 10 }, false, "String")]
307 #[case(ComplexColumnType::Char { length: 1 }, false, "String")]
308 #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, false, "String")]
309 #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, false, "String")]
310 #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, false, "String")]
311 fn test_complex_column_type_to_rust_type_not_nullable(
312 #[case] column_type: ComplexColumnType,
313 #[case] nullable: bool,
314 #[case] expected: &str,
315 ) {
316 assert_eq!(
317 ColumnType::Complex(column_type).to_rust_type(nullable),
318 expected
319 );
320 }
321
322 #[rstest]
323 #[case(ComplexColumnType::Varchar { length: 255 }, "Option<String>")]
324 #[case(ComplexColumnType::Varchar { length: 50 }, "Option<String>")]
325 #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "Option<Decimal>")]
326 #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "Option<Decimal>")]
327 #[case(ComplexColumnType::Char { length: 10 }, "Option<String>")]
328 #[case(ComplexColumnType::Char { length: 1 }, "Option<String>")]
329 #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "Option<String>")]
330 #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "Option<String>")]
331 #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec!["active".into(), "inactive".into()]) }, "Option<String>")]
332 fn test_complex_column_type_to_rust_type_nullable(
333 #[case] column_type: ComplexColumnType,
334 #[case] expected: &str,
335 ) {
336 assert_eq!(
337 ColumnType::Complex(column_type).to_rust_type(true),
338 expected
339 );
340 }
341
342 #[rstest]
343 #[case(ComplexColumnType::Varchar { length: 255 })]
344 #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 })]
345 #[case(ComplexColumnType::Char { length: 1 })]
346 #[case(ComplexColumnType::Custom { custom_type: "SERIAL".into() })]
347 #[case(ComplexColumnType::Enum { name: "status".into(), values: EnumValues::String(vec![]) })]
348 fn test_complex_column_type_does_not_support_auto_increment(
349 #[case] column_type: ComplexColumnType,
350 ) {
351 assert!(!ColumnType::Complex(column_type).supports_auto_increment());
353 }
354
355 #[test]
356 fn test_enum_values_is_string() {
357 let string_vals = EnumValues::String(vec!["active".into()]);
358 let int_vals = EnumValues::Integer(vec![NumValue {
359 name: "Active".into(),
360 value: 1,
361 }]);
362 assert!(string_vals.is_string());
363 assert!(!int_vals.is_string());
364 }
365
366 #[test]
367 fn test_enum_values_is_integer() {
368 let string_vals = EnumValues::String(vec!["active".into()]);
369 let int_vals = EnumValues::Integer(vec![NumValue {
370 name: "Active".into(),
371 value: 1,
372 }]);
373 assert!(!string_vals.is_integer());
374 assert!(int_vals.is_integer());
375 }
376
377 #[test]
378 fn test_enum_values_variant_names_string() {
379 let vals = EnumValues::String(vec!["pending".into(), "active".into()]);
380 assert_eq!(vals.variant_names(), vec!["pending", "active"]);
381 }
382
383 #[test]
384 fn test_enum_values_variant_names_integer() {
385 let vals = EnumValues::Integer(vec![
386 NumValue {
387 name: "Low".into(),
388 value: 0,
389 },
390 NumValue {
391 name: "High".into(),
392 value: 10,
393 },
394 ]);
395 assert_eq!(vals.variant_names(), vec!["Low", "High"]);
396 }
397
398 #[test]
399 fn test_enum_values_len_and_is_empty() {
400 let empty = EnumValues::String(vec![]);
402 let non_empty = EnumValues::String(vec!["a".into()]);
403 assert!(empty.is_empty());
404 assert_eq!(empty.len(), 0);
405 assert!(!non_empty.is_empty());
406 assert_eq!(non_empty.len(), 1);
407
408 let empty_int = EnumValues::Integer(vec![]);
410 let non_empty_int = EnumValues::Integer(vec![
411 NumValue {
412 name: "A".into(),
413 value: 0,
414 },
415 NumValue {
416 name: "B".into(),
417 value: 1,
418 },
419 ]);
420 assert!(empty_int.is_empty());
421 assert_eq!(empty_int.len(), 0);
422 assert!(!non_empty_int.is_empty());
423 assert_eq!(non_empty_int.len(), 2);
424 }
425
426 #[test]
427 fn test_enum_values_to_sql_values_string() {
428 let vals = EnumValues::String(vec!["active".into(), "pending".into()]);
429 assert_eq!(vals.to_sql_values(), vec!["'active'", "'pending'"]);
430 }
431
432 #[test]
433 fn test_enum_values_to_sql_values_integer() {
434 let vals = EnumValues::Integer(vec![
435 NumValue {
436 name: "Low".into(),
437 value: 0,
438 },
439 NumValue {
440 name: "High".into(),
441 value: 10,
442 },
443 ]);
444 assert_eq!(vals.to_sql_values(), vec!["0", "10"]);
445 }
446
447 #[test]
448 fn test_enum_values_from_vec_string() {
449 let vals: EnumValues = vec!["a".to_string(), "b".to_string()].into();
450 assert!(matches!(vals, EnumValues::String(_)));
451 }
452
453 #[test]
454 fn test_enum_values_from_vec_str() {
455 let vals: EnumValues = vec!["a", "b"].into();
456 assert!(matches!(vals, EnumValues::String(_)));
457 }
458
459 #[rstest]
460 #[case(SimpleColumnType::SmallInt, true)]
461 #[case(SimpleColumnType::Integer, true)]
462 #[case(SimpleColumnType::BigInt, true)]
463 #[case(SimpleColumnType::Text, false)]
464 #[case(SimpleColumnType::Boolean, false)]
465 fn test_simple_column_type_supports_auto_increment(
466 #[case] ty: SimpleColumnType,
467 #[case] expected: bool,
468 ) {
469 assert_eq!(ty.supports_auto_increment(), expected);
470 }
471
472 #[rstest]
473 #[case(SimpleColumnType::Integer, true)]
474 #[case(SimpleColumnType::Text, false)]
475 fn test_column_type_simple_supports_auto_increment(
476 #[case] ty: SimpleColumnType,
477 #[case] expected: bool,
478 ) {
479 assert_eq!(ColumnType::Simple(ty).supports_auto_increment(), expected);
480 }
481
482 #[test]
483 fn test_requires_migration_integer_enum_values_changed() {
484 let from = ColumnType::Complex(ComplexColumnType::Enum {
486 name: "status".into(),
487 values: EnumValues::Integer(vec![
488 NumValue {
489 name: "Pending".into(),
490 value: 0,
491 },
492 NumValue {
493 name: "Active".into(),
494 value: 1,
495 },
496 ]),
497 });
498 let to = ColumnType::Complex(ComplexColumnType::Enum {
499 name: "status".into(),
500 values: EnumValues::Integer(vec![
501 NumValue {
502 name: "Pending".into(),
503 value: 0,
504 },
505 NumValue {
506 name: "Active".into(),
507 value: 1,
508 },
509 NumValue {
510 name: "Completed".into(),
511 value: 100,
512 },
513 ]),
514 });
515 assert!(!from.requires_migration(&to));
516 }
517
518 #[test]
519 fn test_requires_migration_integer_enum_name_changed() {
520 let from = ColumnType::Complex(ComplexColumnType::Enum {
522 name: "old_status".into(),
523 values: EnumValues::Integer(vec![NumValue {
524 name: "Pending".into(),
525 value: 0,
526 }]),
527 });
528 let to = ColumnType::Complex(ComplexColumnType::Enum {
529 name: "new_status".into(),
530 values: EnumValues::Integer(vec![NumValue {
531 name: "Pending".into(),
532 value: 0,
533 }]),
534 });
535 assert!(!from.requires_migration(&to));
536 }
537
538 #[test]
539 fn test_requires_migration_string_enum_values_changed() {
540 let from = ColumnType::Complex(ComplexColumnType::Enum {
542 name: "status".into(),
543 values: EnumValues::String(vec!["pending".into(), "active".into()]),
544 });
545 let to = ColumnType::Complex(ComplexColumnType::Enum {
546 name: "status".into(),
547 values: EnumValues::String(vec!["pending".into(), "active".into(), "completed".into()]),
548 });
549 assert!(from.requires_migration(&to));
550 }
551
552 #[test]
553 fn test_requires_migration_simple_types() {
554 let int = ColumnType::Simple(SimpleColumnType::Integer);
555 let text = ColumnType::Simple(SimpleColumnType::Text);
556 assert!(int.requires_migration(&text));
557 assert!(!int.requires_migration(&int));
558 }
559
560 #[test]
561 fn test_requires_migration_mixed_enum_types() {
562 let string_enum = ColumnType::Complex(ComplexColumnType::Enum {
564 name: "status".into(),
565 values: EnumValues::String(vec!["pending".into()]),
566 });
567 let int_enum = ColumnType::Complex(ComplexColumnType::Enum {
568 name: "status".into(),
569 values: EnumValues::Integer(vec![NumValue {
570 name: "Pending".into(),
571 value: 0,
572 }]),
573 });
574 assert!(string_enum.requires_migration(&int_enum));
575 }
576}