rustlavel_db/postgres/
types.rs1use crate::value::Value;
4use rustlavel_core::Json;
5
6pub 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
30pub 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
51fn 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
66pub 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}