1use dibs_sql::{check_constraint_name, index_name, trigger_check_name, unique_index_name};
7use facet::{Facet, Shape, Type, UserType};
8use indexmap::IndexMap;
9use std::fmt;
10
11facet::define_attr_grammar! {
17 ns "dibs";
18 crate_path ::dibs;
19
20 pub enum Attr {
22 Table(&'static str),
26
27 Pk,
31
32 Unique,
36
37 Fk(&'static str),
41
42 NotNull,
46
47 Default(&'static str),
51
52 Column(&'static str),
56
57 Index(Option<&'static str>),
61
62 CompositeIndex(CompositeIndex),
68
69 CompositeUnique(CompositeUnique),
75
76 Check(Check),
82
83 TriggerCheck(TriggerCheck),
91
92 Auto,
96
97 Long,
101
102 Label,
106
107 Lang(&'static str),
112
113 Icon(&'static str),
118
119 Subtype(&'static str),
133 }
134
135 pub struct CompositeIndex {
137 pub name: Option<&'static str>,
139 pub columns: &'static str,
141 pub filter: Option<&'static str>,
145 }
146
147 pub struct CompositeUnique {
154 pub name: Option<&'static str>,
156 pub columns: &'static str,
158 pub filter: Option<&'static str>,
162 }
163
164 pub struct Check {
166 pub name: Option<&'static str>,
168 pub expr: &'static str,
170 }
171
172 pub struct TriggerCheck {
174 pub name: Option<&'static str>,
176 pub expr: &'static str,
180 pub message: Option<&'static str>,
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum PgType {
188 SmallInt,
190 Integer,
192 BigInt,
194 Real,
196 DoublePrecision,
198 Numeric,
200 Boolean,
202 Text,
204 Bytea,
206 Timestamptz,
208 Date,
210 Time,
212 Uuid,
214 Jsonb,
216 TextArray,
218 BigIntArray,
220 IntegerArray,
222}
223
224impl PgType {
225 pub fn to_rust_type(&self) -> &'static str {
229 match self {
230 PgType::SmallInt => "i16",
231 PgType::Integer => "i32",
232 PgType::BigInt => "i64",
233 PgType::Real => "f32",
234 PgType::DoublePrecision => "f64",
235 PgType::Numeric => "Decimal",
236 PgType::Boolean => "bool",
237 PgType::Text => "String",
238 PgType::Bytea => "Vec<u8>",
239 PgType::Timestamptz => "Timestamp",
240 PgType::Date => "Date",
241 PgType::Time => "Time",
242 PgType::Uuid => "Uuid",
243 PgType::Jsonb => "Jsonb<facet_value::Value>",
244 PgType::TextArray => "Vec<String>",
245 PgType::BigIntArray => "Vec<i64>",
246 PgType::IntegerArray => "Vec<i32>",
247 }
248 }
249
250 pub fn is_integer(&self) -> bool {
252 matches!(self, PgType::SmallInt | PgType::Integer | PgType::BigInt)
253 }
254}
255
256impl fmt::Display for PgType {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 match self {
259 PgType::SmallInt => write!(f, "SMALLINT"),
260 PgType::Integer => write!(f, "INTEGER"),
261 PgType::BigInt => write!(f, "BIGINT"),
262 PgType::Real => write!(f, "REAL"),
263 PgType::DoublePrecision => write!(f, "DOUBLE PRECISION"),
264 PgType::Numeric => write!(f, "NUMERIC"),
265 PgType::Boolean => write!(f, "BOOLEAN"),
266 PgType::Text => write!(f, "TEXT"),
267 PgType::Bytea => write!(f, "BYTEA"),
268 PgType::Timestamptz => write!(f, "TIMESTAMPTZ"),
269 PgType::Date => write!(f, "DATE"),
270 PgType::Time => write!(f, "TIME"),
271 PgType::Uuid => write!(f, "UUID"),
272 PgType::Jsonb => write!(f, "JSONB"),
273 PgType::TextArray => write!(f, "TEXT[]"),
274 PgType::BigIntArray => write!(f, "BIGINT[]"),
275 PgType::IntegerArray => write!(f, "INTEGER[]"),
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq)]
282pub struct Column {
283 pub name: String,
285 pub pg_type: PgType,
287 pub rust_type: Option<String>,
289 pub nullable: bool,
291 pub default: Option<String>,
293 pub primary_key: bool,
295 pub unique: bool,
297 pub auto_generated: bool,
299 pub long: bool,
301 pub label: bool,
303 pub enum_variants: Vec<String>,
305 pub doc: Option<String>,
307 pub lang: Option<String>,
309 pub icon: Option<String>,
311 pub subtype: Option<String>,
313}
314
315impl Column {
316 pub fn is_identity(&self) -> bool {
324 self.auto_generated && self.default.is_none() && self.pg_type.is_integer()
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Hash)]
330pub struct ForeignKey {
331 pub columns: Vec<String>,
333 pub references_table: String,
335 pub references_columns: Vec<String>,
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
341pub enum SortOrder {
342 #[default]
344 Asc,
345 Desc,
347}
348
349impl SortOrder {
350 pub fn to_sql(&self) -> &'static str {
352 match self {
353 SortOrder::Asc => "",
354 SortOrder::Desc => " DESC",
355 }
356 }
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
361pub enum NullsOrder {
362 #[default]
364 Default,
365 First,
367 Last,
369}
370
371impl NullsOrder {
372 pub fn to_sql(&self) -> &'static str {
374 match self {
375 NullsOrder::Default => "",
376 NullsOrder::First => " NULLS FIRST",
377 NullsOrder::Last => " NULLS LAST",
378 }
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct IndexColumn {
385 pub name: String,
387 pub order: SortOrder,
389 pub nulls: NullsOrder,
391}
392
393impl IndexColumn {
394 pub fn new(name: impl Into<String>) -> Self {
396 Self {
397 name: name.into(),
398 order: SortOrder::Asc,
399 nulls: NullsOrder::Default,
400 }
401 }
402
403 pub fn desc(name: impl Into<String>) -> Self {
405 Self {
406 name: name.into(),
407 order: SortOrder::Desc,
408 nulls: NullsOrder::Default,
409 }
410 }
411
412 pub fn nulls_first(name: impl Into<String>) -> Self {
414 Self {
415 name: name.into(),
416 order: SortOrder::Asc,
417 nulls: NullsOrder::First,
418 }
419 }
420
421 pub fn to_sql(&self, quote_ident: impl Fn(&str) -> String) -> String {
423 format!(
424 "{}{}{}",
425 quote_ident(&self.name),
426 self.order.to_sql(),
427 self.nulls.to_sql()
428 )
429 }
430
431 pub fn parse(spec: &str) -> Self {
433 let spec = spec.trim();
434 let upper = spec.to_uppercase();
435
436 let (spec_without_nulls, nulls) = if upper.ends_with(" NULLS FIRST") {
438 (&spec[..spec.len() - 12], NullsOrder::First)
439 } else if upper.ends_with(" NULLS LAST") {
440 (&spec[..spec.len() - 11], NullsOrder::Last)
441 } else {
442 (spec, NullsOrder::Default)
443 };
444
445 let trimmed = spec_without_nulls.trim();
446 let upper_trimmed = trimmed.to_uppercase();
447
448 let (name, order) = if upper_trimmed.ends_with(" DESC") {
450 (
451 trimmed[..trimmed.len() - 5].trim().to_string(),
452 SortOrder::Desc,
453 )
454 } else if upper_trimmed.ends_with(" ASC") {
455 (
456 trimmed[..trimmed.len() - 4].trim().to_string(),
457 SortOrder::Asc,
458 )
459 } else {
460 (trimmed.to_string(), SortOrder::Asc)
461 };
462
463 fn unquote_pg_ident_if_quoted(s: &str) -> String {
464 let s = s.trim();
465 if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
466 let inner = &s[1..s.len() - 1];
467 return inner.replace("\"\"", "\"");
468 }
469 s.to_string()
470 }
471
472 Self {
473 name: unquote_pg_ident_if_quoted(&name),
474 order,
475 nulls,
476 }
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
482pub struct Index {
483 pub name: String,
485 pub columns: Vec<IndexColumn>,
487 pub unique: bool,
489 pub where_clause: Option<String>,
491}
492
493#[derive(Debug, Clone, Default, PartialEq)]
495pub struct SourceLocation {
496 pub file: Option<String>,
498 pub line: Option<u32>,
500 pub column: Option<u32>,
502}
503
504impl SourceLocation {
505 pub fn is_known(&self) -> bool {
507 self.file.is_some()
508 }
509
510 pub fn to_string_short(&self) -> Option<String> {
512 let file = self.file.as_ref()?;
513 match (self.line, self.column) {
514 (Some(line), Some(col)) => Some(format!("{}:{}:{}", file, line, col)),
515 (Some(line), None) => Some(format!("{}:{}", file, line)),
516 _ => Some(file.clone()),
517 }
518 }
519}
520
521impl fmt::Display for SourceLocation {
522 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523 match self.to_string_short() {
524 Some(s) => write!(f, "{}", s),
525 None => write!(f, "<unknown>"),
526 }
527 }
528}
529
530#[derive(Debug, Clone, PartialEq)]
532pub struct CheckConstraint {
533 pub name: String,
534 pub expr: String,
535}
536
537#[derive(Debug, Clone, PartialEq)]
539pub struct TriggerCheckConstraint {
540 pub name: String,
541 pub expr: String,
542 pub message: Option<String>,
543}
544
545#[derive(Debug, Clone, PartialEq)]
547pub struct Table {
548 pub name: String,
550 pub columns: Vec<Column>,
552 pub check_constraints: Vec<CheckConstraint>,
554 pub trigger_checks: Vec<TriggerCheckConstraint>,
556 pub foreign_keys: Vec<ForeignKey>,
558 pub indices: Vec<Index>,
560 pub source: SourceLocation,
562 pub doc: Option<String>,
564 pub icon: Option<String>,
566}
567
568#[derive(Debug, Clone, Default)]
570pub struct Schema {
571 pub tables: IndexMap<String, Table>,
573}
574
575impl Schema {
576 pub fn new() -> Self {
578 Self::default()
579 }
580
581 pub fn get_table(&self, name: &str) -> Option<&Table> {
583 self.tables.get(name)
584 }
585
586 pub fn iter_tables(&self) -> impl Iterator<Item = &Table> {
588 self.tables.values()
589 }
590}
591
592pub struct TableDef {
600 pub shape: &'static Shape,
602}
603
604impl TableDef {
605 pub const fn new<T: Facet<'static>>() -> Self {
607 Self { shape: T::SHAPE }
608 }
609
610 pub fn table_name(&self) -> Option<&'static str> {
612 shape_get_dibs_attr_str(self.shape, "table")
613 }
614
615 pub fn to_table(&self) -> Option<Table> {
617 let table_name = self.table_name()?.to_string();
618
619 let struct_type = match &self.shape.ty {
621 Type::User(UserType::Struct(s)) => s,
622 _ => return None,
623 };
624
625 let mut columns = Vec::new();
626 let mut check_constraints = Vec::new();
627 let mut trigger_checks = Vec::new();
628 let mut foreign_keys = Vec::new();
629 let mut indices = Vec::new();
630
631 for attr in self.shape.attributes.iter() {
633 if attr.ns == Some("dibs")
634 && attr.key == "composite_index"
635 && let Some(Attr::CompositeIndex(composite)) = attr.get_as::<Attr>()
636 {
637 let cols: Vec<IndexColumn> = composite
638 .columns
639 .split(',')
640 .map(IndexColumn::parse)
641 .collect();
642 let col_names: Vec<&str> = cols.iter().map(|c| c.name.as_str()).collect();
643 let idx_name = composite
644 .name
645 .map(|s| s.to_string())
646 .unwrap_or_else(|| index_name(&table_name, &col_names));
647 indices.push(Index {
648 name: idx_name,
649 columns: cols,
650 unique: false,
651 where_clause: composite.filter.map(|s| s.to_string()),
652 });
653 }
654 if attr.ns == Some("dibs")
656 && attr.key == "composite_unique"
657 && let Some(Attr::CompositeUnique(composite)) = attr.get_as::<Attr>()
658 {
659 let cols: Vec<IndexColumn> = composite
660 .columns
661 .split(',')
662 .map(IndexColumn::parse)
663 .collect();
664 let col_names: Vec<&str> = cols.iter().map(|c| c.name.as_str()).collect();
665 let idx_name = composite
666 .name
667 .map(|s| s.to_string())
668 .unwrap_or_else(|| unique_index_name(&table_name, &col_names));
669 indices.push(Index {
670 name: idx_name,
671 columns: cols,
672 unique: true,
673 where_clause: composite.filter.map(|s| s.to_string()),
674 });
675 }
676
677 if attr.ns == Some("dibs")
679 && attr.key == "check"
680 && let Some(Attr::Check(check)) = attr.get_as::<Attr>()
681 {
682 let expr = unescape_rust_string_escapes(check.expr);
683 let name = check
684 .name
685 .map(|s| s.to_string())
686 .unwrap_or_else(|| check_constraint_name(&table_name, &expr));
687 check_constraints.push(CheckConstraint { name, expr });
688 }
689
690 if attr.ns == Some("dibs")
692 && attr.key == "trigger_check"
693 && let Some(Attr::TriggerCheck(trig)) = attr.get_as::<Attr>()
694 {
695 let expr = unescape_rust_string_escapes(trig.expr);
696 let name = trig
697 .name
698 .map(|s| s.to_string())
699 .unwrap_or_else(|| trigger_check_name(&table_name, &expr));
700 trigger_checks.push(TriggerCheckConstraint {
701 name,
702 expr,
703 message: trig.message.map(unescape_rust_string_escapes),
704 });
705 }
706 }
707
708 for field in struct_type.fields {
709 let field_shape = field.shape.get();
710
711 let col_name = field_get_dibs_attr_str(field, "column")
713 .map(|s| s.to_string())
714 .unwrap_or_else(|| field.name.to_string());
715
716 let (inner_shape, nullable) = unwrap_option(field_shape);
718
719 let pg_type = match shape_to_pg_type(inner_shape) {
721 Some(pg_type) => pg_type,
722 None => {
723 eprintln!(
724 "dibs: unsupported type '{}' for column '{}' in table '{}' ({})",
725 inner_shape,
726 field.name,
727 table_name,
728 self.shape.source_file.unwrap_or("<unknown>")
729 );
730 return None;
731 }
732 };
733
734 let primary_key = field_has_dibs_attr(field, "pk");
736
737 let unique = field_has_dibs_attr(field, "unique");
739
740 let default = field_get_dibs_attr_str(field, "default").map(|s| s.to_string());
742
743 let doc = if field.doc.is_empty() {
745 None
746 } else {
747 Some(field.doc.join("\n"))
748 };
749
750 let auto_generated =
752 is_auto_generated_default(&default) || field_has_dibs_attr(field, "auto");
753
754 let lang = field_get_dibs_attr_str(field, "lang").map(|s| s.to_string());
756
757 let long = field_has_dibs_attr(field, "long") || lang.is_some();
759
760 let label = field_has_dibs_attr(field, "label");
762
763 let subtype = field_get_dibs_attr_str(field, "subtype").map(|s| s.to_string());
765
766 let explicit_icon = field_get_dibs_attr_str(field, "icon").map(|s| s.to_string());
768 let icon = explicit_icon.or_else(|| {
769 subtype
770 .as_ref()
771 .and_then(|st| subtype_default_icon(st).map(|s| s.to_string()))
772 });
773
774 let enum_variants = extract_enum_variants(inner_shape);
776
777 let rust_type = pg_type.to_rust_type().to_string();
779
780 columns.push(Column {
781 name: col_name.clone(),
782 pg_type,
783 rust_type: Some(rust_type),
784 nullable,
785 default,
786 primary_key,
787 unique,
788 auto_generated,
789 long,
790 label,
791 enum_variants,
792 doc,
793 lang,
794 icon,
795 subtype,
796 });
797
798 if let Some(fk_ref) = field_get_dibs_attr_str(field, "fk") {
800 let parsed = parse_fk_reference(fk_ref);
802 match parsed {
803 Some((ref_table, ref_col)) => {
804 foreign_keys.push(ForeignKey {
805 columns: vec![field.name.to_string()],
806 references_table: ref_table.to_string(),
807 references_columns: vec![ref_col.to_string()],
808 });
809 }
810 None => {
811 eprintln!(
813 "dibs: invalid FK format '{}' for field '{}' in table '{}' - expected 'table.column' or 'table(column)' ({})",
814 fk_ref,
815 field.name,
816 table_name,
817 self.shape.source_file.unwrap_or("<unknown>")
818 );
819 }
820 }
821 }
822
823 if field_has_dibs_attr(field, "index") {
825 let idx_name = field_get_dibs_attr_str(field, "index")
826 .filter(|s| !s.is_empty())
827 .map(|s| s.to_string())
828 .unwrap_or_else(|| crate::index_name(&table_name, &[&col_name]));
829 indices.push(Index {
830 name: idx_name,
831 columns: vec![IndexColumn::new(col_name.clone())],
832 unique: false,
833 where_clause: None, });
835 }
836 }
837
838 let source = SourceLocation {
840 file: self.shape.source_file.map(|s| s.to_string()),
841 line: self.shape.source_line,
842 column: self.shape.source_column,
843 };
844
845 let doc = if self.shape.doc.is_empty() {
847 None
848 } else {
849 Some(self.shape.doc.join("\n"))
850 };
851
852 let icon = shape_get_dibs_attr_str(self.shape, "icon").map(|s| s.to_string());
854
855 Some(Table {
856 name: table_name,
857 columns,
858 check_constraints,
859 trigger_checks,
860 foreign_keys,
861 indices,
862 source,
863 doc,
864 icon,
865 })
866 }
867}
868
869fn unwrap_option(lhs: &'static Shape) -> (&'static Shape, bool) {
871 let rhs = Option::<()>::SHAPE;
872
873 if lhs.decl_id == rhs.decl_id {
874 if let Some(inner) = lhs.inner {
876 return (inner, true);
877 }
878 }
879 (lhs, false)
880}
881
882#[test]
883fn test_unwrap_option() {
884 let (inner, success) = unwrap_option(Option::<dibs_jsonb::Jsonb<facet_value::Value>>::SHAPE);
885 assert!(success);
886 assert_eq!(inner, dibs_jsonb::Jsonb::<facet_value::Value>::SHAPE);
887}
888
889fn subtype_default_icon(subtype: &str) -> Option<&'static str> {
891 match subtype {
892 "email" => Some("mail"),
894 "phone" => Some("phone"),
895 "url" | "website" => Some("link"),
896 "username" => Some("at-sign"),
897
898 "image" | "avatar" | "photo" => Some("image"),
900 "file" => Some("file"),
901 "video" => Some("video"),
902 "audio" => Some("music"),
903
904 "currency" | "money" | "price" => Some("coins"),
906 "percent" | "percentage" => Some("percent"),
907
908 "password" => Some("lock"),
910 "secret" | "token" | "api_key" => Some("key"),
911
912 "code" => Some("code"),
914 "json" => Some("braces"),
915 "markdown" | "md" => Some("file-text"),
916 "html" => Some("code"),
917 "regex" => Some("asterisk"),
918
919 "address" => Some("map-pin"),
921 "city" => Some("building-2"),
922 "country" => Some("flag"),
923 "zip" | "postal_code" => Some("hash"),
924 "ip" | "ip_address" => Some("globe"),
925 "coordinates" | "geo" => Some("map"),
926
927 "slug" => Some("link-2"),
929 "color" | "hex_color" => Some("palette"),
930 "tag" | "tags" => Some("tag"),
931
932 "uuid" => Some("fingerprint"),
934 "sku" | "barcode" => Some("scan-barcode"),
935 "version" => Some("git-branch"),
936
937 "duration" => Some("timer"),
939
940 _ => None,
941 }
942}
943
944fn shape_get_dibs_attr_str(shape: &Shape, key: &str) -> Option<&'static str> {
950 shape.attributes.iter().find_map(|attr| {
951 if attr.ns == Some("dibs") && attr.key == key {
952 attr.get_as::<&str>().copied()
953 } else {
954 None
955 }
956 })
957}
958
959fn field_has_dibs_attr(field: &facet::Field, key: &str) -> bool {
961 field
962 .attributes
963 .iter()
964 .any(|attr| attr.ns == Some("dibs") && attr.key == key)
965}
966
967fn field_get_dibs_attr_str(field: &facet::Field, key: &str) -> Option<&'static str> {
969 field.attributes.iter().find_map(|attr| {
970 if attr.ns == Some("dibs") && attr.key == key {
971 attr.get_as::<&str>().copied()
972 } else {
973 None
974 }
975 })
976}
977
978fn is_auto_generated_default(default: &Option<String>) -> bool {
980 let Some(def) = default else {
983 return false;
984 };
985
986 let lower = def.to_lowercase();
987
988 if lower.contains("nextval(") {
990 return true;
991 }
992
993 if lower.contains("gen_random_uuid()") || lower.contains("uuid_generate_v") {
995 return true;
996 }
997
998 if lower.contains("now()") || lower.contains("current_timestamp") {
1000 return true;
1001 }
1002
1003 false
1004}
1005
1006fn extract_enum_variants(shape: &'static Shape) -> Vec<String> {
1008 if let Type::User(UserType::Enum(enum_type)) = shape.ty {
1009 enum_type
1010 .variants
1011 .iter()
1012 .map(|v| v.name.to_string())
1013 .collect()
1014 } else {
1015 vec![]
1016 }
1017}
1018
1019fn unescape_rust_string_escapes(value: &str) -> String {
1020 if !value.contains('\\') {
1021 return value.to_string();
1022 }
1023
1024 let mut out = String::with_capacity(value.len());
1025 let mut chars = value.chars();
1026 while let Some(ch) = chars.next() {
1027 if ch != '\\' {
1028 out.push(ch);
1029 continue;
1030 }
1031
1032 match chars.next() {
1033 Some('\\') => out.push('\\'),
1034 Some('"') => out.push('"'),
1035 Some('\'') => out.push('\''),
1036 Some('n') => out.push('\n'),
1037 Some('r') => out.push('\r'),
1038 Some('t') => out.push('\t'),
1039 Some('0') => out.push('\0'),
1040 Some(other) => {
1041 out.push('\\');
1043 out.push(other);
1044 }
1045 None => out.push('\\'),
1046 }
1047 }
1048
1049 out
1050}
1051
1052pub fn parse_fk_reference(fk_ref: &str) -> Option<(&str, &str)> {
1060 if let Some((table, col)) = fk_ref.split_once('.')
1062 && !table.is_empty()
1063 && !col.is_empty()
1064 {
1065 return Some((table, col));
1066 }
1067
1068 if let Some(paren_idx) = fk_ref.find('(')
1070 && fk_ref.ends_with(')')
1071 {
1072 let table = &fk_ref[..paren_idx];
1073 let col = &fk_ref[paren_idx + 1..fk_ref.len() - 1];
1074 if !table.is_empty() && !col.is_empty() {
1075 return Some((table, col));
1076 }
1077 }
1078
1079 None
1080}
1081
1082pub fn shape_to_pg_type(shape: &Shape) -> Option<PgType> {
1086 if shape.decl_id == dibs_jsonb::Jsonb::<()>::SHAPE.decl_id {
1087 return Some(PgType::Jsonb);
1088 }
1089
1090 if matches!(&shape.def, facet::Def::List(_)) {
1092 if let Some(inner) = shape.inner {
1093 if inner == u8::SHAPE {
1094 return Some(PgType::Bytea);
1095 } else if inner == String::SHAPE {
1096 return Some(PgType::TextArray);
1097 } else if inner == i64::SHAPE {
1098 return Some(PgType::BigIntArray);
1099 } else if inner == i32::SHAPE {
1100 return Some(PgType::IntegerArray);
1101 }
1102 }
1103 return None;
1104 }
1105
1106 if matches!(&shape.def, facet::Def::Slice(_)) {
1108 if let Some(inner) = shape.inner
1109 && inner == u8::SHAPE
1110 {
1111 return Some(PgType::Bytea);
1112 }
1113 return None;
1114 }
1115
1116 rust_type_to_pg(shape)
1118}
1119
1120pub fn rust_type_to_pg(shape: &Shape) -> Option<PgType> {
1122 if shape == i8::SHAPE || shape == u8::SHAPE || shape == i16::SHAPE {
1124 Some(PgType::SmallInt)
1125 } else if shape == u16::SHAPE || shape == i32::SHAPE {
1127 Some(PgType::Integer)
1128 } else if shape == u32::SHAPE
1130 || shape == i64::SHAPE
1131 || shape == u64::SHAPE
1132 || shape == isize::SHAPE
1133 || shape == usize::SHAPE
1134 {
1135 Some(PgType::BigInt)
1136 } else if shape == f32::SHAPE {
1138 Some(PgType::Real)
1139 } else if shape == f64::SHAPE {
1140 Some(PgType::DoublePrecision)
1141 } else if shape == bool::SHAPE {
1142 Some(PgType::Boolean)
1143 } else if shape == String::SHAPE {
1144 Some(PgType::Text)
1145 } else if shape == rust_decimal::Decimal::SHAPE {
1146 Some(PgType::Numeric)
1147 } else if shape == jiff::Timestamp::SHAPE || shape == jiff::Zoned::SHAPE {
1148 Some(PgType::Timestamptz)
1149 } else if shape == jiff::civil::Date::SHAPE {
1150 Some(PgType::Date)
1151 } else if shape == jiff::civil::Time::SHAPE {
1152 Some(PgType::Time)
1153 } else if shape == chrono::DateTime::<chrono::Utc>::SHAPE
1154 || shape == chrono::DateTime::<chrono::Local>::SHAPE
1155 || shape == chrono::NaiveDateTime::SHAPE
1156 {
1157 Some(PgType::Timestamptz)
1158 } else if shape == chrono::NaiveDate::SHAPE {
1159 Some(PgType::Date)
1160 } else if shape == chrono::NaiveTime::SHAPE {
1161 Some(PgType::Time)
1162 } else if shape == uuid::Uuid::SHAPE {
1163 Some(PgType::Uuid)
1164 } else {
1165 None
1166 }
1167}
1168
1169inventory::collect!(TableDef);
1171
1172#[cfg(test)]
1173mod tests;