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