Skip to main content

reddb_server/wire/postgres/
types.rs

1//! PostgreSQL type OID mapping (Phase 3.1 PG parity).
2//!
3//! PG clients identify column types by OID (see `pg_catalog.pg_type`).
4//! RedDB's `Value` enum is richer than PG's canonical set, so we collapse
5//! domain-specific variants (`Email`, `Phone`, `Money`, ...) onto their
6//! closest PG equivalent — TEXT, NUMERIC, etc. This keeps generic clients
7//! working; clients that need the fine-grained types call the native
8//! gRPC path.
9//!
10//! Reference: PostgreSQL source `src/include/catalog/pg_type_d.h`.
11
12use crate::storage::schema::Value;
13
14/// A subset of PG type OIDs that cover every case we need to encode.
15#[allow(dead_code)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PgOid {
18    Bool = 16,
19    Bytea = 17,
20    Int8 = 20,
21    Int2 = 21,
22    Int4 = 23,
23    Text = 25,
24    Oid = 26,
25    Json = 114,
26    Float4 = 700,
27    Float8 = 701,
28    Unknown = 705,
29    Varchar = 1043,
30    Date = 1082,
31    Time = 1083,
32    Timestamp = 1114,
33    TimestampTz = 1184,
34    Numeric = 1700,
35    Uuid = 2950,
36    Jsonb = 3802,
37}
38
39impl PgOid {
40    pub fn as_u32(self) -> u32 {
41        self as u32
42    }
43
44    /// Preferred type OID for a runtime `Value`. Used by
45    /// `RowDescription` to tell the client what each column is.
46    pub fn from_value(value: &Value) -> Self {
47        match value {
48            Value::Null => PgOid::Text,
49            Value::Boolean(_) => PgOid::Bool,
50            Value::Integer(_) => PgOid::Int8,
51            Value::UnsignedInteger(_) => PgOid::Int8,
52            Value::BigInt(_) => PgOid::Int8,
53            Value::Float(_) => PgOid::Float8,
54            Value::Text(_) => PgOid::Text,
55            Value::Blob(_) => PgOid::Bytea,
56            Value::Json(_) => PgOid::Jsonb,
57            Value::Uuid(_) => PgOid::Uuid,
58            Value::Date(_) => PgOid::Date,
59            Value::Timestamp(_) => PgOid::TimestampTz,
60            Value::TimestampMs(_) => PgOid::TimestampTz,
61            // Domain / richer types collapse to TEXT so psql can render them.
62            _ => PgOid::Text,
63        }
64    }
65}
66
67/// Encode a `Value` as the UTF-8 text representation PG's text-mode
68/// protocol expects. Binary format is opt-in via a flag in the client's
69/// `Bind` message — we don't advertise binary support yet, so simple
70/// text encoding is sufficient for every supported client.
71///
72/// Returns `None` for `Value::Null` (the caller emits a `-1` length).
73pub fn value_to_pg_wire_bytes(value: &Value) -> Option<Vec<u8>> {
74    match value {
75        Value::Null => None,
76        Value::Boolean(b) => Some((if *b { "t" } else { "f" }).as_bytes().to_vec()),
77        Value::Integer(n) => Some(n.to_string().into_bytes()),
78        Value::UnsignedInteger(n) => Some(n.to_string().into_bytes()),
79        Value::BigInt(n) => Some(n.to_string().into_bytes()),
80        Value::Float(f) => Some(f.to_string().into_bytes()),
81        Value::Text(s) => Some(s.as_bytes().to_vec()),
82        Value::Blob(b) => {
83            // PG bytea text format: `\xHEX...`. Two chars per byte.
84            let mut out = Vec::with_capacity(2 + b.len() * 2);
85            out.extend_from_slice(b"\\x");
86            for byte in b {
87                out.extend_from_slice(format!("{byte:02x}").as_bytes());
88            }
89            Some(out)
90        }
91        Value::Json(bytes) => Some(bytes.clone()),
92        // Everything else renders via Display — catches Uuid, Date,
93        // Timestamp, Email, Phone, Money, GeoPoint, etc. PG clients see
94        // these as TEXT columns (OID 25) and can render them verbatim.
95        other => Some(other.to_string().into_bytes()),
96    }
97}