Skip to main content

sqlc_gen_sqlx/
types.rs

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        // Boolean
30        for n in ["bool", "boolean", "pg_catalog.bool"] {
31            defaults.insert(n, ("bool", true));
32        }
33
34        // Integer types
35        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        // Float types
68        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        // Decimal / numeric types
76        for n in ["numeric", "decimal", "pg_catalog.numeric"] {
77            defaults.insert(n, ("bigdecimal::BigDecimal", false));
78        }
79
80        // String types
81        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        // Bytes
96        for n in ["bytea", "blob", "pg_catalog.bytea"] {
97            defaults.insert(n, ("Vec<u8>", false));
98        }
99
100        // UUID
101        defaults.insert("uuid", ("uuid::Uuid", true));
102
103        // JSON
104        for n in ["json", "jsonb"] {
105            defaults.insert(n, ("serde_json::Value", false));
106        }
107
108        // Timestamps
109        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        // Network
129        for n in ["inet", "cidr"] {
130            defaults.insert(n, ("ipnetwork::IpNetwork", false));
131        }
132        defaults.insert("macaddr", ("mac_address::MacAddress", true));
133
134        // Misc
135        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        // Range types
150        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        // Bit types
191        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    /// Register a custom type (enum or composite) discovered from the catalog.
222    /// Registered types are checked after both `type_overrides` and `defaults`,
223    /// so user-level overrides and built-in defaults always take precedence.
224    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        // type_overrides must win over custom_types
488        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        // text is normally not copy_cheap (false in defaults)
530        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        // uuid is already copy_cheap in defaults; listing it in copy_cheap_types is a no-op behavior-wise
541        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}