Skip to main content

chopin_pg/
types.rs

1//! PostgreSQL type system — type OIDs, value conversions, and ToSql/FromSql traits.
2use crate::error::{PgError, PgResult};
3
4/// Well-known PostgreSQL type OIDs.
5pub mod oid {
6    pub const BOOL: u32 = 16;
7    pub const BYTEA: u32 = 17;
8    pub const CHAR: u32 = 18;
9    pub const INT8: u32 = 20;
10    pub const INT2: u32 = 21;
11    pub const INT4: u32 = 23;
12    pub const TEXT: u32 = 25;
13    pub const OID: u32 = 26;
14    pub const FLOAT4: u32 = 700;
15    pub const FLOAT8: u32 = 701;
16    pub const VARCHAR: u32 = 1043;
17    pub const DATE: u32 = 1082;
18    pub const TIME: u32 = 1083;
19    pub const TIMESTAMP: u32 = 1114;
20    pub const TIMESTAMPTZ: u32 = 1184;
21    pub const INTERVAL: u32 = 1186;
22    pub const NUMERIC: u32 = 1700;
23    pub const UUID: u32 = 2950;
24    pub const JSONB: u32 = 3802;
25    pub const JSON: u32 = 114;
26    pub const INET: u32 = 869;
27    pub const CIDR: u32 = 650;
28    pub const MACADDR: u32 = 829;
29    pub const MACADDR8: u32 = 774;
30    pub const POINT: u32 = 600;
31    pub const LINE: u32 = 628;
32    pub const LSEG: u32 = 601;
33    pub const BOX: u32 = 603;
34    pub const PATH: u32 = 602;
35    pub const POLYGON: u32 = 604;
36    pub const CIRCLE: u32 = 718;
37
38    // Array types
39    pub const BOOL_ARRAY: u32 = 1000;
40    pub const INT2_ARRAY: u32 = 1005;
41    pub const INT4_ARRAY: u32 = 1007;
42    pub const INT8_ARRAY: u32 = 1016;
43    pub const TEXT_ARRAY: u32 = 1009;
44    pub const FLOAT4_ARRAY: u32 = 1021;
45    pub const FLOAT8_ARRAY: u32 = 1022;
46    pub const VARCHAR_ARRAY: u32 = 1015;
47    pub const UUID_ARRAY: u32 = 2951;
48    pub const JSONB_ARRAY: u32 = 3807;
49    pub const JSON_ARRAY: u32 = 199;
50
51    // Bit string types
52    pub const BIT: u32 = 1560;
53    pub const VARBIT: u32 = 1562;
54
55    // Range types
56    pub const INT4RANGE: u32 = 3904;
57    pub const INT8RANGE: u32 = 3926;
58    pub const NUMRANGE: u32 = 3906;
59    pub const TSRANGE: u32 = 3908;
60    pub const TSTZRANGE: u32 = 3910;
61    pub const DATERANGE: u32 = 3912;
62}
63
64/// A PostgreSQL value that can be used as a query parameter or read from a row.
65#[derive(Debug, Clone, PartialEq)]
66pub enum PgValue {
67    Null,
68    Bool(bool),
69    Int2(i16),
70    Int4(i32),
71    Int8(i64),
72    Float4(f32),
73    Float8(f64),
74    Text(String),
75    Bytes(Vec<u8>),
76    Json(String),
77    Jsonb(Vec<u8>),
78    /// UUID stored as 16-byte array.
79    Uuid([u8; 16]),
80    /// Date: days since 2000-01-01 (PostgreSQL epoch).
81    Date(i32),
82    /// Time: microseconds since midnight.
83    Time(i64),
84    /// Timestamp: microseconds since 2000-01-01 00:00:00 (PostgreSQL epoch).
85    Timestamp(i64),
86    /// Timestamptz: microseconds since 2000-01-01 00:00:00 UTC.
87    Timestamptz(i64),
88    /// Interval: months, days, microseconds.
89    Interval {
90        months: i32,
91        days: i32,
92        microseconds: i64,
93    },
94    /// Network address (stored as text representation).
95    Inet(String),
96    /// Numeric (stored as text representation for lossless precision).
97    Numeric(String),
98    /// MAC address stored as 6 bytes.
99    MacAddr([u8; 6]),
100    /// 2D point: (x, y).
101    Point {
102        x: f64,
103        y: f64,
104    },
105    /// MAC address stored as 8 bytes (EUI-64).
106    MacAddr8([u8; 8]),
107    /// Bit string: number of bits + packed bytes.
108    Bit {
109        len: u32,
110        data: Vec<u8>,
111    },
112    /// Range value (stored as text representation).
113    /// Examples: `"[1,10)"`, `"[2024-01-01,2024-12-31]"`, `"empty"`.
114    Range(String),
115    /// Array of values (homogeneous).
116    Array(Vec<PgValue>),
117}
118
119impl PgValue {
120    /// Encode this value as text-format bytes for use as a query parameter.
121    pub fn to_text_bytes(&self) -> Option<Vec<u8>> {
122        match self {
123            PgValue::Null => None,
124            PgValue::Bool(b) => Some(if *b { b"t".to_vec() } else { b"f".to_vec() }),
125            PgValue::Int2(v) => Some(v.to_string().into_bytes()),
126            PgValue::Int4(v) => Some(v.to_string().into_bytes()),
127            PgValue::Int8(v) => Some(v.to_string().into_bytes()),
128            PgValue::Float4(v) => Some(v.to_string().into_bytes()),
129            PgValue::Float8(v) => Some(v.to_string().into_bytes()),
130            PgValue::Text(s) => Some(s.as_bytes().to_vec()),
131            PgValue::Bytes(b) => Some(b.clone()),
132            PgValue::Json(s) => Some(s.as_bytes().to_vec()),
133            PgValue::Jsonb(b) => Some(b.clone()),
134            PgValue::Uuid(bytes) => Some(format_uuid(bytes).into_bytes()),
135            PgValue::Date(days) => Some(format_date(*days).into_bytes()),
136            PgValue::Time(us) => Some(format_time(*us).into_bytes()),
137            PgValue::Timestamp(us) => Some(format_timestamp(*us).into_bytes()),
138            PgValue::Timestamptz(us) => Some(format_timestamp_tz(*us).into_bytes()),
139            PgValue::Interval {
140                months,
141                days,
142                microseconds,
143            } => Some(format_interval(*months, *days, *microseconds).into_bytes()),
144            PgValue::Inet(s) => Some(s.as_bytes().to_vec()),
145            PgValue::Numeric(s) => Some(s.as_bytes().to_vec()),
146            PgValue::MacAddr(bytes) => Some(
147                format!(
148                    "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
149                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
150                )
151                .into_bytes(),
152            ),
153            PgValue::Point { x, y } => Some(format!("({},{})", x, y).into_bytes()),
154            PgValue::MacAddr8(bytes) => Some(
155                format!(
156                    "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
157                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]
158                )
159                .into_bytes(),
160            ),
161            PgValue::Bit { len, data } => {
162                let mut s = String::with_capacity(*len as usize);
163                for i in 0..*len as usize {
164                    let byte_idx = i / 8;
165                    let bit_idx = 7 - (i % 8);
166                    if byte_idx < data.len() && (data[byte_idx] >> bit_idx) & 1 == 1 {
167                        s.push('1');
168                    } else {
169                        s.push('0');
170                    }
171                }
172                Some(s.into_bytes())
173            }
174            PgValue::Range(s) => Some(s.as_bytes().to_vec()),
175            PgValue::Array(values) => {
176                let inner: Vec<String> = values
177                    .iter()
178                    .map(|v| match v {
179                        PgValue::Null => "NULL".to_string(),
180                        _ => match v.to_text_bytes() {
181                            Some(b) => {
182                                let s = String::from_utf8_lossy(&b).to_string();
183                                escape_array_element(&s)
184                            }
185                            None => "NULL".to_string(),
186                        },
187                    })
188                    .collect();
189                Some(format!("{{{}}}", inner.join(",")).into_bytes())
190            }
191        }
192    }
193
194    /// Encode this value as binary-format bytes for use as a query parameter.
195    ///
196    /// Returns `None` for Null; `Some(bytes)` for everything else.
197    /// The binary format matches what PostgreSQL expects when the
198    /// parameter format code is 1 (binary).
199    pub fn to_binary_bytes(&self) -> Option<Vec<u8>> {
200        match self {
201            PgValue::Null => None,
202            PgValue::Bool(b) => Some(vec![if *b { 1 } else { 0 }]),
203            PgValue::Int2(v) => Some(v.to_be_bytes().to_vec()),
204            PgValue::Int4(v) => Some(v.to_be_bytes().to_vec()),
205            PgValue::Int8(v) => Some(v.to_be_bytes().to_vec()),
206            PgValue::Float4(v) => Some(v.to_be_bytes().to_vec()),
207            PgValue::Float8(v) => Some(v.to_be_bytes().to_vec()),
208            PgValue::Text(s) => Some(s.as_bytes().to_vec()),
209            PgValue::Bytes(b) => Some(b.clone()),
210            PgValue::Json(s) => Some(s.as_bytes().to_vec()),
211            PgValue::Jsonb(b) => {
212                // Prefix with version byte (1) for binary JSONB
213                let mut buf = Vec::with_capacity(1 + b.len());
214                buf.push(1);
215                buf.extend_from_slice(b);
216                Some(buf)
217            }
218            PgValue::Uuid(bytes) => Some(bytes.to_vec()),
219            PgValue::Date(days) => Some(days.to_be_bytes().to_vec()),
220            PgValue::Time(us) => Some(us.to_be_bytes().to_vec()),
221            PgValue::Timestamp(us) | PgValue::Timestamptz(us) => Some(us.to_be_bytes().to_vec()),
222            PgValue::Interval {
223                months,
224                days,
225                microseconds,
226            } => {
227                let mut buf = Vec::with_capacity(16);
228                buf.extend_from_slice(&microseconds.to_be_bytes());
229                buf.extend_from_slice(&days.to_be_bytes());
230                buf.extend_from_slice(&months.to_be_bytes());
231                Some(buf)
232            }
233            PgValue::Inet(s) => encode_inet_binary(s).ok(),
234            PgValue::Numeric(s) => {
235                // NUMERIC is complex in binary — fall back to text encoding
236                Some(s.as_bytes().to_vec())
237            }
238            PgValue::MacAddr(bytes) => Some(bytes.to_vec()),
239            PgValue::MacAddr8(bytes) => Some(bytes.to_vec()),
240            PgValue::Bit { len, data } => {
241                // Binary format: 4-byte bit length (big-endian) + packed bytes
242                let mut buf = Vec::with_capacity(4 + data.len());
243                buf.extend_from_slice(&(*len as i32).to_be_bytes());
244                buf.extend_from_slice(data);
245                Some(buf)
246            }
247            PgValue::Point { x, y } => {
248                let mut buf = Vec::with_capacity(16);
249                buf.extend_from_slice(&x.to_be_bytes());
250                buf.extend_from_slice(&y.to_be_bytes());
251                Some(buf)
252            }
253            PgValue::Range(s) => {
254                // Range binary encoding is complex — use text
255                Some(s.as_bytes().to_vec())
256            }
257            PgValue::Array(values) => {
258                // Use text array format for encoding — binary array encoding
259                // requires knowing the element OID which PgValue doesn't carry.
260                let inner: Vec<String> = values
261                    .iter()
262                    .map(|v| match v {
263                        PgValue::Null => "NULL".to_string(),
264                        _ => match v.to_text_bytes() {
265                            Some(b) => {
266                                let s = String::from_utf8_lossy(&b).to_string();
267                                escape_array_element(&s)
268                            }
269                            None => "NULL".to_string(),
270                        },
271                    })
272                    .collect();
273                Some(format!("{{{}}}", inner.join(",")).into_bytes())
274            }
275        }
276    }
277
278    /// Determine if this value should be sent as binary or text format.
279    ///
280    /// Returns `true` for types that have an efficient binary encoding
281    /// (scalars, dates, etc.), `false` for types best sent as text
282    /// (arrays, numeric, inet).
283    pub fn prefers_binary(&self) -> bool {
284        matches!(
285            self,
286            PgValue::Bool(_)
287                | PgValue::Int2(_)
288                | PgValue::Int4(_)
289                | PgValue::Int8(_)
290                | PgValue::Float4(_)
291                | PgValue::Float8(_)
292                | PgValue::Bytes(_)
293                | PgValue::Uuid(_)
294                | PgValue::Date(_)
295                | PgValue::Time(_)
296                | PgValue::Timestamp(_)
297                | PgValue::Timestamptz(_)
298                | PgValue::Interval { .. }
299                | PgValue::Jsonb(_)
300                | PgValue::MacAddr(_)
301                | PgValue::Point { .. }
302        )
303    }
304
305    /// Parse a text-format column value based on its type OID.
306    pub fn from_text(type_oid: u32, data: &[u8]) -> PgResult<Self> {
307        let s = std::str::from_utf8(data)
308            .map_err(|_| PgError::TypeConversion("Invalid UTF-8".to_string()))?;
309        match type_oid {
310            oid::BOOL => Ok(PgValue::Bool(s == "t" || s == "true" || s == "1")),
311            oid::INT2 => {
312                Ok(PgValue::Int2(s.parse().map_err(|_| {
313                    PgError::TypeConversion("Invalid INT2".to_string())
314                })?))
315            }
316            oid::INT4 | oid::OID => {
317                Ok(PgValue::Int4(s.parse().map_err(|_| {
318                    PgError::TypeConversion("Invalid INT4/OID".to_string())
319                })?))
320            }
321            oid::INT8 => {
322                Ok(PgValue::Int8(s.parse().map_err(|_| {
323                    PgError::TypeConversion("Invalid INT8".to_string())
324                })?))
325            }
326            oid::FLOAT4 => {
327                Ok(PgValue::Float4(s.parse().map_err(|_| {
328                    PgError::TypeConversion("Invalid FLOAT4".to_string())
329                })?))
330            }
331            oid::FLOAT8 => {
332                Ok(PgValue::Float8(s.parse().map_err(|_| {
333                    PgError::TypeConversion("Invalid FLOAT8".to_string())
334                })?))
335            }
336            oid::NUMERIC => Ok(PgValue::Numeric(s.to_string())),
337            oid::JSONB => Ok(PgValue::Jsonb(data.to_vec())),
338            oid::JSON => Ok(PgValue::Json(s.to_string())),
339            oid::BYTEA => Ok(PgValue::Bytes(decode_bytea_hex(s))),
340            oid::UUID => Ok(PgValue::Uuid(parse_uuid_text(s)?)),
341            oid::DATE => Ok(PgValue::Date(parse_date_text(s)?)),
342            oid::TIME => Ok(PgValue::Time(parse_time_text(s)?)),
343            oid::TIMESTAMP => Ok(PgValue::Timestamp(parse_timestamp_text(s)?)),
344            oid::TIMESTAMPTZ => Ok(PgValue::Timestamptz(parse_timestamp_text(s)?)),
345            oid::INTERVAL => {
346                let (months, days, us) = parse_interval_text(s)?;
347                Ok(PgValue::Interval {
348                    months,
349                    days,
350                    microseconds: us,
351                })
352            }
353            oid::INET | oid::CIDR => Ok(PgValue::Inet(s.to_string())),
354            oid::MACADDR => {
355                // Parse "xx:xx:xx:xx:xx:xx" text format
356                let bytes = parse_macaddr_text(s)?;
357                Ok(PgValue::MacAddr(bytes))
358            }
359            oid::MACADDR8 => {
360                // Parse "xx:xx:xx:xx:xx:xx:xx:xx" text format
361                let bytes = parse_macaddr8_text(s)?;
362                Ok(PgValue::MacAddr8(bytes))
363            }
364            oid::BIT | oid::VARBIT => {
365                // Text format: string of '0' and '1' characters
366                let len = s.len() as u32;
367                let mut data = vec![0u8; (len as usize).div_ceil(8)];
368                for (i, ch) in s.chars().enumerate() {
369                    if ch == '1' {
370                        let byte_idx = i / 8;
371                        let bit_idx = 7 - (i % 8);
372                        data[byte_idx] |= 1 << bit_idx;
373                    }
374                }
375                Ok(PgValue::Bit { len, data })
376            }
377            oid::POINT => {
378                // Parse "(x,y)" text format
379                let (x, y) = parse_point_text(s)?;
380                Ok(PgValue::Point { x, y })
381            }
382            oid::INT4RANGE
383            | oid::INT8RANGE
384            | oid::NUMRANGE
385            | oid::TSRANGE
386            | oid::TSTZRANGE
387            | oid::DATERANGE => Ok(PgValue::Range(s.to_string())),
388            _ => Ok(PgValue::Text(s.to_string())),
389        }
390    }
391
392    /// Parse a binary-format column value based on its type OID.
393    pub fn from_binary(type_oid: u32, data: &[u8]) -> PgResult<Self> {
394        match type_oid {
395            oid::BOOL => Ok(PgValue::Bool(data.first().is_some_and(|&b| b != 0))),
396            oid::INT2 if data.len() >= 2 => {
397                Ok(PgValue::Int2(i16::from_be_bytes([data[0], data[1]])))
398            }
399            oid::INT4 | oid::OID if data.len() >= 4 => Ok(PgValue::Int4(i32::from_be_bytes([
400                data[0], data[1], data[2], data[3],
401            ]))),
402            oid::INT8 if data.len() >= 8 => Ok(PgValue::Int8(i64::from_be_bytes([
403                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
404            ]))),
405            oid::FLOAT4 if data.len() >= 4 => Ok(PgValue::Float4(f32::from_be_bytes([
406                data[0], data[1], data[2], data[3],
407            ]))),
408            oid::FLOAT8 if data.len() >= 8 => Ok(PgValue::Float8(f64::from_be_bytes([
409                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
410            ]))),
411            oid::UUID if data.len() >= 16 => {
412                let mut bytes = [0u8; 16];
413                bytes.copy_from_slice(&data[..16]);
414                Ok(PgValue::Uuid(bytes))
415            }
416            oid::DATE if data.len() >= 4 => Ok(PgValue::Date(i32::from_be_bytes([
417                data[0], data[1], data[2], data[3],
418            ]))),
419            oid::TIME if data.len() >= 8 => Ok(PgValue::Time(i64::from_be_bytes([
420                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
421            ]))),
422            oid::TIMESTAMP | oid::TIMESTAMPTZ if data.len() >= 8 => {
423                let us = i64::from_be_bytes([
424                    data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
425                ]);
426                if type_oid == oid::TIMESTAMPTZ {
427                    Ok(PgValue::Timestamptz(us))
428                } else {
429                    Ok(PgValue::Timestamp(us))
430                }
431            }
432            oid::INTERVAL if data.len() >= 16 => {
433                let microseconds = i64::from_be_bytes([
434                    data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
435                ]);
436                let days = i32::from_be_bytes([data[8], data[9], data[10], data[11]]);
437                let months = i32::from_be_bytes([data[12], data[13], data[14], data[15]]);
438                Ok(PgValue::Interval {
439                    months,
440                    days,
441                    microseconds,
442                })
443            }
444            oid::JSONB => {
445                // First byte is version (1), rest is JSON
446                if data.len() > 1 {
447                    Ok(PgValue::Jsonb(data[1..].to_vec()))
448                } else {
449                    Ok(PgValue::Jsonb(Vec::new()))
450                }
451            }
452            oid::BYTEA => Ok(PgValue::Bytes(data.to_vec())),
453            oid::INET | oid::CIDR => {
454                // Binary format: family(1) + mask(1) + is_cidr(1) + addr_len(1) + addr bytes
455                if data.len() < 4 {
456                    return Err(PgError::TypeConversion("INET/CIDR binary too short".into()));
457                }
458                let family = data[0];
459                let mask = data[1];
460                // data[2] = is_cidr flag (0 = INET, 1 = CIDR)
461                let addr_len = data[3] as usize;
462                if data.len() < 4 + addr_len {
463                    return Err(PgError::TypeConversion(
464                        "INET/CIDR address truncated".into(),
465                    ));
466                }
467                let addr_bytes = &data[4..4 + addr_len];
468                let addr_str = match family {
469                    // AF_INET
470                    2 if addr_len == 4 => {
471                        format!(
472                            "{}.{}.{}.{}",
473                            addr_bytes[0], addr_bytes[1], addr_bytes[2], addr_bytes[3]
474                        )
475                    }
476                    // AF_INET6
477                    3 if addr_len == 16 => format_ipv6(addr_bytes),
478                    _ => {
479                        return Err(PgError::TypeConversion(format!(
480                            "Unknown INET family: {}",
481                            family
482                        )));
483                    }
484                };
485                // Include mask for CIDR or non-default masks
486                let default_mask = if family == 2 { 32 } else { 128 };
487                if mask != default_mask || type_oid == oid::CIDR {
488                    Ok(PgValue::Inet(format!("{}/{}", addr_str, mask)))
489                } else {
490                    Ok(PgValue::Inet(addr_str))
491                }
492            }
493            oid::NUMERIC => {
494                // Binary NUMERIC format:
495                //   ndigits(u16) + weight(i16) + sign(u16) + dscale(u16) + digits(u16 * ndigits)
496                // Each digit is a base-10000 value.
497                if data.len() < 8 {
498                    return Err(PgError::TypeConversion("NUMERIC binary too short".into()));
499                }
500                let ndigits = u16::from_be_bytes([data[0], data[1]]) as usize;
501                let weight = i16::from_be_bytes([data[2], data[3]]);
502                let sign = u16::from_be_bytes([data[4], data[5]]);
503                let dscale = u16::from_be_bytes([data[6], data[7]]) as usize;
504
505                if data.len() < 8 + ndigits * 2 {
506                    return Err(PgError::TypeConversion("NUMERIC binary truncated".into()));
507                }
508
509                let mut digits = Vec::with_capacity(ndigits);
510                for i in 0..ndigits {
511                    let off = 8 + i * 2;
512                    digits.push(u16::from_be_bytes([data[off], data[off + 1]]));
513                }
514
515                Ok(PgValue::Numeric(format_numeric_binary(
516                    weight, sign, dscale, &digits,
517                )))
518            }
519            oid::MACADDR if data.len() >= 6 => {
520                let mut bytes = [0u8; 6];
521                bytes.copy_from_slice(&data[..6]);
522                Ok(PgValue::MacAddr(bytes))
523            }
524            oid::MACADDR8 if data.len() >= 8 => {
525                let mut bytes = [0u8; 8];
526                bytes.copy_from_slice(&data[..8]);
527                Ok(PgValue::MacAddr8(bytes))
528            }
529            oid::BIT | oid::VARBIT if data.len() >= 4 => {
530                // Binary format: 4-byte bit length + packed bytes
531                let bit_len = i32::from_be_bytes([data[0], data[1], data[2], data[3]]) as u32;
532                let packed = data[4..].to_vec();
533                Ok(PgValue::Bit {
534                    len: bit_len,
535                    data: packed,
536                })
537            }
538            oid::POINT if data.len() >= 16 => {
539                let x = f64::from_be_bytes([
540                    data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
541                ]);
542                let y = f64::from_be_bytes([
543                    data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
544                ]);
545                Ok(PgValue::Point { x, y })
546            }
547            oid::BOOL_ARRAY
548            | oid::INT2_ARRAY
549            | oid::INT4_ARRAY
550            | oid::INT8_ARRAY
551            | oid::FLOAT4_ARRAY
552            | oid::FLOAT8_ARRAY
553            | oid::TEXT_ARRAY
554            | oid::VARCHAR_ARRAY
555            | oid::UUID_ARRAY
556            | oid::JSONB_ARRAY
557            | oid::JSON_ARRAY => parse_binary_array(data),
558            _ => {
559                // Fallback: treat as text
560                Ok(PgValue::Text(String::from_utf8_lossy(data).to_string()))
561            }
562        }
563    }
564
565    /// Try to extract as i32.
566    pub fn as_i32(&self) -> Option<i32> {
567        match self {
568            PgValue::Int4(v) => Some(*v),
569            PgValue::Int2(v) => Some(*v as i32),
570            _ => None,
571        }
572    }
573
574    /// Try to extract as i64.
575    pub fn as_i64(&self) -> Option<i64> {
576        match self {
577            PgValue::Int8(v) => Some(*v),
578            PgValue::Int4(v) => Some(*v as i64),
579            PgValue::Int2(v) => Some(*v as i64),
580            _ => None,
581        }
582    }
583
584    /// Try to extract as &str.
585    pub fn as_str(&self) -> Option<&str> {
586        match self {
587            PgValue::Text(s) => Some(s),
588            PgValue::Json(s) => Some(s),
589            PgValue::Inet(s) => Some(s),
590            PgValue::Numeric(s) => Some(s),
591            _ => None,
592        }
593    }
594
595    /// Try to extract as bool.
596    pub fn as_bool(&self) -> Option<bool> {
597        match self {
598            PgValue::Bool(b) => Some(*b),
599            _ => None,
600        }
601    }
602
603    /// Try to extract as f64.
604    pub fn as_f64(&self) -> Option<f64> {
605        match self {
606            PgValue::Float8(v) => Some(*v),
607            PgValue::Float4(v) => Some(*v as f64),
608            PgValue::Int4(v) => Some(*v as f64),
609            PgValue::Int8(v) => Some(*v as f64),
610            _ => None,
611        }
612    }
613
614    /// Returns true if this is a Null value.
615    pub fn is_null(&self) -> bool {
616        matches!(self, PgValue::Null)
617    }
618}
619
620// ─── ToSql / FromSql Traits ──────────────────────────────────
621
622/// Trait for converting Rust types to PostgreSQL parameter values.
623/// Replaces the older `ToParam` — provides the same functionality with
624/// a more standard name and the ability to specify the OID.
625pub trait ToSql {
626    /// Convert this value to a PgValue for use as a query parameter.
627    fn to_sql(&self) -> PgValue;
628
629    /// The PostgreSQL type OID this value maps to (0 = let the server decide).
630    fn type_oid(&self) -> u32 {
631        0
632    }
633}
634
635/// Trait for converting PostgreSQL values to Rust types.
636pub trait FromSql: Sized {
637    /// Convert a PgValue to this Rust type.
638    fn from_sql(value: &PgValue) -> PgResult<Self>;
639}
640
641// ─── ToSql Implementations ───────────────────────────────────
642
643impl ToSql for i16 {
644    fn to_sql(&self) -> PgValue {
645        PgValue::Int2(*self)
646    }
647    fn type_oid(&self) -> u32 {
648        oid::INT2
649    }
650}
651
652impl ToSql for i32 {
653    fn to_sql(&self) -> PgValue {
654        PgValue::Int4(*self)
655    }
656    fn type_oid(&self) -> u32 {
657        oid::INT4
658    }
659}
660
661impl ToSql for i64 {
662    fn to_sql(&self) -> PgValue {
663        PgValue::Int8(*self)
664    }
665    fn type_oid(&self) -> u32 {
666        oid::INT8
667    }
668}
669
670impl ToSql for f32 {
671    fn to_sql(&self) -> PgValue {
672        PgValue::Float4(*self)
673    }
674    fn type_oid(&self) -> u32 {
675        oid::FLOAT4
676    }
677}
678
679impl ToSql for f64 {
680    fn to_sql(&self) -> PgValue {
681        PgValue::Float8(*self)
682    }
683    fn type_oid(&self) -> u32 {
684        oid::FLOAT8
685    }
686}
687
688impl ToSql for bool {
689    fn to_sql(&self) -> PgValue {
690        PgValue::Bool(*self)
691    }
692    fn type_oid(&self) -> u32 {
693        oid::BOOL
694    }
695}
696
697impl ToSql for &str {
698    fn to_sql(&self) -> PgValue {
699        PgValue::Text(self.to_string())
700    }
701    fn type_oid(&self) -> u32 {
702        oid::TEXT
703    }
704}
705
706impl ToSql for String {
707    fn to_sql(&self) -> PgValue {
708        PgValue::Text(self.clone())
709    }
710    fn type_oid(&self) -> u32 {
711        oid::TEXT
712    }
713}
714
715impl ToSql for &[u8] {
716    fn to_sql(&self) -> PgValue {
717        PgValue::Bytes(self.to_vec())
718    }
719    fn type_oid(&self) -> u32 {
720        oid::BYTEA
721    }
722}
723
724impl ToSql for Vec<u8> {
725    fn to_sql(&self) -> PgValue {
726        PgValue::Bytes(self.clone())
727    }
728    fn type_oid(&self) -> u32 {
729        oid::BYTEA
730    }
731}
732
733impl<T: ToSql> ToSql for Option<T> {
734    fn to_sql(&self) -> PgValue {
735        match self {
736            Some(v) => v.to_sql(),
737            None => PgValue::Null,
738        }
739    }
740}
741
742impl ToSql for PgValue {
743    fn to_sql(&self) -> PgValue {
744        self.clone()
745    }
746}
747
748// ─── Array ToSql Implementations ──────────────────────────────
749
750impl ToSql for Vec<i16> {
751    fn to_sql(&self) -> PgValue {
752        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
753    }
754    fn type_oid(&self) -> u32 {
755        oid::INT2_ARRAY
756    }
757}
758
759impl ToSql for Vec<i32> {
760    fn to_sql(&self) -> PgValue {
761        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
762    }
763    fn type_oid(&self) -> u32 {
764        oid::INT4_ARRAY
765    }
766}
767
768impl ToSql for Vec<i64> {
769    fn to_sql(&self) -> PgValue {
770        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
771    }
772    fn type_oid(&self) -> u32 {
773        oid::INT8_ARRAY
774    }
775}
776
777impl ToSql for Vec<f32> {
778    fn to_sql(&self) -> PgValue {
779        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
780    }
781    fn type_oid(&self) -> u32 {
782        oid::FLOAT4_ARRAY
783    }
784}
785
786impl ToSql for Vec<f64> {
787    fn to_sql(&self) -> PgValue {
788        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
789    }
790    fn type_oid(&self) -> u32 {
791        oid::FLOAT8_ARRAY
792    }
793}
794
795impl ToSql for Vec<bool> {
796    fn to_sql(&self) -> PgValue {
797        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
798    }
799    fn type_oid(&self) -> u32 {
800        oid::BOOL_ARRAY
801    }
802}
803
804impl ToSql for Vec<String> {
805    fn to_sql(&self) -> PgValue {
806        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
807    }
808    fn type_oid(&self) -> u32 {
809        oid::TEXT_ARRAY
810    }
811}
812
813impl ToSql for &[i16] {
814    fn to_sql(&self) -> PgValue {
815        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
816    }
817    fn type_oid(&self) -> u32 {
818        oid::INT2_ARRAY
819    }
820}
821
822impl ToSql for &[i32] {
823    fn to_sql(&self) -> PgValue {
824        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
825    }
826    fn type_oid(&self) -> u32 {
827        oid::INT4_ARRAY
828    }
829}
830
831impl ToSql for &[i64] {
832    fn to_sql(&self) -> PgValue {
833        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
834    }
835    fn type_oid(&self) -> u32 {
836        oid::INT8_ARRAY
837    }
838}
839
840impl ToSql for &[f32] {
841    fn to_sql(&self) -> PgValue {
842        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
843    }
844    fn type_oid(&self) -> u32 {
845        oid::FLOAT4_ARRAY
846    }
847}
848
849impl ToSql for &[f64] {
850    fn to_sql(&self) -> PgValue {
851        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
852    }
853    fn type_oid(&self) -> u32 {
854        oid::FLOAT8_ARRAY
855    }
856}
857
858impl ToSql for &[bool] {
859    fn to_sql(&self) -> PgValue {
860        PgValue::Array(self.iter().map(|v| v.to_sql()).collect())
861    }
862    fn type_oid(&self) -> u32 {
863        oid::BOOL_ARRAY
864    }
865}
866
867// ─── Network Type ToSql Implementations ───────────────────────
868
869impl ToSql for std::net::IpAddr {
870    fn to_sql(&self) -> PgValue {
871        PgValue::Inet(self.to_string())
872    }
873    fn type_oid(&self) -> u32 {
874        oid::INET
875    }
876}
877
878impl ToSql for std::net::Ipv4Addr {
879    fn to_sql(&self) -> PgValue {
880        PgValue::Inet(self.to_string())
881    }
882    fn type_oid(&self) -> u32 {
883        oid::INET
884    }
885}
886
887impl ToSql for std::net::Ipv6Addr {
888    fn to_sql(&self) -> PgValue {
889        PgValue::Inet(self.to_string())
890    }
891    fn type_oid(&self) -> u32 {
892        oid::INET
893    }
894}
895
896// ─── MacAddr / Point ToSql Implementations ────────────────────
897
898impl ToSql for [u8; 6] {
899    fn to_sql(&self) -> PgValue {
900        PgValue::MacAddr(*self)
901    }
902    fn type_oid(&self) -> u32 {
903        oid::MACADDR
904    }
905}
906
907impl ToSql for (f64, f64) {
908    fn to_sql(&self) -> PgValue {
909        PgValue::Point {
910            x: self.0,
911            y: self.1,
912        }
913    }
914    fn type_oid(&self) -> u32 {
915        oid::POINT
916    }
917}
918
919// ─── Chrono ToSql Implementations ────────────────────────────
920
921/// PostgreSQL epoch offset: 2000-01-01T00:00:00 UTC − Unix epoch = 946,684,800 seconds.
922#[cfg(feature = "chrono")]
923const PG_EPOCH_OFFSET_SECS: i64 = 946_684_800;
924
925#[cfg(feature = "chrono")]
926impl ToSql for chrono::NaiveDateTime {
927    fn to_sql(&self) -> PgValue {
928        let unix_secs = self.and_utc().timestamp();
929        let sub_micros = self.and_utc().timestamp_subsec_micros() as i64;
930        let pg_micros = (unix_secs - PG_EPOCH_OFFSET_SECS) * 1_000_000 + sub_micros;
931        PgValue::Timestamp(pg_micros)
932    }
933    fn type_oid(&self) -> u32 {
934        oid::TIMESTAMP
935    }
936}
937
938// ─── rust_decimal ToSql Implementations ──────────────────────
939
940#[cfg(feature = "decimal")]
941impl ToSql for rust_decimal::Decimal {
942    fn to_sql(&self) -> PgValue {
943        PgValue::Numeric(self.to_string())
944    }
945    fn type_oid(&self) -> u32 {
946        oid::NUMERIC
947    }
948}
949
950// ─── FromSql Implementations ─────────────────────────────────
951
952impl FromSql for i16 {
953    fn from_sql(value: &PgValue) -> PgResult<Self> {
954        match value {
955            PgValue::Int2(v) => Ok(*v),
956            PgValue::Text(s) => s
957                .parse()
958                .map_err(|_| PgError::TypeConversion("Not an i16".into())),
959            _ => Err(PgError::TypeConversion("Cannot convert to i16".into())),
960        }
961    }
962}
963
964impl FromSql for i32 {
965    fn from_sql(value: &PgValue) -> PgResult<Self> {
966        match value {
967            PgValue::Int4(v) => Ok(*v),
968            PgValue::Int2(v) => Ok(*v as i32),
969            PgValue::Text(s) => s
970                .parse()
971                .map_err(|_| PgError::TypeConversion("Not an i32".into())),
972            _ => Err(PgError::TypeConversion("Cannot convert to i32".into())),
973        }
974    }
975}
976
977impl FromSql for i64 {
978    fn from_sql(value: &PgValue) -> PgResult<Self> {
979        match value {
980            PgValue::Int8(v) => Ok(*v),
981            PgValue::Int4(v) => Ok(*v as i64),
982            PgValue::Int2(v) => Ok(*v as i64),
983            PgValue::Text(s) => s
984                .parse()
985                .map_err(|_| PgError::TypeConversion("Not an i64".into())),
986            _ => Err(PgError::TypeConversion("Cannot convert to i64".into())),
987        }
988    }
989}
990
991impl FromSql for f32 {
992    fn from_sql(value: &PgValue) -> PgResult<Self> {
993        match value {
994            PgValue::Float4(v) => Ok(*v),
995            PgValue::Text(s) => s
996                .parse()
997                .map_err(|_| PgError::TypeConversion("Not an f32".into())),
998            _ => Err(PgError::TypeConversion("Cannot convert to f32".into())),
999        }
1000    }
1001}
1002
1003impl FromSql for f64 {
1004    fn from_sql(value: &PgValue) -> PgResult<Self> {
1005        match value {
1006            PgValue::Float8(v) => Ok(*v),
1007            PgValue::Float4(v) => Ok(*v as f64),
1008            PgValue::Int4(v) => Ok(*v as f64),
1009            PgValue::Int8(v) => Ok(*v as f64),
1010            PgValue::Text(s) => s
1011                .parse()
1012                .map_err(|_| PgError::TypeConversion("Not an f64".into())),
1013            _ => Err(PgError::TypeConversion("Cannot convert to f64".into())),
1014        }
1015    }
1016}
1017
1018impl FromSql for bool {
1019    fn from_sql(value: &PgValue) -> PgResult<Self> {
1020        match value {
1021            PgValue::Bool(v) => Ok(*v),
1022            PgValue::Text(s) => Ok(s == "t" || s == "true" || s == "1"),
1023            _ => Err(PgError::TypeConversion("Cannot convert to bool".into())),
1024        }
1025    }
1026}
1027
1028impl FromSql for String {
1029    fn from_sql(value: &PgValue) -> PgResult<Self> {
1030        match value {
1031            PgValue::Text(s) => Ok(s.clone()),
1032            PgValue::Json(s) => Ok(s.clone()),
1033            PgValue::Inet(s) => Ok(s.clone()),
1034            PgValue::Numeric(s) => Ok(s.clone()),
1035            PgValue::Int2(v) => Ok(v.to_string()),
1036            PgValue::Int4(v) => Ok(v.to_string()),
1037            PgValue::Int8(v) => Ok(v.to_string()),
1038            PgValue::Float4(v) => Ok(v.to_string()),
1039            PgValue::Float8(v) => Ok(v.to_string()),
1040            PgValue::Bool(v) => Ok(v.to_string()),
1041            PgValue::Uuid(b) => Ok(format_uuid(b)),
1042            PgValue::Null => Err(PgError::TypeConversion(
1043                "Cannot convert NULL to String".into(),
1044            )),
1045            _ => Err(PgError::TypeConversion("Cannot convert to String".into())),
1046        }
1047    }
1048}
1049
1050impl<T: FromSql> FromSql for Option<T> {
1051    fn from_sql(value: &PgValue) -> PgResult<Self> {
1052        if value.is_null() {
1053            Ok(None)
1054        } else {
1055            T::from_sql(value).map(Some)
1056        }
1057    }
1058}
1059
1060impl FromSql for Vec<u8> {
1061    fn from_sql(value: &PgValue) -> PgResult<Self> {
1062        match value {
1063            PgValue::Bytes(b) => Ok(b.clone()),
1064            PgValue::Null => Err(PgError::TypeConversion(
1065                "Cannot convert NULL to Vec<u8>".into(),
1066            )),
1067            _ => Err(PgError::TypeConversion("Cannot convert to Vec<u8>".into())),
1068        }
1069    }
1070}
1071
1072impl FromSql for [u8; 16] {
1073    fn from_sql(value: &PgValue) -> PgResult<Self> {
1074        match value {
1075            PgValue::Uuid(b) => Ok(*b),
1076            PgValue::Null => Err(PgError::TypeConversion(
1077                "Cannot convert NULL to [u8; 16]".into(),
1078            )),
1079            _ => Err(PgError::TypeConversion("Cannot convert to [u8; 16]".into())),
1080        }
1081    }
1082}
1083
1084// ─── Array FromSql Implementations ────────────────────────────
1085
1086impl FromSql for Vec<i16> {
1087    fn from_sql(value: &PgValue) -> PgResult<Self> {
1088        match value {
1089            PgValue::Array(arr) => arr.iter().map(i16::from_sql).collect(),
1090            PgValue::Null => Err(PgError::TypeConversion(
1091                "Cannot convert NULL to Vec<i16>".into(),
1092            )),
1093            _ => Err(PgError::TypeConversion("Cannot convert to Vec<i16>".into())),
1094        }
1095    }
1096}
1097
1098impl FromSql for Vec<i32> {
1099    fn from_sql(value: &PgValue) -> PgResult<Self> {
1100        match value {
1101            PgValue::Array(arr) => arr.iter().map(i32::from_sql).collect(),
1102            PgValue::Null => Err(PgError::TypeConversion(
1103                "Cannot convert NULL to Vec<i32>".into(),
1104            )),
1105            _ => Err(PgError::TypeConversion("Cannot convert to Vec<i32>".into())),
1106        }
1107    }
1108}
1109
1110impl FromSql for Vec<i64> {
1111    fn from_sql(value: &PgValue) -> PgResult<Self> {
1112        match value {
1113            PgValue::Array(arr) => arr.iter().map(i64::from_sql).collect(),
1114            PgValue::Null => Err(PgError::TypeConversion(
1115                "Cannot convert NULL to Vec<i64>".into(),
1116            )),
1117            _ => Err(PgError::TypeConversion("Cannot convert to Vec<i64>".into())),
1118        }
1119    }
1120}
1121
1122impl FromSql for Vec<f32> {
1123    fn from_sql(value: &PgValue) -> PgResult<Self> {
1124        match value {
1125            PgValue::Array(arr) => arr.iter().map(f32::from_sql).collect(),
1126            PgValue::Null => Err(PgError::TypeConversion(
1127                "Cannot convert NULL to Vec<f32>".into(),
1128            )),
1129            _ => Err(PgError::TypeConversion("Cannot convert to Vec<f32>".into())),
1130        }
1131    }
1132}
1133
1134impl FromSql for Vec<f64> {
1135    fn from_sql(value: &PgValue) -> PgResult<Self> {
1136        match value {
1137            PgValue::Array(arr) => arr.iter().map(f64::from_sql).collect(),
1138            PgValue::Null => Err(PgError::TypeConversion(
1139                "Cannot convert NULL to Vec<f64>".into(),
1140            )),
1141            _ => Err(PgError::TypeConversion("Cannot convert to Vec<f64>".into())),
1142        }
1143    }
1144}
1145
1146impl FromSql for Vec<bool> {
1147    fn from_sql(value: &PgValue) -> PgResult<Self> {
1148        match value {
1149            PgValue::Array(arr) => arr.iter().map(bool::from_sql).collect(),
1150            PgValue::Null => Err(PgError::TypeConversion(
1151                "Cannot convert NULL to Vec<bool>".into(),
1152            )),
1153            _ => Err(PgError::TypeConversion(
1154                "Cannot convert to Vec<bool>".into(),
1155            )),
1156        }
1157    }
1158}
1159
1160impl FromSql for Vec<String> {
1161    fn from_sql(value: &PgValue) -> PgResult<Self> {
1162        match value {
1163            PgValue::Array(arr) => arr.iter().map(String::from_sql).collect(),
1164            PgValue::Null => Err(PgError::TypeConversion(
1165                "Cannot convert NULL to Vec<String>".into(),
1166            )),
1167            _ => Err(PgError::TypeConversion(
1168                "Cannot convert to Vec<String>".into(),
1169            )),
1170        }
1171    }
1172}
1173
1174// ─── Network Type FromSql Implementations ─────────────────────
1175
1176impl FromSql for std::net::IpAddr {
1177    fn from_sql(value: &PgValue) -> PgResult<Self> {
1178        match value {
1179            PgValue::Inet(s) => {
1180                let addr_str = s.split('/').next().unwrap_or(s);
1181                addr_str
1182                    .parse()
1183                    .map_err(|_| PgError::TypeConversion(format!("Invalid IP address: {}", s)))
1184            }
1185            PgValue::Null => Err(PgError::TypeConversion(
1186                "Cannot convert NULL to IpAddr".into(),
1187            )),
1188            _ => Err(PgError::TypeConversion("Cannot convert to IpAddr".into())),
1189        }
1190    }
1191}
1192
1193impl FromSql for std::net::Ipv4Addr {
1194    fn from_sql(value: &PgValue) -> PgResult<Self> {
1195        match value {
1196            PgValue::Inet(s) => {
1197                let addr_str = s.split('/').next().unwrap_or(s);
1198                addr_str
1199                    .parse()
1200                    .map_err(|_| PgError::TypeConversion(format!("Invalid IPv4 address: {}", s)))
1201            }
1202            PgValue::Null => Err(PgError::TypeConversion(
1203                "Cannot convert NULL to Ipv4Addr".into(),
1204            )),
1205            _ => Err(PgError::TypeConversion("Cannot convert to Ipv4Addr".into())),
1206        }
1207    }
1208}
1209
1210impl FromSql for std::net::Ipv6Addr {
1211    fn from_sql(value: &PgValue) -> PgResult<Self> {
1212        match value {
1213            PgValue::Inet(s) => {
1214                let addr_str = s.split('/').next().unwrap_or(s);
1215                addr_str
1216                    .parse()
1217                    .map_err(|_| PgError::TypeConversion(format!("Invalid IPv6 address: {}", s)))
1218            }
1219            PgValue::Null => Err(PgError::TypeConversion(
1220                "Cannot convert NULL to Ipv6Addr".into(),
1221            )),
1222            _ => Err(PgError::TypeConversion("Cannot convert to Ipv6Addr".into())),
1223        }
1224    }
1225}
1226
1227// ─── MacAddr / Point FromSql ──────────────────────────────────
1228
1229impl FromSql for [u8; 6] {
1230    fn from_sql(value: &PgValue) -> PgResult<Self> {
1231        match value {
1232            PgValue::MacAddr(bytes) => Ok(*bytes),
1233            PgValue::Null => Err(PgError::TypeConversion(
1234                "Cannot convert NULL to [u8; 6]".into(),
1235            )),
1236            _ => Err(PgError::TypeConversion(
1237                "Cannot convert to [u8; 6] (MacAddr)".into(),
1238            )),
1239        }
1240    }
1241}
1242
1243impl FromSql for (f64, f64) {
1244    fn from_sql(value: &PgValue) -> PgResult<Self> {
1245        match value {
1246            PgValue::Point { x, y } => Ok((*x, *y)),
1247            PgValue::Null => Err(PgError::TypeConversion(
1248                "Cannot convert NULL to (f64, f64)".into(),
1249            )),
1250            _ => Err(PgError::TypeConversion(
1251                "Cannot convert to (f64, f64) (Point)".into(),
1252            )),
1253        }
1254    }
1255}
1256
1257// ─── Backward Compatibility ──────────────────────────────────
1258
1259/// Convenience trait for converting Rust types to PgValue parameters.
1260/// Kept for backward compatibility — prefer `ToSql` for new code.
1261pub trait ToParam {
1262    fn to_param(&self) -> PgValue;
1263}
1264
1265impl<T: ToSql> ToParam for T {
1266    fn to_param(&self) -> PgValue {
1267        self.to_sql()
1268    }
1269}
1270
1271// ─── MacAddr / Point Text Parsing ─────────────────────────────
1272
1273/// Parse a MAC address from text format "xx:xx:xx:xx:xx:xx" or "xx-xx-xx-xx-xx-xx".
1274fn parse_macaddr_text(s: &str) -> PgResult<[u8; 6]> {
1275    let parts: Vec<&str> = if s.contains(':') {
1276        s.split(':').collect()
1277    } else if s.contains('-') {
1278        s.split('-').collect()
1279    } else {
1280        return Err(PgError::TypeConversion(format!(
1281            "Invalid MAC address format: {}",
1282            s
1283        )));
1284    };
1285    if parts.len() != 6 {
1286        return Err(PgError::TypeConversion(format!(
1287            "Invalid MAC address: {}",
1288            s
1289        )));
1290    }
1291    let mut bytes = [0u8; 6];
1292    for (i, part) in parts.iter().enumerate() {
1293        bytes[i] = u8::from_str_radix(part, 16)
1294            .map_err(|_| PgError::TypeConversion(format!("Invalid MAC address hex: {}", part)))?;
1295    }
1296    Ok(bytes)
1297}
1298
1299/// Parse a MACADDR8 from text format "xx:xx:xx:xx:xx:xx:xx:xx".
1300fn parse_macaddr8_text(s: &str) -> PgResult<[u8; 8]> {
1301    let parts: Vec<&str> = if s.contains(':') {
1302        s.split(':').collect()
1303    } else if s.contains('-') {
1304        s.split('-').collect()
1305    } else {
1306        return Err(PgError::TypeConversion(format!(
1307            "Invalid MACADDR8 format: {}",
1308            s
1309        )));
1310    };
1311    if parts.len() != 8 {
1312        return Err(PgError::TypeConversion(format!("Invalid MACADDR8: {}", s)));
1313    }
1314    let mut bytes = [0u8; 8];
1315    for (i, part) in parts.iter().enumerate() {
1316        bytes[i] = u8::from_str_radix(part, 16)
1317            .map_err(|_| PgError::TypeConversion(format!("Invalid MACADDR8 hex: {}", part)))?;
1318    }
1319    Ok(bytes)
1320}
1321
1322/// Parse a point from text format "(x,y)".
1323fn parse_point_text(s: &str) -> PgResult<(f64, f64)> {
1324    let trimmed = s.trim();
1325    let inner = if trimmed.starts_with('(') && trimmed.ends_with(')') {
1326        &trimmed[1..trimmed.len() - 1]
1327    } else {
1328        trimmed
1329    };
1330    let comma = inner
1331        .find(',')
1332        .ok_or_else(|| PgError::TypeConversion(format!("Invalid point format: {}", s)))?;
1333    let x: f64 = inner[..comma]
1334        .trim()
1335        .parse()
1336        .map_err(|_| PgError::TypeConversion(format!("Invalid point x: {}", &inner[..comma])))?;
1337    let y: f64 = inner[comma + 1..].trim().parse().map_err(|_| {
1338        PgError::TypeConversion(format!("Invalid point y: {}", &inner[comma + 1..]))
1339    })?;
1340    Ok((x, y))
1341}
1342
1343// ─── Binary NUMERIC Formatting ────────────────────────────────
1344
1345/// Format a PostgreSQL binary NUMERIC value to a decimal string.
1346///
1347/// PostgreSQL stores NUMERIC as base-10000 digits with a weight
1348/// (exponent in base-10000), a sign flag, and a display scale.
1349fn format_numeric_binary(weight: i16, sign: u16, dscale: usize, digits: &[u16]) -> String {
1350    // Special values
1351    const _NUMERIC_POS: u16 = 0x0000;
1352    const NUMERIC_NEG: u16 = 0x4000;
1353    const NUMERIC_NAN: u16 = 0xC000;
1354    const NUMERIC_PINF: u16 = 0xD000;
1355    const NUMERIC_NINF: u16 = 0xF000;
1356
1357    match sign {
1358        NUMERIC_NAN => return "NaN".to_string(),
1359        NUMERIC_PINF => return "Infinity".to_string(),
1360        NUMERIC_NINF => return "-Infinity".to_string(),
1361        _ => {}
1362    }
1363
1364    if digits.is_empty() {
1365        // Zero — respect dscale
1366        return if dscale > 0 {
1367            format!("0.{}", "0".repeat(dscale))
1368        } else {
1369            "0".to_string()
1370        };
1371    }
1372
1373    // Build the full base-10000 digit string
1374    // weight = number of base-10000 digits before the decimal point minus 1
1375    // So with weight=1, the first 2 digit groups (indices 0..=1) are before the decimal.
1376
1377    let mut result = String::with_capacity(digits.len() * 4 + 4);
1378
1379    if sign == NUMERIC_NEG {
1380        result.push('-');
1381    }
1382
1383    // Integer part: digit groups 0..=weight
1384    let int_groups = (weight + 1).max(0) as usize;
1385
1386    if int_groups == 0 {
1387        result.push('0');
1388    } else {
1389        for i in 0..int_groups {
1390            let d = if i < digits.len() { digits[i] } else { 0 };
1391            if i == 0 {
1392                // First group: no leading zeros
1393                result.push_str(&d.to_string());
1394            } else {
1395                // Subsequent groups: pad to 4 digits
1396                result.push_str(&format!("{:04}", d));
1397            }
1398        }
1399    }
1400
1401    // Fractional part
1402    if dscale > 0 {
1403        result.push('.');
1404        let mut frac_chars = 0;
1405        let frac_start = int_groups;
1406        let mut i = frac_start;
1407        while frac_chars < dscale {
1408            let d = if i < digits.len() { digits[i] } else { 0 };
1409            let s = format!("{:04}", d);
1410            for ch in s.chars() {
1411                if frac_chars >= dscale {
1412                    break;
1413                }
1414                result.push(ch);
1415                frac_chars += 1;
1416            }
1417            i += 1;
1418        }
1419    }
1420
1421    result
1422}
1423
1424// ─── Binary Array Parsing ─────────────────────────────────────
1425
1426/// Parse a PostgreSQL binary array value.
1427///
1428/// Binary array format:
1429///   ndim (i32) + flags (i32) + element_oid (i32)
1430///   for each dimension: len (i32) + lower_bound (i32)
1431///   for each element: len (i32) + data (len bytes), or len=-1 for NULL
1432fn parse_binary_array(data: &[u8]) -> PgResult<PgValue> {
1433    if data.len() < 12 {
1434        return Err(PgError::TypeConversion("Binary array too short".into()));
1435    }
1436
1437    let ndim = i32::from_be_bytes([data[0], data[1], data[2], data[3]]);
1438    // flags at offset 4 (has_null indicator — we handle NULLs inline)
1439    let element_oid = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
1440
1441    if ndim == 0 {
1442        return Ok(PgValue::Array(Vec::new()));
1443    }
1444    if ndim != 1 {
1445        // We only support 1-dimensional arrays
1446        return Err(PgError::TypeConversion(format!(
1447            "Unsupported array dimensions: {}",
1448            ndim
1449        )));
1450    }
1451
1452    let mut pos = 12;
1453    // Dimension length and lower bound
1454    if data.len() < pos + 8 {
1455        return Err(PgError::TypeConversion(
1456            "Binary array dimension truncated".into(),
1457        ));
1458    }
1459    let num_elements =
1460        i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
1461    pos += 4;
1462    let _lower_bound = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1463    pos += 4;
1464
1465    let mut values = Vec::with_capacity(num_elements);
1466    for _ in 0..num_elements {
1467        if data.len() < pos + 4 {
1468            return Err(PgError::TypeConversion(
1469                "Binary array element truncated".into(),
1470            ));
1471        }
1472        let elem_len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1473        pos += 4;
1474
1475        if elem_len < 0 {
1476            values.push(PgValue::Null);
1477        } else {
1478            let elem_len = elem_len as usize;
1479            if data.len() < pos + elem_len {
1480                return Err(PgError::TypeConversion(
1481                    "Binary array element data truncated".into(),
1482                ));
1483            }
1484            let elem_data = &data[pos..pos + elem_len];
1485            values.push(PgValue::from_binary(element_oid, elem_data)?);
1486            pos += elem_len;
1487        }
1488    }
1489
1490    Ok(PgValue::Array(values))
1491}
1492
1493// ─── UUID Formatting/Parsing ─────────────────────────────────
1494
1495/// Format a 16-byte UUID as a string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
1496fn format_uuid(bytes: &[u8; 16]) -> String {
1497    fn hex(b: u8) -> (char, char) {
1498        const HEX: &[u8; 16] = b"0123456789abcdef";
1499        (
1500            HEX[(b >> 4) as usize] as char,
1501            HEX[(b & 0xf) as usize] as char,
1502        )
1503    }
1504    let mut s = String::with_capacity(36);
1505    for (i, &b) in bytes.iter().enumerate() {
1506        if i == 4 || i == 6 || i == 8 || i == 10 {
1507            s.push('-');
1508        }
1509        let (hi, lo) = hex(b);
1510        s.push(hi);
1511        s.push(lo);
1512    }
1513    s
1514}
1515
1516/// Parse a UUID from text format.
1517fn parse_uuid_text(s: &str) -> PgResult<[u8; 16]> {
1518    let hex: String = s.chars().filter(|c| *c != '-').collect();
1519    if hex.len() != 32 {
1520        return Err(PgError::TypeConversion(format!("Invalid UUID: {}", s)));
1521    }
1522    let mut bytes = [0u8; 16];
1523    for i in 0..16 {
1524        bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
1525            .map_err(|_| PgError::TypeConversion(format!("Invalid UUID hex: {}", s)))?;
1526    }
1527    Ok(bytes)
1528}
1529
1530// ─── Date/Time Formatting/Parsing ────────────────────────────
1531
1532/// PostgreSQL epoch: 2000-01-01. Days from Unix epoch (1970-01-01) to PG epoch.
1533const PG_EPOCH_DAYS: i32 = 10957;
1534
1535/// Format a PostgreSQL date (days since 2000-01-01) as YYYY-MM-DD.
1536fn format_date(days: i32) -> String {
1537    let (y, m, d) = days_to_ymd(days + PG_EPOCH_DAYS);
1538    format!("{:04}-{:02}-{:02}", y, m, d)
1539}
1540
1541/// Format a PostgreSQL time (microseconds since midnight) as HH:MM:SS.ffffff.
1542fn format_time(us: i64) -> String {
1543    let total_secs = us / 1_000_000;
1544    let frac = us % 1_000_000;
1545    let h = total_secs / 3600;
1546    let m = (total_secs % 3600) / 60;
1547    let s = total_secs % 60;
1548    if frac > 0 {
1549        format!("{:02}:{:02}:{:02}.{:06}", h, m, s, frac)
1550    } else {
1551        format!("{:02}:{:02}:{:02}", h, m, s)
1552    }
1553}
1554
1555/// Format a PostgreSQL timestamp as YYYY-MM-DD HH:MM:SS.ffffff.
1556fn format_timestamp(us: i64) -> String {
1557    let total_days = (us / 86_400_000_000) as i32;
1558    let time_us = us % 86_400_000_000;
1559    let (time_us, total_days) = if time_us < 0 {
1560        (time_us + 86_400_000_000, total_days - 1)
1561    } else {
1562        (time_us, total_days)
1563    };
1564    let date = format_date(total_days);
1565    let time = format_time(time_us);
1566    format!("{} {}", date, time)
1567}
1568
1569/// Format a PostgreSQL timestamptz.
1570fn format_timestamp_tz(us: i64) -> String {
1571    format!("{}+00", format_timestamp(us))
1572}
1573
1574/// Format a PostgreSQL interval.
1575fn format_interval(months: i32, days: i32, us: i64) -> String {
1576    let mut parts = Vec::new();
1577    if months != 0 {
1578        let years = months / 12;
1579        let mons = months % 12;
1580        if years != 0 {
1581            parts.push(format!(
1582                "{} year{}",
1583                years,
1584                if years.abs() != 1 { "s" } else { "" }
1585            ));
1586        }
1587        if mons != 0 {
1588            parts.push(format!(
1589                "{} mon{}",
1590                mons,
1591                if mons.abs() != 1 { "s" } else { "" }
1592            ));
1593        }
1594    }
1595    if days != 0 {
1596        parts.push(format!(
1597            "{} day{}",
1598            days,
1599            if days.abs() != 1 { "s" } else { "" }
1600        ));
1601    }
1602    if us != 0 || parts.is_empty() {
1603        parts.push(format_time(us));
1604    }
1605    parts.join(" ")
1606}
1607
1608/// Parse YYYY-MM-DD to PG days since 2000-01-01.
1609fn parse_date_text(s: &str) -> PgResult<i32> {
1610    let parts: Vec<&str> = s.split('-').collect();
1611    if parts.len() != 3 {
1612        return Err(PgError::TypeConversion(format!("Invalid date: {}", s)));
1613    }
1614    let y: i32 = parts[0]
1615        .parse()
1616        .map_err(|_| PgError::TypeConversion("Bad year".into()))?;
1617    let m: u32 = parts[1]
1618        .parse()
1619        .map_err(|_| PgError::TypeConversion("Bad month".into()))?;
1620    let d: u32 = parts[2]
1621        .parse()
1622        .map_err(|_| PgError::TypeConversion("Bad day".into()))?;
1623    Ok(ymd_to_days(y, m, d) - PG_EPOCH_DAYS)
1624}
1625
1626/// Parse HH:MM:SS[.ffffff] to microseconds since midnight.
1627fn parse_time_text(s: &str) -> PgResult<i64> {
1628    let parts: Vec<&str> = s.split(':').collect();
1629    if parts.len() < 2 {
1630        return Err(PgError::TypeConversion(format!("Invalid time: {}", s)));
1631    }
1632    let h: i64 = parts[0]
1633        .parse()
1634        .map_err(|_| PgError::TypeConversion("Bad hour".into()))?;
1635    let m: i64 = parts[1]
1636        .parse()
1637        .map_err(|_| PgError::TypeConversion("Bad minute".into()))?;
1638    let (s_int, frac) = if parts.len() > 2 {
1639        parse_secs_frac(parts[2])?
1640    } else {
1641        (0, 0)
1642    };
1643    Ok(h * 3_600_000_000 + m * 60_000_000 + s_int * 1_000_000 + frac)
1644}
1645
1646/// Parse a timestamp text: "YYYY-MM-DD HH:MM:SS[.ffffff][+/-TZ]"
1647fn parse_timestamp_text(s: &str) -> PgResult<i64> {
1648    // Strip timezone suffix for basic parsing
1649    let s = s.trim_end_matches("+00").trim_end_matches("+00:00");
1650    let parts: Vec<&str> = s.splitn(2, ' ').collect();
1651    if parts.len() != 2 {
1652        return Err(PgError::TypeConversion(format!("Invalid timestamp: {}", s)));
1653    }
1654    let date_days = parse_date_text(parts[0])?;
1655    let time_us = parse_time_text(parts[1])?;
1656    Ok(date_days as i64 * 86_400_000_000 + time_us)
1657}
1658
1659/// Parse a PostgreSQL interval text representation.
1660fn parse_interval_text(s: &str) -> PgResult<(i32, i32, i64)> {
1661    // Simple parser for common formats like "1 year 2 mons 3 days 04:05:06"
1662    let mut months = 0i32;
1663    let mut days = 0i32;
1664    let mut microseconds = 0i64;
1665
1666    let parts: Vec<&str> = s.split_whitespace().collect();
1667    let mut i = 0;
1668    while i < parts.len() {
1669        if parts[i].contains(':') {
1670            // Time component
1671            microseconds = parse_time_text(parts[i])?;
1672            i += 1;
1673        } else if i + 1 < parts.len() {
1674            let val: i32 = parts[i]
1675                .parse()
1676                .map_err(|_| PgError::TypeConversion(format!("Invalid interval: {}", s)))?;
1677            let unit = parts[i + 1].to_lowercase();
1678            if unit.starts_with("year") {
1679                months += val * 12;
1680            } else if unit.starts_with("mon") {
1681                months += val;
1682            } else if unit.starts_with("day") {
1683                days += val;
1684            } else if unit.starts_with("hour") {
1685                microseconds += val as i64 * 3_600_000_000;
1686            } else if unit.starts_with("min") {
1687                microseconds += val as i64 * 60_000_000;
1688            } else if unit.starts_with("sec") {
1689                microseconds += val as i64 * 1_000_000;
1690            }
1691            i += 2;
1692        } else {
1693            i += 1;
1694        }
1695    }
1696
1697    Ok((months, days, microseconds))
1698}
1699
1700/// Parse seconds with optional fractional part.
1701fn parse_secs_frac(s: &str) -> PgResult<(i64, i64)> {
1702    if let Some((int_s, frac_s)) = s.split_once('.') {
1703        let int_val: i64 = int_s
1704            .parse()
1705            .map_err(|_| PgError::TypeConversion("Bad seconds".into()))?;
1706        // Pad or truncate fractional part to 6 digits
1707        let frac_str = if frac_s.len() >= 6 {
1708            &frac_s[..6]
1709        } else {
1710            frac_s
1711        };
1712        let frac_val: i64 = frac_str
1713            .parse()
1714            .map_err(|_| PgError::TypeConversion("Bad fractional seconds".into()))?;
1715        let padding = 10i64.pow(6 - frac_str.len() as u32);
1716        Ok((int_val, frac_val * padding))
1717    } else {
1718        let int_val: i64 = s
1719            .parse()
1720            .map_err(|_| PgError::TypeConversion("Bad seconds".into()))?;
1721        Ok((int_val, 0))
1722    }
1723}
1724
1725// ─── INET / Array Helpers ────────────────────────────────────
1726
1727/// Format 16 bytes as an IPv6 address (abbreviated).
1728fn format_ipv6(bytes: &[u8]) -> String {
1729    // Build 8 groups of 16-bit values
1730    let mut groups = [0u16; 8];
1731    for i in 0..8 {
1732        groups[i] = u16::from_be_bytes([bytes[i * 2], bytes[i * 2 + 1]]);
1733    }
1734    // Simple formatting (no zero-compression for correctness)
1735    groups
1736        .iter()
1737        .map(|g| format!("{:x}", g))
1738        .collect::<Vec<_>>()
1739        .join(":")
1740}
1741
1742/// Encode an INET/CIDR text value into PostgreSQL binary format.
1743///
1744/// Accepts strings like `"192.168.1.0/24"`, `"10.0.0.1"`,
1745/// `"::1"`, `"2001:db8::/32"`.
1746pub fn encode_inet_binary(s: &str) -> PgResult<Vec<u8>> {
1747    let (addr_part, mask) = if let Some((a, m)) = s.split_once('/') {
1748        let mask: u8 = m
1749            .parse()
1750            .map_err(|_| PgError::TypeConversion(format!("Invalid mask: {}", m)))?;
1751        (a, Some(mask))
1752    } else {
1753        (s, None)
1754    };
1755
1756    // Try IPv4 first
1757    if let Some(bytes) = parse_ipv4(addr_part) {
1758        let mask = mask.unwrap_or(32);
1759        Ok(vec![2, mask, 0, 4, bytes[0], bytes[1], bytes[2], bytes[3]])
1760    } else if let Some(bytes) = parse_ipv6(addr_part) {
1761        let mask = mask.unwrap_or(128);
1762        let mut buf = vec![3, mask, 0, 16];
1763        buf.extend_from_slice(&bytes);
1764        Ok(buf)
1765    } else {
1766        Err(PgError::TypeConversion(format!(
1767            "Invalid IP address: {}",
1768            s
1769        )))
1770    }
1771}
1772
1773/// Parse an IPv4 dotted-decimal string into 4 bytes.
1774fn parse_ipv4(s: &str) -> Option<[u8; 4]> {
1775    let parts: Vec<&str> = s.split('.').collect();
1776    if parts.len() != 4 {
1777        return None;
1778    }
1779    let mut bytes = [0u8; 4];
1780    for (i, part) in parts.iter().enumerate() {
1781        bytes[i] = part.parse().ok()?;
1782    }
1783    Some(bytes)
1784}
1785
1786/// Parse an IPv6 address string into 16 bytes.
1787/// Supports `::` abbreviation.
1788fn parse_ipv6(s: &str) -> Option<[u8; 16]> {
1789    let mut bytes = [0u8; 16];
1790
1791    if s == "::" {
1792        return Some(bytes); // all zeros
1793    }
1794
1795    // Split on `::`
1796    let (left, right) = if let Some((l, r)) = s.split_once("::") {
1797        (l, r)
1798    } else {
1799        (s, "")
1800    };
1801
1802    let left_groups: Vec<&str> = if left.is_empty() {
1803        Vec::new()
1804    } else {
1805        left.split(':').collect()
1806    };
1807    let right_groups: Vec<&str> = if right.is_empty() {
1808        Vec::new()
1809    } else {
1810        right.split(':').collect()
1811    };
1812
1813    let left_count = left_groups.len();
1814    let right_count = right_groups.len();
1815
1816    if s.contains("::") {
1817        if left_count + right_count > 8 {
1818            return None;
1819        }
1820    } else if left_count != 8 {
1821        return None;
1822    }
1823
1824    // Fill from left
1825    for (i, group) in left_groups.iter().enumerate() {
1826        let val = u16::from_str_radix(group, 16).ok()?;
1827        bytes[i * 2] = (val >> 8) as u8;
1828        bytes[i * 2 + 1] = val as u8;
1829    }
1830
1831    // Fill from right (anchored to the end)
1832    let right_start = 8 - right_count;
1833    for (i, group) in right_groups.iter().enumerate() {
1834        let val = u16::from_str_radix(group, 16).ok()?;
1835        let idx = (right_start + i) * 2;
1836        bytes[idx] = (val >> 8) as u8;
1837        bytes[idx + 1] = val as u8;
1838    }
1839
1840    Some(bytes)
1841}
1842
1843/// Escape an array element for PostgreSQL text array format.
1844///
1845/// PostgreSQL rules:
1846/// - `NULL` is unquoted.
1847/// - If the element contains `{`, `}`, `"`, `,`, `\`, or whitespace, or is
1848///   empty, wrap it in double quotes and escape embedded `"` and `\` by
1849///   doubling / backslash-escaping.
1850fn escape_array_element(s: &str) -> String {
1851    if s.is_empty() || s.eq_ignore_ascii_case("null") || needs_array_quoting(s) {
1852        let mut out = String::with_capacity(s.len() + 2);
1853        out.push('"');
1854        for ch in s.chars() {
1855            if ch == '"' || ch == '\\' {
1856                out.push('\\');
1857            }
1858            out.push(ch);
1859        }
1860        out.push('"');
1861        out
1862    } else {
1863        s.to_string()
1864    }
1865}
1866
1867/// Returns true if the string needs quoting inside a PG array literal.
1868fn needs_array_quoting(s: &str) -> bool {
1869    s.chars()
1870        .any(|c| matches!(c, '{' | '}' | ',' | '"' | '\\') || c.is_whitespace())
1871}
1872
1873// ─── Calendar Helpers ────────────────────────────────────────
1874
1875/// Convert year/month/day to days since Unix epoch (1970-01-01).
1876fn ymd_to_days(y: i32, m: u32, d: u32) -> i32 {
1877    // Use the algorithm from http://howardhinnant.github.io/date_algorithms.html
1878    let y = if m <= 2 { y - 1 } else { y };
1879    let era = if y >= 0 { y } else { y - 399 } / 400;
1880    let yoe = (y - era * 400) as u32;
1881    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
1882    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
1883    era * 146097 + doe as i32 - 719468
1884}
1885
1886/// Convert days since Unix epoch to year/month/day.
1887fn days_to_ymd(days: i32) -> (i32, u32, u32) {
1888    let z = days + 719468;
1889    let era = if z >= 0 { z } else { z - 146096 } / 146097;
1890    let doe = (z - era * 146097) as u32;
1891    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1892    let y = yoe as i32 + era * 400;
1893    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1894    let mp = (5 * doy + 2) / 153;
1895    let d = doy - (153 * mp + 2) / 5 + 1;
1896    let m = if mp < 10 { mp + 3 } else { mp - 9 };
1897    let y = if m <= 2 { y + 1 } else { y };
1898    (y, m, d)
1899}
1900
1901/// Decode PostgreSQL hex-format bytea (\\x prefix).
1902fn decode_bytea_hex(s: &str) -> Vec<u8> {
1903    if let Some(hex) = s.strip_prefix("\\x") {
1904        let mut result = Vec::with_capacity(hex.len() / 2);
1905        let bytes = hex.as_bytes();
1906        let mut i = 0;
1907        while i + 1 < bytes.len() {
1908            let hi = hex_digit(bytes[i]);
1909            let lo = hex_digit(bytes[i + 1]);
1910            result.push((hi << 4) | lo);
1911            i += 2;
1912        }
1913        result
1914    } else {
1915        s.as_bytes().to_vec()
1916    }
1917}
1918
1919fn hex_digit(b: u8) -> u8 {
1920    match b {
1921        b'0'..=b'9' => b - b'0',
1922        b'a'..=b'f' => b - b'a' + 10,
1923        b'A'..=b'F' => b - b'A' + 10,
1924        _ => 0,
1925    }
1926}
1927
1928// ─── C.2: Custom Type Registry ───────────────────────────────
1929
1930/// Function type for encoding a custom type to text format.
1931pub type CustomEncoder = Box<dyn Fn(&PgValue) -> Option<Vec<u8>> + Send + Sync>;
1932/// Function type for decoding a custom type from text format.
1933pub type CustomDecoder = Box<dyn Fn(&[u8]) -> PgResult<PgValue> + Send + Sync>;
1934
1935/// A registry mapping PostgreSQL OIDs to custom encode/decode functions.
1936///
1937/// This allows extensions, custom enums, and composite types to be handled
1938/// transparently by `PgValue::from_text` and `PgValue::to_text_bytes`.
1939///
1940/// # Example
1941/// ```ignore
1942/// use chopin_pg::types::{TypeRegistry, PgValue};
1943///
1944/// let mut registry = TypeRegistry::new();
1945/// // Register a custom enum type at OID 12345
1946/// registry.register_enum(12345, &["active", "inactive", "pending"]);
1947/// let val = registry.decode(12345, b"active").unwrap();
1948/// ```
1949pub struct TypeRegistry {
1950    decoders: std::collections::HashMap<u32, CustomDecoder>,
1951    encoders: std::collections::HashMap<u32, CustomEncoder>,
1952}
1953
1954impl TypeRegistry {
1955    /// Create an empty type registry.
1956    pub fn new() -> Self {
1957        Self {
1958            decoders: std::collections::HashMap::new(),
1959            encoders: std::collections::HashMap::new(),
1960        }
1961    }
1962
1963    /// Register custom encode and decode functions for a given OID.
1964    pub fn register(&mut self, type_oid: u32, encoder: CustomEncoder, decoder: CustomDecoder) {
1965        self.encoders.insert(type_oid, encoder);
1966        self.decoders.insert(type_oid, decoder);
1967    }
1968
1969    /// Decode a value using the registered decoder for the given OID.
1970    /// Returns `None` if no decoder is registered for this OID.
1971    pub fn decode(&self, type_oid: u32, data: &[u8]) -> Option<PgResult<PgValue>> {
1972        self.decoders.get(&type_oid).map(|f| f(data))
1973    }
1974
1975    /// Encode a value using the registered encoder for the given OID.
1976    /// Returns `None` if no encoder is registered for this OID.
1977    pub fn encode(&self, type_oid: u32, value: &PgValue) -> Option<Option<Vec<u8>>> {
1978        self.encoders.get(&type_oid).map(|f| f(value))
1979    }
1980
1981    /// Check if a decoder is registered for the given OID.
1982    pub fn has_decoder(&self, type_oid: u32) -> bool {
1983        self.decoders.contains_key(&type_oid)
1984    }
1985
1986    // ── C.3: Custom PostgreSQL Enum Support ─────────────────────
1987
1988    /// Register a PostgreSQL enum type.
1989    ///
1990    /// Enum values are stored as `PgValue::Text` and validated against the
1991    /// provided variant list on decode. Encoding just returns the text bytes.
1992    ///
1993    /// # Arguments
1994    /// * `type_oid` — The OID of the enum type (query `pg_type` to find it).
1995    /// * `variants` — The valid variant labels for this enum.
1996    pub fn register_enum(&mut self, type_oid: u32, variants: &[&str]) {
1997        let variants_owned: Vec<String> = variants.iter().map(|s| s.to_string()).collect();
1998
1999        let decode_variants = variants_owned.clone();
2000        let decoder: CustomDecoder = Box::new(move |data: &[u8]| {
2001            let text = std::str::from_utf8(data)
2002                .map_err(|_| PgError::Protocol("Invalid UTF-8 in enum value".to_string()))?;
2003            if !decode_variants.iter().any(|v| v == text) {
2004                return Err(PgError::Protocol(format!(
2005                    "Unknown enum variant: '{}' (expected one of: {:?})",
2006                    text, decode_variants
2007                )));
2008            }
2009            Ok(PgValue::Text(text.to_string()))
2010        });
2011
2012        let encoder: CustomEncoder = Box::new(|value: &PgValue| match value {
2013            PgValue::Text(s) => Some(s.as_bytes().to_vec()),
2014            _ => None,
2015        });
2016
2017        self.register(type_oid, encoder, decoder);
2018    }
2019
2020    // ── C.4: Composite / Record Type Support ────────────────────
2021
2022    /// Register a composite type with named fields and their OIDs.
2023    ///
2024    /// Composite values are decoded from PostgreSQL's text representation
2025    /// (parenthesized, comma-separated fields) and stored as `PgValue::Array`
2026    /// (one element per field). Fields are decoded using their respective OIDs
2027    /// via `PgValue::from_text`.
2028    ///
2029    /// # Arguments
2030    /// * `type_oid` — The OID of the composite type.
2031    /// * `field_oids` — The OIDs of each field in order.
2032    pub fn register_composite(&mut self, type_oid: u32, field_oids: &[u32]) {
2033        let oids = field_oids.to_vec();
2034
2035        let decoder: CustomDecoder = Box::new(move |data: &[u8]| {
2036            let text = std::str::from_utf8(data)
2037                .map_err(|_| PgError::Protocol("Invalid UTF-8 in composite value".to_string()))?;
2038            // PostgreSQL text format: (val1,val2,val3)
2039            let inner = text.trim();
2040            let inner = if inner.starts_with('(') && inner.ends_with(')') {
2041                &inner[1..inner.len() - 1]
2042            } else {
2043                inner
2044            };
2045
2046            let fields = parse_composite_fields(inner);
2047            if fields.len() != oids.len() {
2048                return Err(PgError::Protocol(format!(
2049                    "Composite field count mismatch: expected {}, got {}",
2050                    oids.len(),
2051                    fields.len()
2052                )));
2053            }
2054
2055            let mut values = Vec::with_capacity(oids.len());
2056            for (field_val, &field_oid) in fields.iter().zip(oids.iter()) {
2057                if field_val.is_empty() {
2058                    values.push(PgValue::Null);
2059                } else {
2060                    values.push(PgValue::from_text(field_oid, field_val.as_bytes())?);
2061                }
2062            }
2063            Ok(PgValue::Array(values))
2064        });
2065
2066        let encoder: CustomEncoder = Box::new(|value: &PgValue| {
2067            match value {
2068                PgValue::Array(fields) => {
2069                    let mut out = String::from("(");
2070                    for (i, field) in fields.iter().enumerate() {
2071                        if i > 0 {
2072                            out.push(',');
2073                        }
2074                        match field {
2075                            PgValue::Null => {} // empty field
2076                            PgValue::Text(s) => out.push_str(s),
2077                            other => {
2078                                if let Some(bytes) = other.to_text_bytes()
2079                                    && let Ok(s) = std::str::from_utf8(&bytes)
2080                                {
2081                                    out.push_str(s);
2082                                }
2083                            }
2084                        }
2085                    }
2086                    out.push(')');
2087                    Some(out.into_bytes())
2088                }
2089                _ => None,
2090            }
2091        });
2092
2093        self.register(type_oid, encoder, decoder);
2094    }
2095}
2096
2097impl Default for TypeRegistry {
2098    fn default() -> Self {
2099        Self::new()
2100    }
2101}
2102
2103/// Parse composite field values from the inner text (without surrounding parens).
2104/// Handles quoted fields and escaped characters.
2105fn parse_composite_fields(input: &str) -> Vec<String> {
2106    let mut fields = Vec::new();
2107    let mut current = String::new();
2108    let mut in_quotes = false;
2109    let mut chars = input.chars().peekable();
2110
2111    while let Some(ch) = chars.next() {
2112        if in_quotes {
2113            if ch == '"' {
2114                if chars.peek() == Some(&'"') {
2115                    // Escaped quote
2116                    chars.next();
2117                    current.push('"');
2118                } else {
2119                    in_quotes = false;
2120                }
2121            } else {
2122                current.push(ch);
2123            }
2124        } else if ch == '"' {
2125            in_quotes = true;
2126        } else if ch == ',' {
2127            fields.push(std::mem::take(&mut current));
2128        } else {
2129            current.push(ch);
2130        }
2131    }
2132    fields.push(current);
2133    fields
2134}
2135
2136#[cfg(test)]
2137mod tests {
2138    use super::*;
2139
2140    #[test]
2141    fn test_uuid_roundtrip() {
2142        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
2143        let bytes = parse_uuid_text(uuid_str).unwrap();
2144        let formatted = format_uuid(&bytes);
2145        assert_eq!(formatted, uuid_str);
2146    }
2147
2148    #[test]
2149    fn test_date_roundtrip() {
2150        let s = "2024-03-15";
2151        let days = parse_date_text(s).unwrap();
2152        let formatted = format_date(days);
2153        assert_eq!(formatted, s);
2154    }
2155
2156    #[test]
2157    fn test_time_roundtrip() {
2158        let s = "14:30:45";
2159        let us = parse_time_text(s).unwrap();
2160        let formatted = format_time(us);
2161        assert_eq!(formatted, s);
2162    }
2163
2164    #[test]
2165    fn test_time_with_frac() {
2166        let s = "14:30:45.123456";
2167        let us = parse_time_text(s).unwrap();
2168        let formatted = format_time(us);
2169        assert_eq!(formatted, s);
2170    }
2171
2172    #[test]
2173    fn test_to_sql_i32() {
2174        assert_eq!(42i32.to_sql(), PgValue::Int4(42));
2175    }
2176
2177    #[test]
2178    fn test_from_sql_i32() {
2179        let val = PgValue::Int4(42);
2180        assert_eq!(i32::from_sql(&val).unwrap(), 42);
2181    }
2182
2183    #[test]
2184    fn test_from_sql_option() {
2185        let null = PgValue::Null;
2186        assert_eq!(Option::<i32>::from_sql(&null).unwrap(), None);
2187        let val = PgValue::Int4(42);
2188        assert_eq!(Option::<i32>::from_sql(&val).unwrap(), Some(42));
2189    }
2190
2191    #[test]
2192    fn test_pg_epoch() {
2193        // 2000-01-01 should be day 0 in PG
2194        assert_eq!(parse_date_text("2000-01-01").unwrap(), 0);
2195        // 2000-01-02 should be day 1
2196        assert_eq!(parse_date_text("2000-01-02").unwrap(), 1);
2197    }
2198
2199    // ─── INET binary format tests ─────────────────────────────
2200
2201    #[test]
2202    fn test_inet_binary_ipv4() {
2203        // Binary: family=2, mask=32, is_cidr=0, len=4, addr=192.168.1.1
2204        let data = vec![2, 32, 0, 4, 192, 168, 1, 1];
2205        let val = PgValue::from_binary(oid::INET, &data).unwrap();
2206        assert_eq!(val, PgValue::Inet("192.168.1.1".to_string()));
2207    }
2208
2209    #[test]
2210    fn test_inet_binary_ipv4_cidr() {
2211        // CIDR: 10.0.0.0/8
2212        let data = vec![2, 8, 1, 4, 10, 0, 0, 0];
2213        let val = PgValue::from_binary(oid::CIDR, &data).unwrap();
2214        assert_eq!(val, PgValue::Inet("10.0.0.0/8".to_string()));
2215    }
2216
2217    #[test]
2218    fn test_inet_binary_ipv6() {
2219        // ::1 in binary: family=3, mask=128, is_cidr=0, len=16
2220        let mut data = vec![3, 128, 0, 16];
2221        data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
2222        let val = PgValue::from_binary(oid::INET, &data).unwrap();
2223        assert_eq!(val, PgValue::Inet("0:0:0:0:0:0:0:1".to_string()));
2224    }
2225
2226    #[test]
2227    fn test_encode_inet_binary_ipv4() {
2228        let encoded = encode_inet_binary("192.168.1.1").unwrap();
2229        assert_eq!(encoded, vec![2, 32, 0, 4, 192, 168, 1, 1]);
2230    }
2231
2232    #[test]
2233    fn test_encode_inet_binary_ipv4_cidr() {
2234        let encoded = encode_inet_binary("10.0.0.0/8").unwrap();
2235        assert_eq!(encoded, vec![2, 8, 0, 4, 10, 0, 0, 0]);
2236    }
2237
2238    #[test]
2239    fn test_encode_inet_binary_ipv6_loopback() {
2240        let encoded = encode_inet_binary("::1").unwrap();
2241        assert_eq!(encoded.len(), 20); // 4 header + 16 addr
2242        assert_eq!(encoded[0], 3); // AF_INET6
2243        assert_eq!(encoded[1], 128); // /128
2244        assert_eq!(encoded[19], 1); // last byte = 1
2245    }
2246
2247    // ─── Array escaping tests ─────────────────────────────────
2248
2249    #[test]
2250    fn test_array_simple() {
2251        let arr = PgValue::Array(vec![PgValue::Int4(1), PgValue::Int4(2), PgValue::Int4(3)]);
2252        let bytes = arr.to_text_bytes().unwrap();
2253        assert_eq!(String::from_utf8(bytes).unwrap(), "{1,2,3}");
2254    }
2255
2256    #[test]
2257    fn test_array_with_null() {
2258        let arr = PgValue::Array(vec![
2259            PgValue::Text("hello".to_string()),
2260            PgValue::Null,
2261            PgValue::Text("world".to_string()),
2262        ]);
2263        let bytes = arr.to_text_bytes().unwrap();
2264        assert_eq!(String::from_utf8(bytes).unwrap(), "{hello,NULL,world}");
2265    }
2266
2267    #[test]
2268    fn test_array_escaping_special_chars() {
2269        let arr = PgValue::Array(vec![
2270            PgValue::Text("hello world".to_string()), // contains space
2271            PgValue::Text("a,b".to_string()),         // contains comma
2272            PgValue::Text("say \"hi\"".to_string()),  // contains quotes
2273        ]);
2274        let bytes = arr.to_text_bytes().unwrap();
2275        let s = String::from_utf8(bytes).unwrap();
2276        assert_eq!(s, r#"{"hello world","a,b","say \"hi\""}"#);
2277    }
2278
2279    #[test]
2280    fn test_array_escaping_empty_string() {
2281        let arr = PgValue::Array(vec![PgValue::Text("".to_string())]);
2282        let bytes = arr.to_text_bytes().unwrap();
2283        assert_eq!(String::from_utf8(bytes).unwrap(), r#"{""}"#);
2284    }
2285
2286    #[test]
2287    fn test_array_escaping_null_string() {
2288        // A text value that literally says "NULL" should be quoted
2289        let arr = PgValue::Array(vec![PgValue::Text("NULL".to_string())]);
2290        let bytes = arr.to_text_bytes().unwrap();
2291        assert_eq!(String::from_utf8(bytes).unwrap(), r#"{"NULL"}"#);
2292    }
2293
2294    // ─── IPv6 parse / format tests ────────────────────────────
2295
2296    #[test]
2297    fn test_parse_ipv6_loopback() {
2298        let bytes = parse_ipv6("::1").unwrap();
2299        let mut expected = [0u8; 16];
2300        expected[15] = 1;
2301        assert_eq!(bytes, expected);
2302    }
2303
2304    #[test]
2305    fn test_parse_ipv6_full() {
2306        let bytes = parse_ipv6("2001:db8:0:0:0:0:0:1").unwrap();
2307        assert_eq!(bytes[0], 0x20);
2308        assert_eq!(bytes[1], 0x01);
2309        assert_eq!(bytes[2], 0x0d);
2310        assert_eq!(bytes[3], 0xb8);
2311        assert_eq!(bytes[15], 1);
2312    }
2313
2314    #[test]
2315    fn test_parse_ipv6_abbreviated() {
2316        let bytes = parse_ipv6("2001:db8::1").unwrap();
2317        assert_eq!(bytes[0], 0x20);
2318        assert_eq!(bytes[1], 0x01);
2319        assert_eq!(bytes[15], 1);
2320    }
2321
2322    // ─── Sprint 1: FromSql for Vec<u8> and [u8; 16] ──────────
2323
2324    #[test]
2325    fn test_from_sql_vec_u8() {
2326        let val = PgValue::Bytes(vec![1, 2, 3]);
2327        assert_eq!(Vec::<u8>::from_sql(&val).unwrap(), vec![1, 2, 3]);
2328    }
2329
2330    #[test]
2331    fn test_from_sql_vec_u8_null() {
2332        let val = PgValue::Null;
2333        assert!(Vec::<u8>::from_sql(&val).is_err());
2334    }
2335
2336    #[test]
2337    fn test_from_sql_uuid_bytes() {
2338        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
2339        let val = PgValue::Uuid(bytes);
2340        assert_eq!(<[u8; 16]>::from_sql(&val).unwrap(), bytes);
2341    }
2342
2343    #[test]
2344    fn test_from_sql_uuid_bytes_null() {
2345        let val = PgValue::Null;
2346        assert!(<[u8; 16]>::from_sql(&val).is_err());
2347    }
2348
2349    // ─── Sprint 1: Array ToSql / FromSql ──────────────────────
2350
2351    #[test]
2352    fn test_to_sql_vec_i32() {
2353        let arr = vec![1i32, 2, 3];
2354        assert_eq!(
2355            arr.to_sql(),
2356            PgValue::Array(vec![PgValue::Int4(1), PgValue::Int4(2), PgValue::Int4(3)])
2357        );
2358    }
2359
2360    #[test]
2361    fn test_to_sql_vec_i32_type_oid() {
2362        let arr = vec![1i32, 2, 3];
2363        assert_eq!(arr.type_oid(), oid::INT4_ARRAY);
2364    }
2365
2366    #[test]
2367    fn test_from_sql_vec_i32() {
2368        let val = PgValue::Array(vec![PgValue::Int4(1), PgValue::Int4(2), PgValue::Int4(3)]);
2369        assert_eq!(Vec::<i32>::from_sql(&val).unwrap(), vec![1, 2, 3]);
2370    }
2371
2372    #[test]
2373    fn test_to_sql_vec_string() {
2374        let arr = vec!["hello".to_string(), "world".to_string()];
2375        assert_eq!(
2376            arr.to_sql(),
2377            PgValue::Array(vec![
2378                PgValue::Text("hello".to_string()),
2379                PgValue::Text("world".to_string())
2380            ])
2381        );
2382    }
2383
2384    #[test]
2385    fn test_from_sql_vec_string() {
2386        let val = PgValue::Array(vec![
2387            PgValue::Text("hello".to_string()),
2388            PgValue::Text("world".to_string()),
2389        ]);
2390        assert_eq!(
2391            Vec::<String>::from_sql(&val).unwrap(),
2392            vec!["hello".to_string(), "world".to_string()]
2393        );
2394    }
2395
2396    #[test]
2397    fn test_to_sql_vec_bool() {
2398        let arr = vec![true, false, true];
2399        assert_eq!(
2400            arr.to_sql(),
2401            PgValue::Array(vec![
2402                PgValue::Bool(true),
2403                PgValue::Bool(false),
2404                PgValue::Bool(true)
2405            ])
2406        );
2407    }
2408
2409    #[test]
2410    fn test_from_sql_vec_bool() {
2411        let val = PgValue::Array(vec![PgValue::Bool(true), PgValue::Bool(false)]);
2412        assert_eq!(Vec::<bool>::from_sql(&val).unwrap(), vec![true, false]);
2413    }
2414
2415    #[test]
2416    fn test_to_sql_vec_f64() {
2417        let arr = vec![1.5f64, 2.5];
2418        assert_eq!(
2419            arr.to_sql(),
2420            PgValue::Array(vec![PgValue::Float8(1.5), PgValue::Float8(2.5)])
2421        );
2422    }
2423
2424    #[test]
2425    fn test_from_sql_vec_f64() {
2426        let val = PgValue::Array(vec![PgValue::Float8(1.5), PgValue::Float8(2.5)]);
2427        assert_eq!(Vec::<f64>::from_sql(&val).unwrap(), vec![1.5, 2.5]);
2428    }
2429
2430    #[test]
2431    fn test_to_sql_slice_i32() {
2432        let arr: &[i32] = &[10, 20, 30];
2433        assert_eq!(
2434            arr.to_sql(),
2435            PgValue::Array(vec![
2436                PgValue::Int4(10),
2437                PgValue::Int4(20),
2438                PgValue::Int4(30)
2439            ])
2440        );
2441    }
2442
2443    #[test]
2444    fn test_from_sql_vec_i64() {
2445        let val = PgValue::Array(vec![PgValue::Int8(100), PgValue::Int8(200)]);
2446        assert_eq!(Vec::<i64>::from_sql(&val).unwrap(), vec![100i64, 200]);
2447    }
2448
2449    #[test]
2450    fn test_from_sql_empty_array() {
2451        let val = PgValue::Array(vec![]);
2452        assert_eq!(Vec::<i32>::from_sql(&val).unwrap(), Vec::<i32>::new());
2453    }
2454
2455    // ─── Sprint 1: Network type ToSql / FromSql ──────────────
2456
2457    #[test]
2458    fn test_to_sql_ipaddr_v4() {
2459        use std::net::{IpAddr, Ipv4Addr};
2460        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
2461        assert_eq!(ip.to_sql(), PgValue::Inet("192.168.1.1".to_string()));
2462        assert_eq!(ip.type_oid(), oid::INET);
2463    }
2464
2465    #[test]
2466    fn test_to_sql_ipaddr_v6() {
2467        use std::net::{IpAddr, Ipv6Addr};
2468        let ip = IpAddr::V6(Ipv6Addr::LOCALHOST);
2469        assert_eq!(ip.to_sql(), PgValue::Inet("::1".to_string()));
2470    }
2471
2472    #[test]
2473    fn test_to_sql_ipv4addr() {
2474        use std::net::Ipv4Addr;
2475        let ip = Ipv4Addr::new(10, 0, 0, 1);
2476        assert_eq!(ip.to_sql(), PgValue::Inet("10.0.0.1".to_string()));
2477    }
2478
2479    #[test]
2480    fn test_to_sql_ipv6addr() {
2481        use std::net::Ipv6Addr;
2482        let ip = Ipv6Addr::LOCALHOST;
2483        assert_eq!(ip.to_sql(), PgValue::Inet("::1".to_string()));
2484    }
2485
2486    #[test]
2487    fn test_from_sql_ipaddr() {
2488        use std::net::{IpAddr, Ipv4Addr};
2489        let val = PgValue::Inet("192.168.1.1".to_string());
2490        let ip: IpAddr = IpAddr::from_sql(&val).unwrap();
2491        assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
2492    }
2493
2494    #[test]
2495    fn test_from_sql_ipaddr_with_cidr() {
2496        use std::net::{IpAddr, Ipv4Addr};
2497        // Should strip the CIDR mask when converting to IpAddr
2498        let val = PgValue::Inet("10.0.0.0/8".to_string());
2499        let ip: IpAddr = IpAddr::from_sql(&val).unwrap();
2500        assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)));
2501    }
2502
2503    #[test]
2504    fn test_from_sql_ipv4addr() {
2505        use std::net::Ipv4Addr;
2506        let val = PgValue::Inet("10.0.0.1".to_string());
2507        let ip = Ipv4Addr::from_sql(&val).unwrap();
2508        assert_eq!(ip, Ipv4Addr::new(10, 0, 0, 1));
2509    }
2510
2511    #[test]
2512    fn test_from_sql_ipv6addr() {
2513        use std::net::Ipv6Addr;
2514        let val = PgValue::Inet("::1".to_string());
2515        let ip = Ipv6Addr::from_sql(&val).unwrap();
2516        assert_eq!(ip, Ipv6Addr::LOCALHOST);
2517    }
2518
2519    #[test]
2520    fn test_from_sql_ipaddr_null() {
2521        use std::net::IpAddr;
2522        let val = PgValue::Null;
2523        assert!(IpAddr::from_sql(&val).is_err());
2524    }
2525
2526    // ─── Sprint 3: Binary encoding/decoding ───────────────────
2527
2528    #[test]
2529    fn test_to_binary_bytes_bool() {
2530        assert_eq!(PgValue::Bool(true).to_binary_bytes(), Some(vec![1]));
2531        assert_eq!(PgValue::Bool(false).to_binary_bytes(), Some(vec![0]));
2532    }
2533
2534    #[test]
2535    fn test_to_binary_bytes_int2() {
2536        let val = PgValue::Int2(256);
2537        assert_eq!(val.to_binary_bytes(), Some(vec![1, 0]));
2538    }
2539
2540    #[test]
2541    fn test_to_binary_bytes_int4() {
2542        let val = PgValue::Int4(0x01020304);
2543        assert_eq!(val.to_binary_bytes(), Some(vec![1, 2, 3, 4]));
2544    }
2545
2546    #[test]
2547    fn test_to_binary_bytes_int8() {
2548        let val = PgValue::Int8(1);
2549        assert_eq!(val.to_binary_bytes(), Some(vec![0, 0, 0, 0, 0, 0, 0, 1]));
2550    }
2551
2552    #[test]
2553    fn test_to_binary_bytes_float4() {
2554        let val = PgValue::Float4(1.0);
2555        assert_eq!(val.to_binary_bytes(), Some(1.0_f32.to_be_bytes().to_vec()));
2556    }
2557
2558    #[test]
2559    fn test_to_binary_bytes_float8() {
2560        let val = PgValue::Float8(std::f64::consts::PI);
2561        assert_eq!(
2562            val.to_binary_bytes(),
2563            Some(std::f64::consts::PI.to_be_bytes().to_vec())
2564        );
2565    }
2566
2567    #[test]
2568    fn test_to_binary_bytes_uuid() {
2569        let bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
2570        let val = PgValue::Uuid(bytes);
2571        assert_eq!(val.to_binary_bytes(), Some(bytes.to_vec()));
2572    }
2573
2574    #[test]
2575    fn test_to_binary_bytes_date() {
2576        // 2000-01-01 = day 0
2577        let val = PgValue::Date(0);
2578        assert_eq!(val.to_binary_bytes(), Some(vec![0, 0, 0, 0]));
2579    }
2580
2581    #[test]
2582    fn test_to_binary_bytes_interval() {
2583        let val = PgValue::Interval {
2584            months: 1,
2585            days: 2,
2586            microseconds: 3_000_000,
2587        };
2588        let mut expected = Vec::new();
2589        expected.extend_from_slice(&3_000_000_i64.to_be_bytes());
2590        expected.extend_from_slice(&2_i32.to_be_bytes());
2591        expected.extend_from_slice(&1_i32.to_be_bytes());
2592        assert_eq!(val.to_binary_bytes(), Some(expected));
2593    }
2594
2595    #[test]
2596    fn test_to_binary_bytes_jsonb() {
2597        let val = PgValue::Jsonb(b"{}".to_vec());
2598        assert_eq!(val.to_binary_bytes(), Some(vec![1, b'{', b'}']));
2599    }
2600
2601    #[test]
2602    fn test_to_binary_bytes_null() {
2603        assert_eq!(PgValue::Null.to_binary_bytes(), None);
2604    }
2605
2606    #[test]
2607    fn test_to_binary_bytes_text() {
2608        let val = PgValue::Text("hello".to_string());
2609        assert_eq!(val.to_binary_bytes(), Some(b"hello".to_vec()));
2610    }
2611
2612    #[test]
2613    fn test_prefers_binary() {
2614        assert!(PgValue::Int4(1).prefers_binary());
2615        assert!(PgValue::Bool(true).prefers_binary());
2616        assert!(PgValue::Float8(1.0).prefers_binary());
2617        assert!(PgValue::Uuid([0; 16]).prefers_binary());
2618        assert!(!PgValue::Text("hi".into()).prefers_binary());
2619        assert!(!PgValue::Numeric("1.23".into()).prefers_binary());
2620        assert!(!PgValue::Array(vec![]).prefers_binary());
2621        assert!(!PgValue::Inet("127.0.0.1".into()).prefers_binary());
2622    }
2623
2624    #[test]
2625    fn test_from_binary_numeric_zero() {
2626        // ndigits=0, weight=0, sign=0 (pos), dscale=0
2627        let data = [0u8, 0, 0, 0, 0, 0, 0, 0];
2628        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2629        assert_eq!(val, PgValue::Numeric("0".to_string()));
2630    }
2631
2632    #[test]
2633    fn test_from_binary_numeric_simple_integer() {
2634        // Value: 42
2635        // ndigits=1, weight=0, sign=0(pos), dscale=0, digit=42
2636        let data = [
2637            0, 1, // ndigits = 1
2638            0, 0, // weight = 0
2639            0, 0, // sign = positive
2640            0, 0, // dscale = 0
2641            0, 42, // digit[0] = 42
2642        ];
2643        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2644        assert_eq!(val, PgValue::Numeric("42".to_string()));
2645    }
2646
2647    #[test]
2648    fn test_from_binary_numeric_negative() {
2649        // Value: -42
2650        // ndigits=1, weight=0, sign=0x4000(neg), dscale=0, digit=42
2651        let data = [
2652            0, 1, // ndigits = 1
2653            0, 0, // weight = 0
2654            0x40, 0x00, // sign = NUMERIC_NEG
2655            0, 0, // dscale = 0
2656            0, 42, // digit[0] = 42
2657        ];
2658        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2659        assert_eq!(val, PgValue::Numeric("-42".to_string()));
2660    }
2661
2662    #[test]
2663    fn test_from_binary_numeric_with_decimal() {
2664        // Value: 1.23
2665        // ndigits=2, weight=0, sign=0(pos), dscale=2
2666        // digit[0]=1 (integer part), digit[1]=2300 (.2300 → 2 decimal places)
2667        let data = [
2668            0, 2, // ndigits = 2
2669            0, 0, // weight = 0
2670            0, 0, // sign = positive
2671            0, 2, // dscale = 2
2672            0, 1, // digit[0] = 1
2673            0x08, 0xFC, // digit[1] = 2300
2674        ];
2675        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2676        assert_eq!(val, PgValue::Numeric("1.23".to_string()));
2677    }
2678
2679    #[test]
2680    fn test_from_binary_numeric_nan() {
2681        // NaN: ndigits=0, weight=0, sign=0xC000, dscale=0
2682        let data = [0, 0, 0, 0, 0xC0, 0x00, 0, 0];
2683        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2684        assert_eq!(val, PgValue::Numeric("NaN".to_string()));
2685    }
2686
2687    #[test]
2688    fn test_from_binary_numeric_zero_with_scale() {
2689        // 0.00: ndigits=0, weight=0, sign=0, dscale=2
2690        let data = [0, 0, 0, 0, 0, 0, 0, 2];
2691        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2692        assert_eq!(val, PgValue::Numeric("0.00".to_string()));
2693    }
2694
2695    #[test]
2696    fn test_from_binary_numeric_large() {
2697        // Value: 10000 (weight=1, digit=1 → 1*10000^(1+1-1) = 1*10000 = 10000)
2698        // Actually: weight=1 means 2 digit groups before decimal
2699        // digit[0]=1 → "1" then pad 4 zeros for next group = "10000"
2700        let data = [
2701            0, 1, // ndigits = 1
2702            0, 1, // weight = 1
2703            0, 0, // sign = positive
2704            0, 0, // dscale = 0
2705            0, 1, // digit[0] = 1
2706        ];
2707        let val = PgValue::from_binary(oid::NUMERIC, &data).unwrap();
2708        assert_eq!(val, PgValue::Numeric("10000".to_string()));
2709    }
2710
2711    #[test]
2712    fn test_from_binary_array_i32() {
2713        // Binary array: 1 dimension, no nulls, element OID = INT4 (23)
2714        // dimension: len=3, lower_bound=1
2715        // elements: 10, 20, 30
2716        let mut data = Vec::new();
2717        data.extend_from_slice(&1_i32.to_be_bytes()); // ndim = 1
2718        data.extend_from_slice(&0_i32.to_be_bytes()); // flags = 0
2719        data.extend_from_slice(&23_u32.to_be_bytes()); // element OID = INT4
2720        data.extend_from_slice(&3_i32.to_be_bytes()); // dim length = 3
2721        data.extend_from_slice(&1_i32.to_be_bytes()); // lower bound = 1
2722        // element 0: 10
2723        data.extend_from_slice(&4_i32.to_be_bytes()); // len = 4
2724        data.extend_from_slice(&10_i32.to_be_bytes()); // value = 10
2725        // element 1: 20
2726        data.extend_from_slice(&4_i32.to_be_bytes());
2727        data.extend_from_slice(&20_i32.to_be_bytes());
2728        // element 2: 30
2729        data.extend_from_slice(&4_i32.to_be_bytes());
2730        data.extend_from_slice(&30_i32.to_be_bytes());
2731
2732        let val = PgValue::from_binary(oid::INT4_ARRAY, &data).unwrap();
2733        assert_eq!(
2734            val,
2735            PgValue::Array(vec![
2736                PgValue::Int4(10),
2737                PgValue::Int4(20),
2738                PgValue::Int4(30),
2739            ])
2740        );
2741    }
2742
2743    #[test]
2744    fn test_from_binary_array_with_null() {
2745        // Binary array with a NULL element
2746        let mut data = Vec::new();
2747        data.extend_from_slice(&1_i32.to_be_bytes()); // ndim = 1
2748        data.extend_from_slice(&1_i32.to_be_bytes()); // flags = has_null
2749        data.extend_from_slice(&23_u32.to_be_bytes()); // element OID = INT4
2750        data.extend_from_slice(&2_i32.to_be_bytes()); // dim length = 2
2751        data.extend_from_slice(&1_i32.to_be_bytes()); // lower bound = 1
2752        // element 0: 42
2753        data.extend_from_slice(&4_i32.to_be_bytes());
2754        data.extend_from_slice(&42_i32.to_be_bytes());
2755        // element 1: NULL
2756        data.extend_from_slice(&(-1_i32).to_be_bytes());
2757
2758        let val = PgValue::from_binary(oid::INT4_ARRAY, &data).unwrap();
2759        assert_eq!(val, PgValue::Array(vec![PgValue::Int4(42), PgValue::Null,]));
2760    }
2761
2762    #[test]
2763    fn test_from_binary_array_empty() {
2764        // ndim=0 → empty array
2765        let mut data = Vec::new();
2766        data.extend_from_slice(&0_i32.to_be_bytes()); // ndim = 0
2767        data.extend_from_slice(&0_i32.to_be_bytes()); // flags = 0
2768        data.extend_from_slice(&23_u32.to_be_bytes()); // element OID
2769
2770        let val = PgValue::from_binary(oid::INT4_ARRAY, &data).unwrap();
2771        assert_eq!(val, PgValue::Array(Vec::new()));
2772    }
2773
2774    #[test]
2775    fn test_from_binary_array_bool() {
2776        let mut data = Vec::new();
2777        data.extend_from_slice(&1_i32.to_be_bytes()); // ndim = 1
2778        data.extend_from_slice(&0_i32.to_be_bytes()); // flags
2779        data.extend_from_slice(&16_u32.to_be_bytes()); // element OID = BOOL
2780        data.extend_from_slice(&2_i32.to_be_bytes()); // dim length = 2
2781        data.extend_from_slice(&1_i32.to_be_bytes()); // lower bound
2782        // true
2783        data.extend_from_slice(&1_i32.to_be_bytes()); // len = 1
2784        data.push(1);
2785        // false
2786        data.extend_from_slice(&1_i32.to_be_bytes());
2787        data.push(0);
2788
2789        let val = PgValue::from_binary(oid::BOOL_ARRAY, &data).unwrap();
2790        assert_eq!(
2791            val,
2792            PgValue::Array(vec![PgValue::Bool(true), PgValue::Bool(false)])
2793        );
2794    }
2795
2796    #[test]
2797    fn test_from_binary_array_float8() {
2798        let mut data = Vec::new();
2799        data.extend_from_slice(&1_i32.to_be_bytes());
2800        data.extend_from_slice(&0_i32.to_be_bytes());
2801        data.extend_from_slice(&701_u32.to_be_bytes()); // FLOAT8
2802        data.extend_from_slice(&2_i32.to_be_bytes());
2803        data.extend_from_slice(&1_i32.to_be_bytes());
2804        // 1.5
2805        data.extend_from_slice(&8_i32.to_be_bytes());
2806        data.extend_from_slice(&1.5_f64.to_be_bytes());
2807        // 2.5
2808        data.extend_from_slice(&8_i32.to_be_bytes());
2809        data.extend_from_slice(&2.5_f64.to_be_bytes());
2810
2811        let val = PgValue::from_binary(oid::FLOAT8_ARRAY, &data).unwrap();
2812        assert_eq!(
2813            val,
2814            PgValue::Array(vec![PgValue::Float8(1.5), PgValue::Float8(2.5)])
2815        );
2816    }
2817
2818    #[test]
2819    fn test_binary_roundtrip_int4() {
2820        let original = PgValue::Int4(12345);
2821        let bytes = original.to_binary_bytes().unwrap();
2822        let decoded = PgValue::from_binary(oid::INT4, &bytes).unwrap();
2823        assert_eq!(original, decoded);
2824    }
2825
2826    #[test]
2827    fn test_binary_roundtrip_float8() {
2828        let original = PgValue::Float8(std::f64::consts::PI);
2829        let bytes = original.to_binary_bytes().unwrap();
2830        let decoded = PgValue::from_binary(oid::FLOAT8, &bytes).unwrap();
2831        assert_eq!(original, decoded);
2832    }
2833
2834    #[test]
2835    fn test_binary_roundtrip_uuid() {
2836        let original = PgValue::Uuid([
2837            0xDE, 0xAD, 0xBE, 0xEF, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
2838        ]);
2839        let bytes = original.to_binary_bytes().unwrap();
2840        let decoded = PgValue::from_binary(oid::UUID, &bytes).unwrap();
2841        assert_eq!(original, decoded);
2842    }
2843
2844    #[test]
2845    fn test_binary_roundtrip_interval() {
2846        let original = PgValue::Interval {
2847            months: 13,
2848            days: 5,
2849            microseconds: 7_200_000_000,
2850        };
2851        let bytes = original.to_binary_bytes().unwrap();
2852        let decoded = PgValue::from_binary(oid::INTERVAL, &bytes).unwrap();
2853        assert_eq!(original, decoded);
2854    }
2855
2856    #[test]
2857    fn test_binary_roundtrip_bool() {
2858        for b in [true, false] {
2859            let original = PgValue::Bool(b);
2860            let bytes = original.to_binary_bytes().unwrap();
2861            let decoded = PgValue::from_binary(oid::BOOL, &bytes).unwrap();
2862            assert_eq!(original, decoded);
2863        }
2864    }
2865
2866    // ─── MacAddr Tests ────────────────────────────────────────────
2867
2868    #[test]
2869    fn test_macaddr_text_roundtrip() {
2870        let mac = [0x08, 0x00, 0x2b, 0x01, 0x02, 0x03];
2871        let val = PgValue::MacAddr(mac);
2872        let text = val.to_text_bytes().unwrap();
2873        assert_eq!(std::str::from_utf8(&text).unwrap(), "08:00:2b:01:02:03");
2874        let decoded = PgValue::from_text(oid::MACADDR, b"08:00:2b:01:02:03").unwrap();
2875        assert_eq!(decoded, PgValue::MacAddr(mac));
2876    }
2877
2878    #[test]
2879    fn test_macaddr_binary_roundtrip() {
2880        let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
2881        let val = PgValue::MacAddr(mac);
2882        let bytes = val.to_binary_bytes().unwrap();
2883        assert_eq!(bytes, vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
2884        let decoded = PgValue::from_binary(oid::MACADDR, &bytes).unwrap();
2885        assert_eq!(decoded, PgValue::MacAddr(mac));
2886    }
2887
2888    #[test]
2889    fn test_macaddr_dash_format() {
2890        let decoded = parse_macaddr_text("08-00-2b-01-02-03").unwrap();
2891        assert_eq!(decoded, [0x08, 0x00, 0x2b, 0x01, 0x02, 0x03]);
2892    }
2893
2894    #[test]
2895    fn test_macaddr_invalid() {
2896        assert!(parse_macaddr_text("not_a_mac").is_err());
2897        assert!(parse_macaddr_text("08:00:2b").is_err()); // too few
2898        assert!(parse_macaddr_text("08:00:2b:01:02:03:04").is_err()); // too many
2899        assert!(parse_macaddr_text("ZZ:00:2b:01:02:03").is_err()); // bad hex
2900    }
2901
2902    #[test]
2903    fn test_macaddr_tosql_fromsql() {
2904        let mac: [u8; 6] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB];
2905        let val = mac.to_sql();
2906        assert!(matches!(val, PgValue::MacAddr(_)));
2907        assert_eq!(mac.type_oid(), oid::MACADDR);
2908        let decoded: [u8; 6] = FromSql::from_sql(&val).unwrap();
2909        assert_eq!(decoded, mac);
2910    }
2911
2912    // ─── Point Tests ──────────────────────────────────────────────
2913
2914    #[test]
2915    fn test_point_text_roundtrip() {
2916        let val = PgValue::Point { x: 1.5, y: -2.5 };
2917        let text = val.to_text_bytes().unwrap();
2918        assert_eq!(std::str::from_utf8(&text).unwrap(), "(1.5,-2.5)");
2919        let decoded = PgValue::from_text(oid::POINT, b"(1.5,-2.5)").unwrap();
2920        match decoded {
2921            PgValue::Point { x, y } => {
2922                assert!((x - 1.5).abs() < f64::EPSILON);
2923                assert!((y - (-2.5)).abs() < f64::EPSILON);
2924            }
2925            _ => panic!("Expected Point"),
2926        }
2927    }
2928
2929    #[test]
2930    fn test_point_binary_roundtrip() {
2931        let val = PgValue::Point {
2932            x: std::f64::consts::PI,
2933            y: std::f64::consts::E,
2934        };
2935        let bytes = val.to_binary_bytes().unwrap();
2936        assert_eq!(bytes.len(), 16);
2937        let decoded = PgValue::from_binary(oid::POINT, &bytes).unwrap();
2938        match decoded {
2939            PgValue::Point { x, y } => {
2940                assert!((x - std::f64::consts::PI).abs() < f64::EPSILON);
2941                assert!((y - std::f64::consts::E).abs() < f64::EPSILON);
2942            }
2943            _ => panic!("Expected Point"),
2944        }
2945    }
2946
2947    #[test]
2948    fn test_point_tosql_fromsql() {
2949        let pt: (f64, f64) = (10.0, 20.0);
2950        let val = pt.to_sql();
2951        assert!(matches!(val, PgValue::Point { .. }));
2952        assert_eq!(pt.type_oid(), oid::POINT);
2953        let decoded: (f64, f64) = FromSql::from_sql(&val).unwrap();
2954        assert!((decoded.0 - 10.0).abs() < f64::EPSILON);
2955        assert!((decoded.1 - 20.0).abs() < f64::EPSILON);
2956    }
2957
2958    #[test]
2959    fn test_point_parse_without_parens() {
2960        let (x, y) = parse_point_text("3.5,7.2").unwrap();
2961        assert!((x - 3.5).abs() < f64::EPSILON);
2962        assert!((y - 7.2).abs() < f64::EPSILON);
2963    }
2964
2965    #[test]
2966    fn test_point_parse_with_spaces() {
2967        let (x, y) = parse_point_text("( 3.5 , 7.2 )").unwrap();
2968        assert!((x - 3.5).abs() < f64::EPSILON);
2969        assert!((y - 7.2).abs() < f64::EPSILON);
2970    }
2971
2972    #[test]
2973    fn test_point_parse_invalid() {
2974        assert!(parse_point_text("nope").is_err());
2975        assert!(parse_point_text("(1.0)").is_err());
2976    }
2977
2978    // ─── Range Tests ──────────────────────────────────────────────
2979
2980    #[test]
2981    fn test_range_text_roundtrip() {
2982        let val = PgValue::Range("[1,10)".to_string());
2983        let text = val.to_text_bytes().unwrap();
2984        assert_eq!(std::str::from_utf8(&text).unwrap(), "[1,10)");
2985    }
2986
2987    #[test]
2988    fn test_range_from_text_int4range() {
2989        let decoded = PgValue::from_text(oid::INT4RANGE, b"[1,10)").unwrap();
2990        assert_eq!(decoded, PgValue::Range("[1,10)".to_string()));
2991    }
2992
2993    #[test]
2994    fn test_range_from_text_tsrange() {
2995        let decoded = PgValue::from_text(oid::TSRANGE, b"[2024-01-01,2024-12-31]").unwrap();
2996        assert_eq!(
2997            decoded,
2998            PgValue::Range("[2024-01-01,2024-12-31]".to_string())
2999        );
3000    }
3001
3002    #[test]
3003    fn test_range_empty() {
3004        let decoded = PgValue::from_text(oid::INT4RANGE, b"empty").unwrap();
3005        assert_eq!(decoded, PgValue::Range("empty".to_string()));
3006    }
3007
3008    #[test]
3009    fn test_range_all_oid_variants() {
3010        for oid_val in [
3011            oid::INT4RANGE,
3012            oid::INT8RANGE,
3013            oid::NUMRANGE,
3014            oid::TSRANGE,
3015            oid::TSTZRANGE,
3016            oid::DATERANGE,
3017        ] {
3018            let decoded = PgValue::from_text(oid_val, b"[1,10)").unwrap();
3019            assert!(matches!(decoded, PgValue::Range(_)));
3020        }
3021    }
3022
3023    // ─── Unix Socket Config Tests ─────────────────────────────────
3024
3025    #[test]
3026    fn test_pgconfig_with_socket_dir() {
3027        use crate::connection::PgConfig;
3028        let config = PgConfig::new("localhost", 5432, "user", "pass", "mydb")
3029            .with_socket_dir("/var/run/postgresql");
3030        assert_eq!(config.socket_dir, Some("/var/run/postgresql".to_string()));
3031    }
3032
3033    #[test]
3034    fn test_pgconfig_from_url_unix_query_param() {
3035        use crate::connection::PgConfig;
3036        let config =
3037            PgConfig::from_url("postgres://user:pass@/mydb?host=/var/run/postgresql").unwrap();
3038        assert_eq!(config.socket_dir, Some("/var/run/postgresql".to_string()));
3039        assert_eq!(config.database, "mydb");
3040        assert_eq!(config.user, "user");
3041    }
3042
3043    #[test]
3044    fn test_pgconfig_from_url_percent_encoded() {
3045        use crate::connection::PgConfig;
3046        let config =
3047            PgConfig::from_url("postgres://user:pass@%2Fvar%2Frun%2Fpostgresql/mydb").unwrap();
3048        assert_eq!(config.socket_dir, Some("/var/run/postgresql".to_string()));
3049        assert_eq!(config.database, "mydb");
3050    }
3051
3052    #[test]
3053    fn test_pgconfig_from_url_tcp_unchanged() {
3054        use crate::connection::PgConfig;
3055        let config = PgConfig::from_url("postgres://user:pass@localhost:5433/mydb").unwrap();
3056        assert!(config.socket_dir.is_none());
3057        assert_eq!(config.host, "localhost");
3058        assert_eq!(config.port, 5433);
3059        assert_eq!(config.database, "mydb");
3060    }
3061
3062    #[test]
3063    fn test_macaddr_prefers_binary() {
3064        let val = PgValue::MacAddr([0; 6]);
3065        assert!(val.prefers_binary());
3066    }
3067
3068    #[test]
3069    fn test_point_prefers_binary() {
3070        let val = PgValue::Point { x: 0.0, y: 0.0 };
3071        assert!(val.prefers_binary());
3072    }
3073
3074    // ─── C.2: TypeRegistry tests ──────────────────────────────────────────────
3075
3076    #[test]
3077    fn test_type_registry_custom_codec() {
3078        let mut registry = TypeRegistry::new();
3079        let oid = 99999;
3080        registry.register(
3081            oid,
3082            Box::new(|v| match v {
3083                PgValue::Text(s) => Some(s.as_bytes().to_vec()),
3084                _ => None,
3085            }),
3086            Box::new(|data| {
3087                let s =
3088                    std::str::from_utf8(data).map_err(|_| PgError::Protocol("bad utf8".into()))?;
3089                Ok(PgValue::Text(s.to_uppercase()))
3090            }),
3091        );
3092        assert!(registry.has_decoder(oid));
3093        let decoded = registry.decode(oid, b"hello").unwrap().unwrap();
3094        assert_eq!(decoded, PgValue::Text("HELLO".to_string()));
3095
3096        let encoded = registry
3097            .encode(oid, &PgValue::Text("world".to_string()))
3098            .unwrap();
3099        assert_eq!(encoded, Some(b"world".to_vec()));
3100    }
3101
3102    #[test]
3103    fn test_type_registry_unknown_oid() {
3104        let registry = TypeRegistry::new();
3105        assert!(registry.decode(12345, b"data").is_none());
3106        assert!(registry.encode(12345, &PgValue::Null).is_none());
3107        assert!(!registry.has_decoder(12345));
3108    }
3109
3110    // ─── C.3: Custom Enum tests ───────────────────────────────────────────────
3111
3112    #[test]
3113    fn test_register_enum_decode_valid() {
3114        let mut registry = TypeRegistry::new();
3115        registry.register_enum(50001, &["active", "inactive", "pending"]);
3116        let val = registry.decode(50001, b"active").unwrap().unwrap();
3117        assert_eq!(val, PgValue::Text("active".to_string()));
3118    }
3119
3120    #[test]
3121    fn test_register_enum_decode_invalid_variant() {
3122        let mut registry = TypeRegistry::new();
3123        registry.register_enum(50001, &["active", "inactive"]);
3124        let result = registry.decode(50001, b"deleted");
3125        assert!(result.unwrap().is_err());
3126    }
3127
3128    #[test]
3129    fn test_register_enum_encode() {
3130        let mut registry = TypeRegistry::new();
3131        registry.register_enum(50001, &["active", "inactive"]);
3132        let encoded = registry
3133            .encode(50001, &PgValue::Text("active".to_string()))
3134            .unwrap();
3135        assert_eq!(encoded, Some(b"active".to_vec()));
3136    }
3137
3138    // ─── C.4: Composite type tests ───────────────────────────────────────────
3139
3140    #[test]
3141    fn test_register_composite_decode() {
3142        let mut registry = TypeRegistry::new();
3143        // Composite with (int4, text, bool) fields
3144        registry.register_composite(60001, &[oid::INT4, oid::TEXT, oid::BOOL]);
3145        let val = registry.decode(60001, b"(42,hello,t)").unwrap().unwrap();
3146        match val {
3147            PgValue::Array(fields) => {
3148                assert_eq!(fields.len(), 3);
3149                assert_eq!(fields[0], PgValue::Int4(42));
3150                assert_eq!(fields[1], PgValue::Text("hello".to_string()));
3151                assert_eq!(fields[2], PgValue::Bool(true));
3152            }
3153            other => panic!("Expected Array, got {:?}", other),
3154        }
3155    }
3156
3157    #[test]
3158    fn test_register_composite_with_nulls() {
3159        let mut registry = TypeRegistry::new();
3160        registry.register_composite(60002, &[oid::INT4, oid::TEXT]);
3161        let val = registry.decode(60002, b"(42,)").unwrap().unwrap();
3162        match val {
3163            PgValue::Array(fields) => {
3164                assert_eq!(fields.len(), 2);
3165                assert_eq!(fields[0], PgValue::Int4(42));
3166                assert_eq!(fields[1], PgValue::Null);
3167            }
3168            other => panic!("Expected Array, got {:?}", other),
3169        }
3170    }
3171
3172    #[test]
3173    fn test_register_composite_encode() {
3174        let mut registry = TypeRegistry::new();
3175        registry.register_composite(60001, &[oid::INT4, oid::TEXT]);
3176        let val = PgValue::Array(vec![PgValue::Int4(42), PgValue::Text("hello".to_string())]);
3177        let encoded = registry.encode(60001, &val).unwrap();
3178        assert_eq!(encoded, Some(b"(42,hello)".to_vec()));
3179    }
3180
3181    #[test]
3182    fn test_parse_composite_fields_quoted() {
3183        let fields = parse_composite_fields(r#"42,"hello, world",t"#);
3184        assert_eq!(fields, vec!["42", "hello, world", "t"]);
3185    }
3186
3187    #[test]
3188    fn test_parse_composite_fields_escaped_quote() {
3189        let fields = parse_composite_fields(r#""say ""hi""",42"#);
3190        assert_eq!(fields, vec![r#"say "hi""#, "42"]);
3191    }
3192}