Skip to main content

rustlavel_db/postgres/
types.rs

1//! Decoding column text into [`Value`], guided by the column's type OID.
2
3use crate::value::Value;
4use rustlavel_core::Json;
5
6// The OIDs the driver recognises. They are stable constants in PostgreSQL's
7// catalog, which is why hard-coding them is safe.
8pub const BOOL: i32 = 16;
9pub const BYTEA: i32 = 17;
10pub const CHAR: i32 = 18;
11pub const NAME: i32 = 19;
12pub const INT8: i32 = 20;
13pub const INT2: i32 = 21;
14pub const INT4: i32 = 23;
15pub const TEXT: i32 = 25;
16pub const OID: i32 = 26;
17pub const JSON: i32 = 114;
18pub const FLOAT4: i32 = 700;
19pub const FLOAT8: i32 = 701;
20pub const BPCHAR: i32 = 1042;
21pub const VARCHAR: i32 = 1043;
22pub const DATE: i32 = 1082;
23pub const TIME: i32 = 1083;
24pub const TIMESTAMP: i32 = 1114;
25pub const TIMESTAMPTZ: i32 = 1184;
26pub const NUMERIC: i32 = 1700;
27pub const UUID: i32 = 2950;
28pub const JSONB: i32 = 3802;
29
30/// Turn a column's text representation into a [`Value`].
31///
32/// Timestamps, dates, numerics and uuids stay as text: the framework has no
33/// date type of its own yet, and silently converting a `numeric` to `f64` would
34/// lose the precision the column exists to preserve.
35pub fn decode(type_oid: i32, raw: Option<&[u8]>) -> Value {
36    let Some(bytes) = raw else { return Value::Null };
37    let text = String::from_utf8_lossy(bytes);
38
39    match type_oid {
40        BOOL => Value::Bool(text == "t"),
41        INT2 | INT4 | INT8 | OID => text.parse::<i64>().map_or(Value::Text(text.into_owned()), Value::Int),
42        FLOAT4 | FLOAT8 => {
43            text.parse::<f64>().map_or(Value::Text(text.into_owned()), Value::Float)
44        }
45        JSON | JSONB => Json::parse(&text).map_or(Value::Text(text.into_owned()), Value::Json),
46        BYTEA => Value::Bytes(decode_bytea(&text)),
47        _ => Value::Text(text.into_owned()),
48    }
49}
50
51/// PostgreSQL sends `bytea` in text mode as `\xdeadbeef`.
52fn decode_bytea(text: &str) -> Vec<u8> {
53    let Some(hex) = text.strip_prefix("\\x") else {
54        return text.as_bytes().to_vec();
55    };
56
57    hex.as_bytes()
58        .chunks(2)
59        .filter_map(|pair| {
60            let text = std::str::from_utf8(pair).ok()?;
61            u8::from_str_radix(text, 16).ok()
62        })
63        .collect()
64}
65
66/// The SQL type a schema builder emits for a logical column type.
67pub fn type_name(oid: i32) -> &'static str {
68    match oid {
69        BOOL => "boolean",
70        INT2 => "smallint",
71        INT4 => "integer",
72        INT8 => "bigint",
73        FLOAT4 => "real",
74        FLOAT8 => "double precision",
75        NUMERIC => "numeric",
76        TEXT => "text",
77        VARCHAR | BPCHAR | CHAR | NAME => "varchar",
78        DATE => "date",
79        TIME => "time",
80        TIMESTAMP => "timestamp",
81        TIMESTAMPTZ => "timestamptz",
82        UUID => "uuid",
83        JSON => "json",
84        JSONB => "jsonb",
85        BYTEA => "bytea",
86        _ => "unknown",
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn decodes_the_scalar_types() {
96        assert_eq!(decode(BOOL, Some(b"t")), Value::Bool(true));
97        assert_eq!(decode(BOOL, Some(b"f")), Value::Bool(false));
98        assert_eq!(decode(INT4, Some(b"42")), Value::Int(42));
99        assert_eq!(decode(INT8, Some(b"-7")), Value::Int(-7));
100        assert_eq!(decode(FLOAT8, Some(b"1.5")), Value::Float(1.5));
101        assert_eq!(decode(TEXT, Some(b"hello")), Value::Text("hello".into()));
102    }
103
104    #[test]
105    fn a_null_column_decodes_to_null_whatever_its_type() {
106        assert_eq!(decode(INT4, None), Value::Null);
107        assert_eq!(decode(TEXT, None), Value::Null);
108    }
109
110    #[test]
111    fn decodes_json_columns_into_parsed_values() {
112        let value = decode(JSONB, Some(br#"{"a":1}"#));
113        match value {
114            Value::Json(json) => assert_eq!(json.get("a").unwrap().as_i64(), Some(1)),
115            other => panic!("expected parsed JSON, got {other:?}"),
116        }
117    }
118
119    #[test]
120    fn decodes_hex_bytea() {
121        assert_eq!(decode(BYTEA, Some(b"\\xdead")), Value::Bytes(vec![0xde, 0xad]));
122    }
123
124    #[test]
125    fn numerics_and_timestamps_stay_text_so_precision_survives() {
126        assert_eq!(
127            decode(NUMERIC, Some(b"12345.678901234567890")),
128            Value::Text("12345.678901234567890".into())
129        );
130        assert_eq!(
131            decode(TIMESTAMPTZ, Some(b"2026-08-29 10:00:00+00")),
132            Value::Text("2026-08-29 10:00:00+00".into())
133        );
134    }
135}