1use crate::config::{Dialect, Matcher, Types};
16use crate::error::{GenError, Result};
17use crate::schema::{ColumnDef, TableDef};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ResolvedType {
23 pub rust_type: String,
25 pub overridden: bool,
27}
28
29pub(crate) fn normalise(db_type: &str) -> String {
33 let base = match db_type.find('(') {
34 Some(i) => &db_type[..i],
35 None => db_type,
36 };
37 base.trim().to_lowercase()
38}
39
40pub(crate) fn resolve(
42 dialect: Dialect,
43 types: &Types,
44 table: &TableDef,
45 column: &ColumnDef,
46) -> Result<ResolvedType> {
47 for o in &types.overrides {
48 let in_scope = o.tables.is_empty() || o.tables.contains(&table.name);
49 if in_scope && matches(&o.matcher, column) {
50 return Ok(ResolvedType {
51 rust_type: o.rust_type.clone(),
52 overridden: true,
53 });
54 }
55 }
56 let norm = normalise(&column.db_type);
57 if let Some(t) = types.map.get(&norm) {
58 return Ok(ResolvedType {
59 rust_type: t.clone(),
60 overridden: true,
61 });
62 }
63 match default_type(dialect, &norm, column) {
64 Some(t) => Ok(ResolvedType {
65 rust_type: t.to_owned(),
66 overridden: false,
67 }),
68 None => Err(GenError::UnmappedType {
69 column: format!("{}.{}", table.name, column.name),
70 db_type: column.db_type.clone(),
71 }),
72 }
73}
74
75fn matches(m: &Matcher, c: &ColumnDef) -> bool {
76 if let Some(name) = &m.name
77 && *name != c.name
78 {
79 return false;
80 }
81 if let Some(db_type) = &m.db_type
82 && normalise(db_type) != normalise(&c.db_type)
83 {
84 return false;
85 }
86 if let Some(nullable) = m.nullable
87 && nullable != c.nullable
88 {
89 return false;
90 }
91 if let Some(default) = &m.default
92 && Some(default.as_str()) != c.default.as_deref()
93 {
94 return false;
95 }
96 if let Some(autoincrement) = m.autoincrement
97 && autoincrement != c.autoincrement
98 {
99 return false;
100 }
101 if let Some(comment) = &m.comment
102 && Some(comment.as_str()) != c.comment.as_deref()
103 {
104 return false;
105 }
106 true
107}
108
109fn default_type(dialect: Dialect, norm: &str, column: &ColumnDef) -> Option<&'static str> {
111 match dialect {
112 Dialect::Psql => psql_default(norm),
113 Dialect::Sqlite => sqlite_default(norm, column),
114 Dialect::Mysql => mysql_default(norm, column),
115 }
116}
117
118fn psql_default(norm: &str) -> Option<&'static str> {
120 Some(match norm {
121 "smallint" | "int2" => "i16",
122 "integer" | "int4" => "i32",
123 "bigint" | "int8" => "i64",
124 "real" | "float4" => "f32",
125 "double precision" | "float8" => "f64",
126 "boolean" | "bool" => "bool",
127 "text" | "character varying" | "varchar" | "character" | "bpchar" | "citext" | "name" => {
128 "String"
129 }
130 "bytea" => "Vec<u8>",
131 "date" => "chrono::NaiveDate",
132 "time" | "time without time zone" => "chrono::NaiveTime",
133 "timestamp" | "timestamp without time zone" => "chrono::NaiveDateTime",
134 "timestamptz" | "timestamp with time zone" => "chrono::DateTime<chrono::Utc>",
135 "uuid" => "uuid::Uuid",
136 "numeric" | "decimal" => "rust_decimal::Decimal",
137 "json" | "jsonb" => "serde_json::Value",
138 _ => return None,
139 })
140}
141
142fn sqlite_default(norm: &str, column: &ColumnDef) -> Option<&'static str> {
150 match norm {
152 "boolean" | "bool" => return Some("bool"),
153 "datetime" | "timestamp" => return Some("chrono::NaiveDateTime"),
154 "date" => return Some("chrono::NaiveDate"),
155 "time" => return Some("chrono::NaiveTime"),
156 "" => return Some("Vec<u8>"), _ => {}
158 }
159 if norm.contains("int") {
160 return Some("i64"); }
162 if norm.contains("char") || norm.contains("clob") || norm.contains("text") {
163 if column
164 .default
165 .as_deref()
166 .is_some_and(|d| d.trim().eq_ignore_ascii_case("current_timestamp"))
167 {
168 return Some("chrono::NaiveDateTime");
169 }
170 return Some("String");
171 }
172 if norm.contains("blob") {
173 return Some("Vec<u8>");
174 }
175 if norm.contains("real") || norm.contains("floa") || norm.contains("doub") {
176 return Some("f64");
177 }
178 if norm.contains("dec") || norm.contains("numeric") {
179 return Some("rust_decimal::Decimal");
180 }
181 None
182}
183
184fn mysql_default(norm: &str, column: &ColumnDef) -> Option<&'static str> {
200 let raw = column.db_type.trim().to_lowercase();
201 if raw.starts_with("tinyint(1)") || norm == "bool" || norm == "boolean" {
202 return Some("bool");
203 }
204 Some(match norm {
205 "tinyint" => "i8",
206 "smallint" => "i16",
207 "mediumint" | "int" | "integer" => "i32",
208 "bigint" => "i64",
209 "tinyint unsigned" => "u8",
210 "smallint unsigned" => "u16",
211 "mediumint unsigned" | "int unsigned" | "integer unsigned" => "u32",
212 "bigint unsigned" => "u64",
213 "float" => "f32",
214 "double" | "double precision" | "real" => "f64",
215 "decimal" | "numeric" => "rust_decimal::Decimal",
216 "char" | "varchar" | "tinytext" | "text" | "mediumtext" | "longtext" | "enum" | "set" => {
217 "String"
218 }
219 "binary" | "varbinary" | "tinyblob" | "blob" | "mediumblob" | "longblob" => "Vec<u8>",
220 "date" => "chrono::NaiveDate",
221 "time" => "chrono::NaiveTime",
222 "datetime" => "chrono::NaiveDateTime",
223 "timestamp" => "chrono::DateTime<chrono::Utc>",
224 "json" => "serde_json::Value",
225 _ => return None,
228 })
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::config::Config;
235 use crate::schema::TableKind;
236
237 fn col(name: &str, db_type: &str, nullable: bool) -> ColumnDef {
238 ColumnDef {
239 name: name.to_owned(),
240 db_type: db_type.to_owned(),
241 nullable,
242 default: None,
243 autoincrement: false,
244 comment: None,
245 }
246 }
247
248 fn table(name: &str, columns: Vec<ColumnDef>) -> TableDef {
249 TableDef {
250 name: name.to_owned(),
251 kind: TableKind::Table,
252 columns,
253 primary_key: vec![],
254 foreign_keys: vec![],
255 unique_keys: vec![],
256 }
257 }
258
259 fn plain(dialect: Dialect, db_type: &str) -> String {
260 let c = col("c", db_type, false);
261 let t = table("t", vec![c.clone()]);
262 resolve(dialect, &Types::default(), &t, &c)
263 .unwrap()
264 .rust_type
265 }
266
267 #[test]
268 fn the_psql_defaults_follow_the_type_table() {
269 assert_eq!(plain(Dialect::Psql, "integer"), "i32");
270 assert_eq!(plain(Dialect::Psql, "bigint"), "i64");
271 assert_eq!(plain(Dialect::Psql, "text"), "String");
272 assert_eq!(plain(Dialect::Psql, "character varying(255)"), "String");
273 assert_eq!(plain(Dialect::Psql, "boolean"), "bool");
274 assert_eq!(
275 plain(Dialect::Psql, "timestamp with time zone"),
276 "chrono::DateTime<chrono::Utc>"
277 );
278 assert_eq!(
279 plain(Dialect::Psql, "timestamp without time zone"),
280 "chrono::NaiveDateTime"
281 );
282 assert_eq!(plain(Dialect::Psql, "date"), "chrono::NaiveDate");
283 assert_eq!(plain(Dialect::Psql, "uuid"), "uuid::Uuid");
284 assert_eq!(
285 plain(Dialect::Psql, "numeric(10,2)"),
286 "rust_decimal::Decimal"
287 );
288 assert_eq!(plain(Dialect::Psql, "jsonb"), "serde_json::Value");
289 }
290
291 #[test]
292 fn the_sqlite_defaults_follow_declared_types_and_the_spec_notes() {
293 assert_eq!(plain(Dialect::Sqlite, "INTEGER"), "i64");
294 assert_eq!(plain(Dialect::Sqlite, "TEXT"), "String");
295 assert_eq!(plain(Dialect::Sqlite, "BOOLEAN"), "bool");
296 assert_eq!(plain(Dialect::Sqlite, "VARCHAR(80)"), "String");
297 assert_eq!(plain(Dialect::Sqlite, "BLOB"), "Vec<u8>");
298 assert_eq!(plain(Dialect::Sqlite, "REAL"), "f64");
299 assert_eq!(plain(Dialect::Sqlite, "DATETIME"), "chrono::NaiveDateTime");
300
301 let mut c = col("created_at", "TEXT", false);
304 c.default = Some("CURRENT_TIMESTAMP".to_owned());
305 let t = table("users", vec![c.clone()]);
306 assert_eq!(
307 resolve(Dialect::Sqlite, &Types::default(), &t, &c)
308 .unwrap()
309 .rust_type,
310 "chrono::NaiveDateTime"
311 );
312 }
313
314 #[test]
315 fn the_mysql_defaults_follow_the_type_table_including_tinyint_one() {
316 assert_eq!(plain(Dialect::Mysql, "int"), "i32");
317 assert_eq!(plain(Dialect::Mysql, "bigint"), "i64");
318 assert_eq!(plain(Dialect::Mysql, "bigint unsigned"), "u64");
319 assert_eq!(plain(Dialect::Mysql, "varchar(255)"), "String");
320 assert_eq!(plain(Dialect::Mysql, "text"), "String");
321 assert_eq!(plain(Dialect::Mysql, "datetime"), "chrono::NaiveDateTime");
322 assert_eq!(
323 plain(Dialect::Mysql, "timestamp"),
324 "chrono::DateTime<chrono::Utc>"
325 );
326 assert_eq!(
327 plain(Dialect::Mysql, "decimal(10,2)"),
328 "rust_decimal::Decimal"
329 );
330 assert_eq!(plain(Dialect::Mysql, "json"), "serde_json::Value");
331 assert_eq!(plain(Dialect::Mysql, "blob"), "Vec<u8>");
332
333 assert_eq!(plain(Dialect::Mysql, "tinyint(1)"), "bool");
336 assert_eq!(plain(Dialect::Mysql, "tinyint"), "i8");
337 assert_eq!(plain(Dialect::Mysql, "tinyint(4)"), "i8");
338 }
339
340 #[test]
341 fn an_unmapped_mysql_type_is_a_loud_error_too() {
342 let c = col("born", "year", false);
343 let t = table("people", vec![c.clone()]);
344 let err = resolve(Dialect::Mysql, &Types::default(), &t, &c).unwrap_err();
345 assert!(err.to_string().contains("people.born"), "{err}");
346 }
347
348 #[test]
349 fn an_unmapped_type_is_a_loud_error_naming_the_column() {
350 let c = col("shape", "polygon", false);
351 let t = table("zones", vec![c.clone()]);
352 let err = resolve(Dialect::Psql, &Types::default(), &t, &c).unwrap_err();
353 assert!(err.to_string().contains("zones.shape"), "{err}");
354 assert!(err.to_string().contains("polygon"), "{err}");
355 }
356
357 #[test]
358 fn overrides_win_and_are_marked_for_assert_bind() {
359 let cfg = Config::from_toml(
360 r#"
361 dialect = "sqlite"
362 [types.map]
363 "numeric" = "MyMoney"
364 [[types.override]]
365 tables = ["posts"]
366 rust_type = "chrono::NaiveDateTime"
367 [types.override.match]
368 name = "published_at"
369 db_type = "text"
370 "#,
371 )
372 .unwrap();
373
374 let c = col("published_at", "TEXT", true);
375 let t = table("posts", vec![c.clone()]);
376 let r = resolve(Dialect::Sqlite, &cfg.types, &t, &c).unwrap();
377 assert_eq!(r.rust_type, "chrono::NaiveDateTime");
378 assert!(r.overridden);
379
380 let t2 = table("drafts", vec![c.clone()]);
382 let r2 = resolve(Dialect::Sqlite, &cfg.types, &t2, &c).unwrap();
383 assert_eq!(r2.rust_type, "String");
384 assert!(!r2.overridden);
385
386 let c3 = col("price", "NUMERIC(10,2)", false);
388 let t3 = table("orders", vec![c3.clone()]);
389 let r3 = resolve(Dialect::Sqlite, &cfg.types, &t3, &c3).unwrap();
390 assert_eq!(r3.rust_type, "MyMoney");
391 assert!(r3.overridden);
392 }
393
394 #[test]
395 fn matcher_fields_are_conjunctive() {
396 let cfg = Config::from_toml(
397 r#"
398 dialect = "sqlite"
399 [[types.override]]
400 rust_type = "X"
401 [types.override.match]
402 db_type = "integer"
403 nullable = false
404 "#,
405 )
406 .unwrap();
407 let yes = col("a", "INTEGER", false);
408 let no = col("a", "INTEGER", true);
409 let t = table("t", vec![yes.clone(), no.clone()]);
410 assert!(
411 resolve(Dialect::Sqlite, &cfg.types, &t, &yes)
412 .unwrap()
413 .overridden
414 );
415 assert!(
416 !resolve(Dialect::Sqlite, &cfg.types, &t, &no)
417 .unwrap()
418 .overridden
419 );
420 }
421}