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