qail_pg/types/
mod.rs

1//! Type conversion traits and implementations for PostgreSQL types.
2//!
3//! This module provides traits for converting Rust types to/from PostgreSQL wire format.
4
5pub mod numeric;
6pub mod temporal;
7
8pub use numeric::Numeric;
9pub use temporal::{Date, Time, Timestamp};
10
11use crate::protocol::types::{decode_json, decode_jsonb, decode_text_array, decode_uuid, oid};
12
13/// Error type for type conversion failures.
14#[derive(Debug, Clone)]
15pub enum TypeError {
16    /// Wrong OID for expected type
17    UnexpectedOid { expected: &'static str, got: u32 },
18    /// Invalid binary data
19    InvalidData(String),
20    /// Null value where non-null expected
21    UnexpectedNull,
22}
23
24impl std::fmt::Display for TypeError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            TypeError::UnexpectedOid { expected, got } => {
28                write!(f, "Expected {} type, got OID {}", expected, got)
29            }
30            TypeError::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
31            TypeError::UnexpectedNull => write!(f, "Unexpected NULL value"),
32        }
33    }
34}
35
36impl std::error::Error for TypeError {}
37
38/// Trait for converting PostgreSQL binary/text data to Rust types.
39pub trait FromPg: Sized {
40    /// Convert from PostgreSQL wire format.
41    /// # Arguments
42    /// * `bytes` - Raw bytes from PostgreSQL (may be text or binary format)
43    /// * `oid` - PostgreSQL type OID
44    /// * `format` - 0 = text, 1 = binary
45    fn from_pg(bytes: &[u8], oid: u32, format: i16) -> Result<Self, TypeError>;
46}
47
48/// Trait for converting Rust types to PostgreSQL wire format.
49pub trait ToPg {
50    /// Convert to PostgreSQL wire format.
51    /// Returns (bytes, oid, format_code)
52    fn to_pg(&self) -> (Vec<u8>, u32, i16);
53}
54
55// ==================== String Types ====================
56
57impl FromPg for String {
58    fn from_pg(bytes: &[u8], _oid: u32, _format: i16) -> Result<Self, TypeError> {
59        String::from_utf8(bytes.to_vec())
60            .map_err(|e| TypeError::InvalidData(format!("Invalid UTF-8: {}", e)))
61    }
62}
63
64impl ToPg for String {
65    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
66        (self.as_bytes().to_vec(), oid::TEXT, 0)
67    }
68}
69
70impl ToPg for &str {
71    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
72        (self.as_bytes().to_vec(), oid::TEXT, 0)
73    }
74}
75
76// ==================== Integer Types ====================
77
78impl FromPg for i32 {
79    fn from_pg(bytes: &[u8], _oid: u32, format: i16) -> Result<Self, TypeError> {
80        if format == 1 {
81            // Binary format: 4 bytes big-endian
82            if bytes.len() != 4 {
83                return Err(TypeError::InvalidData(
84                    "Expected 4 bytes for i32".to_string(),
85                ));
86            }
87            Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
88        } else {
89            // Text format
90            std::str::from_utf8(bytes)
91                .map_err(|e| TypeError::InvalidData(e.to_string()))?
92                .parse()
93                .map_err(|e| TypeError::InvalidData(format!("Invalid i32: {}", e)))
94        }
95    }
96}
97
98impl ToPg for i32 {
99    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
100        (self.to_be_bytes().to_vec(), oid::INT4, 1)
101    }
102}
103
104impl FromPg for i64 {
105    fn from_pg(bytes: &[u8], _oid: u32, format: i16) -> Result<Self, TypeError> {
106        if format == 1 {
107            // Binary format: 8 bytes big-endian
108            if bytes.len() != 8 {
109                return Err(TypeError::InvalidData(
110                    "Expected 8 bytes for i64".to_string(),
111                ));
112            }
113            Ok(i64::from_be_bytes([
114                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
115            ]))
116        } else {
117            // Text format
118            std::str::from_utf8(bytes)
119                .map_err(|e| TypeError::InvalidData(e.to_string()))?
120                .parse()
121                .map_err(|e| TypeError::InvalidData(format!("Invalid i64: {}", e)))
122        }
123    }
124}
125
126impl ToPg for i64 {
127    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
128        (self.to_be_bytes().to_vec(), oid::INT8, 1)
129    }
130}
131
132// ==================== Float Types ====================
133
134impl FromPg for f64 {
135    fn from_pg(bytes: &[u8], _oid: u32, format: i16) -> Result<Self, TypeError> {
136        if format == 1 {
137            // Binary format: 8 bytes IEEE 754
138            if bytes.len() != 8 {
139                return Err(TypeError::InvalidData(
140                    "Expected 8 bytes for f64".to_string(),
141                ));
142            }
143            Ok(f64::from_be_bytes([
144                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
145            ]))
146        } else {
147            // Text format
148            std::str::from_utf8(bytes)
149                .map_err(|e| TypeError::InvalidData(e.to_string()))?
150                .parse()
151                .map_err(|e| TypeError::InvalidData(format!("Invalid f64: {}", e)))
152        }
153    }
154}
155
156impl ToPg for f64 {
157    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
158        (self.to_be_bytes().to_vec(), oid::FLOAT8, 1)
159    }
160}
161
162// ==================== Boolean ====================
163
164impl FromPg for bool {
165    fn from_pg(bytes: &[u8], _oid: u32, format: i16) -> Result<Self, TypeError> {
166        if format == 1 {
167            // Binary: 1 byte, 0 or 1
168            Ok(bytes.first().map(|b| *b != 0).unwrap_or(false))
169        } else {
170            // Text: 't' or 'f'
171            match bytes.first() {
172                Some(b't') | Some(b'T') | Some(b'1') => Ok(true),
173                Some(b'f') | Some(b'F') | Some(b'0') => Ok(false),
174                _ => Err(TypeError::InvalidData("Invalid boolean".to_string())),
175            }
176        }
177    }
178}
179
180impl ToPg for bool {
181    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
182        (vec![if *self { 1 } else { 0 }], oid::BOOL, 1)
183    }
184}
185
186// ==================== UUID ====================
187
188/// UUID type (uses String internally for simplicity)
189#[derive(Debug, Clone, PartialEq)]
190pub struct Uuid(pub String);
191
192impl FromPg for Uuid {
193    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
194        if oid_val != oid::UUID {
195            return Err(TypeError::UnexpectedOid {
196                expected: "uuid",
197                got: oid_val,
198            });
199        }
200
201        if format == 1 && bytes.len() == 16 {
202            // Binary format: 16 bytes
203            decode_uuid(bytes).map(Uuid).map_err(TypeError::InvalidData)
204        } else {
205            // Text format
206            String::from_utf8(bytes.to_vec())
207                .map(Uuid)
208                .map_err(|e| TypeError::InvalidData(e.to_string()))
209        }
210    }
211}
212
213impl ToPg for Uuid {
214    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
215        // Send as text for simplicity
216        (self.0.as_bytes().to_vec(), oid::UUID, 0)
217    }
218}
219
220// ==================== JSON/JSONB ====================
221
222/// JSON value (wraps the raw JSON string)
223#[derive(Debug, Clone, PartialEq)]
224pub struct Json(pub String);
225
226impl FromPg for Json {
227    fn from_pg(bytes: &[u8], oid_val: u32, _format: i16) -> Result<Self, TypeError> {
228        let json_str = if oid_val == oid::JSONB {
229            decode_jsonb(bytes).map_err(TypeError::InvalidData)?
230        } else {
231            decode_json(bytes).map_err(TypeError::InvalidData)?
232        };
233        Ok(Json(json_str))
234    }
235}
236
237impl ToPg for Json {
238    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
239        // Send as JSONB with version byte
240        let mut buf = Vec::with_capacity(1 + self.0.len());
241        buf.push(1); // JSONB version
242        buf.extend_from_slice(self.0.as_bytes());
243        (buf, oid::JSONB, 1)
244    }
245}
246
247// ==================== Arrays ====================
248
249impl FromPg for Vec<String> {
250    fn from_pg(bytes: &[u8], _oid: u32, _format: i16) -> Result<Self, TypeError> {
251        let s = std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
252        Ok(decode_text_array(s))
253    }
254}
255
256impl FromPg for Vec<i64> {
257    fn from_pg(bytes: &[u8], _oid: u32, _format: i16) -> Result<Self, TypeError> {
258        let s = std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
259        crate::protocol::types::decode_int_array(s).map_err(TypeError::InvalidData)
260    }
261}
262
263// ==================== Option<T> ====================
264
265impl<T: FromPg> FromPg for Option<T> {
266    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
267        // This is for non-null; actual NULL handling is done at row level
268        Ok(Some(T::from_pg(bytes, oid_val, format)?))
269    }
270}
271
272// ==================== Bytes ====================
273
274impl FromPg for Vec<u8> {
275    fn from_pg(bytes: &[u8], _oid: u32, _format: i16) -> Result<Self, TypeError> {
276        Ok(bytes.to_vec())
277    }
278}
279
280impl ToPg for Vec<u8> {
281    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
282        (self.clone(), oid::BYTEA, 1)
283    }
284}
285
286impl ToPg for &[u8] {
287    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
288        (self.to_vec(), oid::BYTEA, 1)
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_string_from_pg() {
298        let result = String::from_pg(b"hello", oid::TEXT, 0).unwrap();
299        assert_eq!(result, "hello");
300    }
301
302    #[test]
303    fn test_i32_from_pg_text() {
304        let result = i32::from_pg(b"42", oid::INT4, 0).unwrap();
305        assert_eq!(result, 42);
306    }
307
308    #[test]
309    fn test_i32_from_pg_binary() {
310        let bytes = 42i32.to_be_bytes();
311        let result = i32::from_pg(&bytes, oid::INT4, 1).unwrap();
312        assert_eq!(result, 42);
313    }
314
315    #[test]
316    fn test_bool_from_pg() {
317        assert!(bool::from_pg(b"t", oid::BOOL, 0).unwrap());
318        assert!(!bool::from_pg(b"f", oid::BOOL, 0).unwrap());
319        assert!(bool::from_pg(&[1], oid::BOOL, 1).unwrap());
320        assert!(!bool::from_pg(&[0], oid::BOOL, 1).unwrap());
321    }
322
323    #[test]
324    fn test_uuid_from_pg_binary() {
325        let uuid_bytes: [u8; 16] = [
326            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
327            0x00, 0x00,
328        ];
329        let result = Uuid::from_pg(&uuid_bytes, oid::UUID, 1).unwrap();
330        assert_eq!(result.0, "550e8400-e29b-41d4-a716-446655440000");
331    }
332}