rust_ef/migration/
dialect.rs1use super::types::SnapshotColumn;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum MigrationDialect {
8 Postgres,
9 MySql,
10 Sqlite,
11}
12
13impl MigrationDialect {
14 pub fn quote(&self, ident: &str) -> String {
19 match self {
20 MigrationDialect::Postgres | MigrationDialect::Sqlite => quote_qualified(ident, '"'),
21 MigrationDialect::MySql => quote_qualified(ident, '`'),
22 }
23 }
24
25 pub fn map_column_type(&self, col: &SnapshotColumn) -> String {
27 if let Some(explicit) = &col.column_type {
34 if !explicit.trim().is_empty() {
35 return explicit.trim().to_string();
36 }
37 }
38
39 let tn = inner_type_name(&col.type_name);
43
44 if col.is_auto_increment {
46 if tn.ends_with("i32") {
47 return match self {
48 MigrationDialect::Postgres => "SERIAL".into(),
49 MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
50 MigrationDialect::Sqlite => "INTEGER".into(),
51 };
52 }
53 if tn.ends_with("i64") {
54 return match self {
55 MigrationDialect::Postgres => "BIGSERIAL".into(),
56 MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
57 MigrationDialect::Sqlite => "INTEGER".into(),
58 };
59 }
60 }
61
62 if col.is_sequence {
65 if tn.ends_with("i32") {
66 return match self {
67 MigrationDialect::Postgres => "INTEGER".into(),
68 MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
69 MigrationDialect::Sqlite => "INTEGER".into(),
70 };
71 }
72 if tn.ends_with("i64") {
73 return match self {
74 MigrationDialect::Postgres => "BIGINT".into(),
75 MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
76 MigrationDialect::Sqlite => "INTEGER".into(),
77 };
78 }
79 }
80
81 let base: &str = if tn.ends_with("i16") {
82 "SMALLINT"
83 } else if tn.ends_with("i32") {
84 "INTEGER"
85 } else if tn.ends_with("i64") {
86 "BIGINT"
87 } else if tn.ends_with("f32") {
88 "REAL"
89 } else if tn.ends_with("f64") {
90 "DOUBLE PRECISION"
91 } else if tn.ends_with("bool") {
92 "BOOLEAN"
93 } else if tn.ends_with("String") {
94 if col.is_fixed_length {
95 return format!("CHAR({})", col.max_length.unwrap_or(1));
98 }
99 return match col.max_length {
100 Some(n) => format!("VARCHAR({})", n),
101 None => "TEXT".into(),
102 };
103 } else if tn.ends_with("Vec<u8>") {
104 return match self {
105 MigrationDialect::Postgres => "BYTEA".into(),
106 MigrationDialect::MySql | MigrationDialect::Sqlite => "BLOB".into(),
107 };
108 } else if tn.contains("NaiveDateTime") {
109 return match (self, col.precision) {
110 (MigrationDialect::Postgres, Some(p)) => format!("TIMESTAMP({p})"),
111 (MigrationDialect::MySql, Some(p)) => format!("DATETIME({p})"),
112 (MigrationDialect::Postgres, None) => "TIMESTAMP".into(),
113 (MigrationDialect::MySql, None) => "DATETIME".into(),
114 (MigrationDialect::Sqlite, _) => "TEXT".into(),
115 };
116 } else if tn.contains("NaiveDate") {
117 return match self {
118 MigrationDialect::Postgres => "DATE".into(),
119 MigrationDialect::MySql => "DATE".into(),
120 MigrationDialect::Sqlite => "TEXT".into(),
121 };
122 } else if tn.contains("DateTime") {
123 return match (self, col.precision) {
125 (MigrationDialect::Postgres, Some(p)) => format!("TIMESTAMPTZ({p})"),
126 (MigrationDialect::MySql, Some(p)) => format!("DATETIME({p})"),
127 (MigrationDialect::Postgres, None) => "TIMESTAMPTZ".into(),
128 (MigrationDialect::MySql, None) => "DATETIME".into(),
129 (MigrationDialect::Sqlite, _) => "TEXT".into(),
130 };
131 } else if tn.contains("Uuid") {
132 return match self {
133 MigrationDialect::Postgres => "UUID".into(),
134 MigrationDialect::MySql => "CHAR(36)".into(),
135 MigrationDialect::Sqlite => "TEXT".into(),
136 };
137 } else if tn.contains("Decimal") {
138 let (p, s) = (col.precision.unwrap_or(38), col.scale.unwrap_or(18));
139 return match self {
140 MigrationDialect::Postgres => {
141 if col.precision.is_some() {
142 format!("NUMERIC({p},{s})")
143 } else {
144 "NUMERIC".into()
145 }
146 }
147 MigrationDialect::MySql => format!("DECIMAL({p},{s})"),
148 MigrationDialect::Sqlite => "TEXT".into(),
149 };
150 } else {
151 "TEXT"
152 };
153 base.to_string()
154 }
155}
156
157pub(crate) fn quote_qualified(ident: &str, quote: char) -> String {
159 ident
160 .split('.')
161 .map(|part| format!("{quote}{part}{quote}"))
162 .collect::<Vec<_>>()
163 .join(".")
164}
165
166fn inner_type_name(type_name: &str) -> &str {
172 if let Some(pos) = type_name.find("Option<") {
173 let rest = &type_name[pos + "Option<".len()..];
174 return rest.strip_suffix('>').unwrap_or(rest);
175 }
176 type_name
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn option_string_uses_inner_type_and_max_length() {
185 let col = SnapshotColumn {
186 type_name: "core::option::Option<alloc::string::String>".into(),
187 max_length: Some(32),
188 ..Default::default()
189 };
190 assert_eq!(MigrationDialect::MySql.map_column_type(&col), "VARCHAR(32)");
191 assert_eq!(
192 MigrationDialect::Postgres.map_column_type(&col),
193 "VARCHAR(32)"
194 );
195 }
196
197 #[test]
198 fn option_uuid_and_decimal_use_inner_type() {
199 let uuid = SnapshotColumn {
200 type_name: "core::option::Option<uuid::Uuid>".into(),
201 ..Default::default()
202 };
203 assert_eq!(MigrationDialect::MySql.map_column_type(&uuid), "CHAR(36)");
204
205 let decimal = SnapshotColumn {
206 type_name: "core::option::Option<rust_decimal::Decimal>".into(),
207 ..Default::default()
208 };
209 assert_eq!(
210 MigrationDialect::MySql.map_column_type(&decimal),
211 "DECIMAL(38,18)"
212 );
213 }
214}