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