1use crate::config::TypeOverride;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone)]
5pub struct ResolvedType {
6 pub rust_type: String,
7 pub copy_cheap: bool,
8}
9
10impl ResolvedType {
11 fn new(rust_type: impl Into<String>, copy_cheap: bool) -> Self {
12 Self {
13 rust_type: rust_type.into(),
14 copy_cheap,
15 }
16 }
17}
18
19pub struct TypeMap {
20 defaults: HashMap<&'static str, (&'static str, bool)>,
21 type_overrides: HashMap<String, ResolvedType>,
22 custom_types: HashMap<String, ResolvedType>,
23}
24
25impl TypeMap {
26 pub fn new(overrides: &[TypeOverride], copy_cheap_types: &[String]) -> Self {
27 let mut defaults: HashMap<&'static str, (&'static str, bool)> = HashMap::new();
28
29 for n in ["bool", "boolean", "pg_catalog.bool"] {
31 defaults.insert(n, ("bool", true));
32 }
33
34 for n in [
36 "int2",
37 "smallint",
38 "pg_catalog.int2",
39 "smallserial",
40 "serial2",
41 "pg_catalog.serial2",
42 ] {
43 defaults.insert(n, ("i16", true));
44 }
45 for n in [
46 "int4",
47 "integer",
48 "int",
49 "pg_catalog.int4",
50 "serial",
51 "serial4",
52 "pg_catalog.serial4",
53 ] {
54 defaults.insert(n, ("i32", true));
55 }
56 for n in [
57 "int8",
58 "bigint",
59 "pg_catalog.int8",
60 "bigserial",
61 "serial8",
62 "pg_catalog.serial8",
63 ] {
64 defaults.insert(n, ("i64", true));
65 }
66
67 for n in ["float4", "real", "pg_catalog.float4"] {
69 defaults.insert(n, ("f32", true));
70 }
71 for n in ["float8", "float", "double precision", "pg_catalog.float8"] {
72 defaults.insert(n, ("f64", true));
73 }
74
75 for n in ["numeric", "decimal", "pg_catalog.numeric"] {
77 defaults.insert(n, ("bigdecimal::BigDecimal", false));
78 }
79
80 for n in [
82 "text",
83 "varchar",
84 "pg_catalog.varchar",
85 "pg_catalog.bpchar",
86 "bpchar",
87 "string",
88 "citext",
89 "name",
90 "pg_catalog.name",
91 ] {
92 defaults.insert(n, ("String", false));
93 }
94
95 for n in ["bytea", "blob", "pg_catalog.bytea"] {
97 defaults.insert(n, ("Vec<u8>", false));
98 }
99
100 defaults.insert("uuid", ("uuid::Uuid", true));
102
103 for n in ["json", "jsonb"] {
105 defaults.insert(n, ("serde_json::Value", false));
106 }
107
108 for n in [
110 "timestamptz",
111 "pg_catalog.timestamptz",
112 "timestamp with time zone",
113 ] {
114 defaults.insert(n, ("chrono::DateTime<chrono::Utc>", false));
115 }
116 for n in [
117 "timestamp",
118 "pg_catalog.timestamp",
119 "timestamp without time zone",
120 ] {
121 defaults.insert(n, ("chrono::NaiveDateTime", false));
122 }
123 defaults.insert("date", ("chrono::NaiveDate", true));
124 for n in ["time", "pg_catalog.time", "time without time zone"] {
125 defaults.insert(n, ("chrono::NaiveTime", false));
126 }
127
128 for n in ["inet", "cidr"] {
130 defaults.insert(n, ("ipnetwork::IpNetwork", false));
131 }
132 defaults.insert("macaddr", ("mac_address::MacAddress", true));
133
134 defaults.insert(
136 "hstore",
137 ("std::collections::HashMap<String, Option<String>>", false),
138 );
139 for n in ["interval", "pg_catalog.interval"] {
140 defaults.insert(n, ("sqlx::postgres::types::PgInterval", false));
141 }
142 defaults.insert("money", ("sqlx::postgres::types::PgMoney", true));
143 defaults.insert("oid", ("sqlx::postgres::types::Oid", true));
144 defaults.insert("pg_catalog.oid", ("sqlx::postgres::types::Oid", true));
145 for n in ["ltree", "lquery"] {
146 defaults.insert(n, ("String", false));
147 }
148
149 for n in ["int4range", "pg_catalog.int4range"] {
151 defaults.insert(n, ("sqlx::postgres::types::PgRange<i32>", false));
152 }
153 for n in ["int8range", "pg_catalog.int8range"] {
154 defaults.insert(n, ("sqlx::postgres::types::PgRange<i64>", false));
155 }
156 for n in ["numrange", "pg_catalog.numrange"] {
157 defaults.insert(
158 n,
159 (
160 "sqlx::postgres::types::PgRange<bigdecimal::BigDecimal>",
161 false,
162 ),
163 );
164 }
165 for n in ["tsrange", "pg_catalog.tsrange"] {
166 defaults.insert(
167 n,
168 (
169 "sqlx::postgres::types::PgRange<chrono::NaiveDateTime>",
170 false,
171 ),
172 );
173 }
174 for n in ["tstzrange", "pg_catalog.tstzrange"] {
175 defaults.insert(
176 n,
177 (
178 "sqlx::postgres::types::PgRange<chrono::DateTime<chrono::Utc>>",
179 false,
180 ),
181 );
182 }
183 for n in ["daterange", "pg_catalog.daterange"] {
184 defaults.insert(
185 n,
186 ("sqlx::postgres::types::PgRange<chrono::NaiveDate>", false),
187 );
188 }
189
190 for n in ["bit", "varbit", "pg_catalog.varbit"] {
192 defaults.insert(n, ("bit_vec::BitVec", false));
193 }
194
195 let mut type_overrides = HashMap::new();
196 for o in overrides {
197 if let Some(db_type) = &o.db_type {
198 type_overrides.insert(
199 db_type.to_lowercase(),
200 ResolvedType::new(o.rs_type.clone(), o.copy_cheap),
201 );
202 }
203 }
204
205 for name in copy_cheap_types {
206 let key = name.to_lowercase();
207 if let Some(ovr) = type_overrides.get_mut(&key) {
208 ovr.copy_cheap = true;
209 } else if let Some(&(ty, _)) = defaults.get(key.as_str()) {
210 type_overrides.insert(key, ResolvedType::new(ty.to_string(), true));
211 }
212 }
213
214 Self {
215 defaults,
216 type_overrides,
217 custom_types: HashMap::new(),
218 }
219 }
220
221 pub fn register(&mut self, pg_name: &str, rust_name: &str, copy_cheap: bool) {
225 self.custom_types.insert(
226 pg_name.to_lowercase(),
227 ResolvedType::new(rust_name.to_string(), copy_cheap),
228 );
229 }
230
231 pub fn resolve_pg_type(
232 &self,
233 pg_type: &str,
234 nullable: bool,
235 is_array: bool,
236 ) -> Option<ResolvedType> {
237 self.resolve_pg_type_dims(pg_type, nullable, usize::from(is_array))
238 }
239
240 pub fn resolve_pg_type_dims(
241 &self,
242 pg_type: &str,
243 nullable: bool,
244 array_dims: usize,
245 ) -> Option<ResolvedType> {
246 let key = pg_type.to_lowercase();
247 let (inner, copy_cheap) = if let Some(ovr) = self.type_overrides.get(&key) {
248 (ovr.rust_type.clone(), ovr.copy_cheap)
249 } else if let Some(&(ty, cc)) = self.defaults.get(key.as_str()) {
250 (ty.to_string(), cc)
251 } else if let Some(custom) = self.custom_types.get(&key) {
252 (custom.rust_type.clone(), custom.copy_cheap)
253 } else {
254 return None;
255 };
256
257 let rust_type = wrap_type(&inner, nullable, array_dims);
258 let effective_copy_cheap = copy_cheap && !nullable && array_dims == 0;
259 Some(ResolvedType {
260 rust_type,
261 copy_cheap: effective_copy_cheap,
262 })
263 }
264
265 pub fn resolve_column(
266 &self,
267 pg_type: &str,
268 nullable: bool,
269 is_array: bool,
270 column_key: Option<&str>,
271 column_overrides: &HashMap<String, ResolvedType>,
272 ) -> Option<ResolvedType> {
273 self.resolve_column_dims(
274 pg_type,
275 nullable,
276 usize::from(is_array),
277 column_key,
278 column_overrides,
279 )
280 }
281
282 pub fn resolve_column_dims(
283 &self,
284 pg_type: &str,
285 nullable: bool,
286 array_dims: usize,
287 column_key: Option<&str>,
288 column_overrides: &HashMap<String, ResolvedType>,
289 ) -> Option<ResolvedType> {
290 if let Some(key) = column_key
291 && let Some(ovr) = column_overrides.get(key)
292 {
293 let rust_type = wrap_type(&ovr.rust_type, nullable, array_dims);
294 let cc = ovr.copy_cheap && !nullable && array_dims == 0;
295 return Some(ResolvedType {
296 rust_type,
297 copy_cheap: cc,
298 });
299 }
300 self.resolve_pg_type_dims(pg_type, nullable, array_dims)
301 }
302}
303
304fn wrap_type(inner: &str, nullable: bool, array_dims: usize) -> String {
305 let mut t = inner.to_string();
306 for _ in 0..array_dims {
307 t = format!("Vec<{t}>");
308 }
309 if nullable { format!("Option<{t}>") } else { t }
310}
311
312pub fn build_column_overrides(overrides: &[TypeOverride]) -> HashMap<String, ResolvedType> {
313 overrides
314 .iter()
315 .filter_map(|o| {
316 o.column.as_ref().map(|col| {
317 (
318 col.clone(),
319 ResolvedType::new(o.rs_type.clone(), o.copy_cheap),
320 )
321 })
322 })
323 .collect()
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn map() -> TypeMap {
331 TypeMap::new(&[], &[])
332 }
333
334 #[test]
335 fn maps_text() {
336 let t = map().resolve_pg_type("text", false, false).unwrap();
337 assert_eq!(t.rust_type, "String");
338 assert!(!t.copy_cheap);
339 }
340 #[test]
341 fn maps_int4_copy_cheap() {
342 let t = map().resolve_pg_type("int4", false, false).unwrap();
343 assert_eq!(t.rust_type, "i32");
344 assert!(t.copy_cheap);
345 }
346 #[test]
347 fn maps_bool() {
348 let t = map().resolve_pg_type("bool", false, false).unwrap();
349 assert_eq!(t.rust_type, "bool");
350 assert!(t.copy_cheap);
351 }
352 #[test]
353 fn maps_timestamptz() {
354 let t = map().resolve_pg_type("timestamptz", false, false).unwrap();
355 assert_eq!(t.rust_type, "chrono::DateTime<chrono::Utc>");
356 }
357 #[test]
358 fn maps_uuid() {
359 let t = map().resolve_pg_type("uuid", false, false).unwrap();
360 assert_eq!(t.rust_type, "uuid::Uuid");
361 assert!(t.copy_cheap);
362 }
363 #[test]
364 fn maps_jsonb() {
365 let t = map().resolve_pg_type("jsonb", false, false).unwrap();
366 assert_eq!(t.rust_type, "serde_json::Value");
367 }
368 #[test]
369 fn nullable_wraps_option() {
370 let t = map().resolve_pg_type("text", true, false).unwrap();
371 assert_eq!(t.rust_type, "Option<String>");
372 assert!(!t.copy_cheap);
373 }
374 #[test]
375 fn array_wraps_vec() {
376 let t = map().resolve_pg_type("text", false, true).unwrap();
377 assert_eq!(t.rust_type, "Vec<String>");
378 assert!(!t.copy_cheap);
379 }
380 #[test]
381 fn nullable_array() {
382 let t = map().resolve_pg_type("text", true, true).unwrap();
383 assert_eq!(t.rust_type, "Option<Vec<String>>");
384 }
385 #[test]
386 fn multidimensional_array_wraps_nested_vec() {
387 let t = map().resolve_pg_type_dims("int8", false, 2).unwrap();
388 assert_eq!(t.rust_type, "Vec<Vec<i64>>");
389 }
390 #[test]
391 fn nullable_multidimensional_array_wraps_option_nested_vec() {
392 let t = map().resolve_pg_type_dims("text", true, 3).unwrap();
393 assert_eq!(t.rust_type, "Option<Vec<Vec<Vec<String>>>>");
394 }
395 #[test]
396 fn type_override_replaces_default() {
397 use crate::config::TypeOverride;
398 let ovr = TypeOverride {
399 db_type: Some("timestamptz".to_string()),
400 column: None,
401 rs_type: "time::OffsetDateTime".to_string(),
402 copy_cheap: false,
403 };
404 let t = TypeMap::new(&[ovr], &[])
405 .resolve_pg_type("timestamptz", false, false)
406 .unwrap();
407 assert_eq!(t.rust_type, "time::OffsetDateTime");
408 }
409 #[test]
410 fn column_override_beats_type_override() {
411 use crate::config::TypeOverride;
412 let overrides = vec![
413 TypeOverride {
414 db_type: Some("text".to_string()),
415 column: None,
416 rs_type: "TypeLevel".to_string(),
417 copy_cheap: false,
418 },
419 TypeOverride {
420 db_type: None,
421 column: Some("users.name".to_string()),
422 rs_type: "ColumnLevel".to_string(),
423 copy_cheap: false,
424 },
425 ];
426 let col_ovrs = build_column_overrides(&overrides);
427 let map = TypeMap::new(&overrides, &[]);
428 let t = map
429 .resolve_column("text", false, false, Some("users.name"), &col_ovrs)
430 .unwrap();
431 assert_eq!(t.rust_type, "ColumnLevel");
432 }
433 #[test]
434 fn maps_numeric() {
435 let t = map().resolve_pg_type("numeric", false, false).unwrap();
436 assert_eq!(t.rust_type, "bigdecimal::BigDecimal");
437 assert!(!t.copy_cheap);
438 }
439 #[test]
440 fn maps_decimal() {
441 let t = map().resolve_pg_type("decimal", false, false).unwrap();
442 assert_eq!(t.rust_type, "bigdecimal::BigDecimal");
443 }
444 #[test]
445 fn maps_pg_catalog_numeric() {
446 let t = map()
447 .resolve_pg_type("pg_catalog.numeric", false, false)
448 .unwrap();
449 assert_eq!(t.rust_type, "bigdecimal::BigDecimal");
450 }
451 #[test]
452 fn unknown_type_returns_none() {
453 assert!(
454 map()
455 .resolve_pg_type("no_such_type", false, false)
456 .is_none()
457 );
458 }
459 #[test]
460 fn registers_custom_type() {
461 let mut map = TypeMap::new(&[], &[]);
462 map.register("my_enum", "MyEnum", false);
463 let t = map.resolve_pg_type("my_enum", false, false).unwrap();
464 assert_eq!(t.rust_type, "MyEnum");
465 assert!(!t.copy_cheap);
466 }
467 #[test]
468 fn registered_type_nullable() {
469 let mut map = TypeMap::new(&[], &[]);
470 map.register("my_enum", "MyEnum", false);
471 let t = map.resolve_pg_type("my_enum", true, false).unwrap();
472 assert_eq!(t.rust_type, "Option<MyEnum>");
473 assert!(!t.copy_cheap);
474 }
475 #[test]
476 fn type_override_beats_registered_custom() {
477 use crate::config::TypeOverride;
478 let ovr = TypeOverride {
479 db_type: Some("my_enum".to_string()),
480 column: None,
481 rs_type: "Override".to_string(),
482 copy_cheap: false,
483 };
484 let mut map = TypeMap::new(&[ovr], &[]);
485 map.register("my_enum", "MyEnum", false);
486 let t = map.resolve_pg_type("my_enum", false, false).unwrap();
487 assert_eq!(t.rust_type, "Override");
489 }
490 #[test]
491 fn registered_copy_cheap_type_is_cheap() {
492 let mut map = TypeMap::new(&[], &[]);
493 map.register("my_value_type", "MyValueType", true);
494 let t = map.resolve_pg_type("my_value_type", false, false).unwrap();
495 assert_eq!(t.rust_type, "MyValueType");
496 assert!(
497 t.copy_cheap,
498 "registered type with copy_cheap=true should resolve as copy_cheap"
499 );
500 }
501
502 #[test]
503 fn registered_copy_cheap_nullable_is_not_cheap() {
504 let mut map = TypeMap::new(&[], &[]);
505 map.register("my_value_type", "MyValueType", true);
506 let t = map.resolve_pg_type("my_value_type", true, false).unwrap();
507 assert_eq!(t.rust_type, "Option<MyValueType>");
508 assert!(
509 !t.copy_cheap,
510 "nullable type should not be copy_cheap even if base is"
511 );
512 }
513
514 #[test]
515 fn registered_copy_cheap_array_is_not_cheap() {
516 let mut map = TypeMap::new(&[], &[]);
517 map.register("my_value_type", "MyValueType", true);
518 let t = map.resolve_pg_type("my_value_type", false, true).unwrap();
519 assert_eq!(t.rust_type, "Vec<MyValueType>");
520 assert!(
521 !t.copy_cheap,
522 "array type should not be copy_cheap even if base is"
523 );
524 }
525
526 #[test]
527 fn copy_cheap_types_marks_type_as_cheap() {
528 let map = TypeMap::new(&[], &["text".to_string()]);
529 let t = map.resolve_pg_type("text", false, false).unwrap();
531 assert_eq!(t.rust_type, "String");
532 assert!(
533 t.copy_cheap,
534 "text should be copy_cheap after config promotion"
535 );
536 }
537
538 #[test]
539 fn copy_cheap_types_promotes_default_to_override() {
540 let map = TypeMap::new(&[], &["uuid".to_string()]);
542 let t = map.resolve_pg_type("uuid", false, false).unwrap();
543 assert!(t.copy_cheap);
544 }
545}