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