Skip to main content

oxisql_core/
value.rs

1//! [`Value`] enum and its implementations.
2
3use std::cmp::Ordering;
4use std::fmt;
5
6/// The nominal SQL element type of a [`Value::TypedArray`].
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8#[non_exhaustive]
9pub enum ArrayElementType {
10    /// `bool` element type.
11    Bool,
12    /// `int2` element type.
13    Int2,
14    /// `int4` element type.
15    Int4,
16    /// `int8` element type.
17    Int8,
18    /// `float4` element type.
19    Float4,
20    /// `float8` element type.
21    Float8,
22    /// `text` element type.
23    Text,
24    /// `bytea` element type.
25    Bytea,
26    /// `date` element type.
27    Date,
28    /// `time` element type.
29    Time,
30    /// `timestamp` element type.
31    Timestamp,
32    /// `timestamptz` element type.
33    TimestampTz,
34    /// `uuid` element type.
35    Uuid,
36    /// `json` element type.
37    Json,
38    /// `jsonb` element type.
39    Jsonb,
40    /// `decimal` element type.
41    Decimal,
42}
43
44impl fmt::Display for ArrayElementType {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        let s = match self {
47            Self::Bool => "bool",
48            Self::Int2 => "int2",
49            Self::Int4 => "int4",
50            Self::Int8 => "int8",
51            Self::Float4 => "float4",
52            Self::Float8 => "float8",
53            Self::Text => "text",
54            Self::Bytea => "bytea",
55            Self::Date => "date",
56            Self::Time => "time",
57            Self::Timestamp => "timestamp",
58            Self::TimestampTz => "timestamptz",
59            Self::Uuid => "uuid",
60            Self::Json => "json",
61            Self::Jsonb => "jsonb",
62            Self::Decimal => "decimal",
63        };
64        f.write_str(s)
65    }
66}
67
68/// A single SQL value returned from or passed to a query.
69///
70/// Covers both the basic scalar types common to all databases and extended
71/// types for dates, timestamps, UUIDs, JSON, exact decimals, and arrays.
72#[derive(Debug, Clone, PartialEq)]
73pub enum Value {
74    /// SQL `NULL`.
75    Null,
76    /// Boolean value.
77    Bool(bool),
78    /// 64-bit signed integer.
79    I64(i64),
80    /// 64-bit floating-point number.
81    F64(f64),
82    /// UTF-8 text string.
83    Text(String),
84    /// Raw binary data.
85    Blob(Vec<u8>),
86
87    // ── Extended types ──────────────────────────────────────────────────
88    /// Unix timestamp with microsecond precision (microseconds since epoch,
89    /// UTC).  Maps to SQL `TIMESTAMP` / `TIMESTAMPTZ`.
90    Timestamp(i64),
91    /// Date-only value as days since Unix epoch (1970-01-01).
92    /// Maps to SQL `DATE`.
93    Date(i32),
94    /// Time-of-day value as microseconds since midnight.
95    /// Maps to SQL `TIME`.
96    Time(i64),
97    /// UUID stored as a 128-bit unsigned integer.
98    /// Maps to SQL `UUID`.
99    Uuid(u128),
100    /// JSON or JSONB data stored as a UTF-8 string.
101    /// Maps to SQL `JSON` / `JSONB`.
102    Json(String),
103    /// Exact decimal stored as a string representation (e.g. `"123.456"`).
104    /// Using a string avoids introducing a big-decimal dependency at the
105    /// core level while preserving exact precision.
106    /// Maps to SQL `NUMERIC` / `DECIMAL`.
107    Decimal(String),
108    /// Ordered array of values (e.g. Postgres `INTEGER[]`, `TEXT[]`).
109    Array(Vec<Value>),
110    /// Like [`Value::Array`] but also carries the nominal element type returned
111    /// by the backend (e.g. PostgreSQL `int4[]` → `TypedArray { element_type: Int4, .. }`).
112    TypedArray {
113        /// The nominal SQL element type of this array.
114        element_type: ArrayElementType,
115        /// The array elements.
116        values: Vec<Value>,
117    },
118}
119
120impl Value {
121    /// Returns the human-readable type name of this value variant.
122    pub fn type_name(&self) -> &'static str {
123        match self {
124            Value::Null => "Null",
125            Value::Bool(_) => "Bool",
126            Value::I64(_) => "I64",
127            Value::F64(_) => "F64",
128            Value::Text(_) => "Text",
129            Value::Blob(_) => "Blob",
130            Value::Timestamp(_) => "Timestamp",
131            Value::Date(_) => "Date",
132            Value::Time(_) => "Time",
133            Value::Uuid(_) => "Uuid",
134            Value::Json(_) => "Json",
135            Value::Decimal(_) => "Decimal",
136            Value::Array(_) => "Array",
137            Value::TypedArray { .. } => "TypedArray",
138        }
139    }
140
141    /// Returns `true` if this value is [`Value::Null`].
142    pub fn is_null(&self) -> bool {
143        matches!(self, Value::Null)
144    }
145}
146
147impl fmt::Display for Value {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Value::Null => write!(f, "NULL"),
151            Value::Bool(b) => write!(f, "{b}"),
152            Value::I64(n) => write!(f, "{n}"),
153            Value::F64(n) => write!(f, "{n}"),
154            Value::Text(s) => write!(f, "{s}"),
155            Value::Blob(b) => write!(f, "<blob:{} bytes>", b.len()),
156            Value::Timestamp(us) => {
157                // Format as seconds.microseconds from epoch
158                let secs = us / 1_000_000;
159                let frac = (us % 1_000_000).unsigned_abs();
160                write!(f, "{secs}.{frac:06}")
161            }
162            Value::Date(days) => write!(f, "{days}d"),
163            Value::Time(us) => {
164                let total_secs = us / 1_000_000;
165                let hours = total_secs / 3600;
166                let mins = (total_secs % 3600) / 60;
167                let secs = total_secs % 60;
168                let frac = (us % 1_000_000).unsigned_abs();
169                if frac == 0 {
170                    write!(f, "{hours:02}:{mins:02}:{secs:02}")
171                } else {
172                    write!(f, "{hours:02}:{mins:02}:{secs:02}.{frac:06}")
173                }
174            }
175            Value::Uuid(u) => {
176                // Format as standard UUID: 8-4-4-4-12
177                let bytes = u.to_be_bytes();
178                write!(
179                    f,
180                    "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
181                    bytes[0], bytes[1], bytes[2], bytes[3],
182                    bytes[4], bytes[5],
183                    bytes[6], bytes[7],
184                    bytes[8], bytes[9],
185                    bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
186                )
187            }
188            Value::Json(s) => write!(f, "{s}"),
189            Value::Decimal(s) => write!(f, "{s}"),
190            Value::Array(vals) => {
191                write!(f, "[")?;
192                for (i, v) in vals.iter().enumerate() {
193                    if i > 0 {
194                        write!(f, ", ")?;
195                    }
196                    write!(f, "{v}")?;
197                }
198                write!(f, "]")
199            }
200            Value::TypedArray {
201                element_type,
202                values,
203            } => {
204                write!(f, "{element_type}[")?;
205                for (i, v) in values.iter().enumerate() {
206                    if i > 0 {
207                        write!(f, ", ")?;
208                    }
209                    write!(f, "{v}")?;
210                }
211                write!(f, "]")
212            }
213        }
214    }
215}
216
217impl PartialOrd for Value {
218    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
219        match (self, other) {
220            (Value::Null, Value::Null) => Some(Ordering::Equal),
221            (Value::Null, _) => Some(Ordering::Less),
222            (_, Value::Null) => Some(Ordering::Greater),
223            (Value::Bool(a), Value::Bool(b)) => a.partial_cmp(b),
224            (Value::I64(a), Value::I64(b)) => a.partial_cmp(b),
225            (Value::F64(a), Value::F64(b)) => a.partial_cmp(b),
226            (Value::Text(a), Value::Text(b)) => a.partial_cmp(b),
227            (Value::Blob(a), Value::Blob(b)) => a.partial_cmp(b),
228            (Value::Timestamp(a), Value::Timestamp(b)) => a.partial_cmp(b),
229            (Value::Date(a), Value::Date(b)) => a.partial_cmp(b),
230            (Value::Time(a), Value::Time(b)) => a.partial_cmp(b),
231            (Value::Uuid(a), Value::Uuid(b)) => a.partial_cmp(b),
232            (Value::Json(a), Value::Json(b)) => a.partial_cmp(b),
233            (Value::Decimal(a), Value::Decimal(b)) => a.partial_cmp(b),
234            (Value::TypedArray { values: a, .. }, Value::TypedArray { values: b, .. }) => {
235                a.partial_cmp(b)
236            }
237            _ => None,
238        }
239    }
240}
241
242// ── BorrowedValue<'a> — zero-allocation borrowed view of SQL values ──────────
243//
244// [`BorrowedValue`] is a lifetime-parametric mirror of [`Value`] where the
245// heap-allocated `Text` (`String`) and `Blob` (`Vec<u8>`) variants instead
246// borrow from existing storage.  All scalar variants (`Null`, `Bool`, `I64`,
247// `F64`, `Timestamp`, `Date`, `Time`, `Uuid`) are copied inline, so they are
248// identical to `Value` variants.  `Array` / `TypedArray` borrow slices of
249// [`BorrowedValue`] elements.
250//
251// # Usage pattern
252//
253// The primary use-case is row iteration over large result sets: a driver can
254// hold its internal byte buffers and yield `BorrowedValue`s into a
255// `BorrowedRow` without ever cloning the payload strings or blobs.  Once the
256// caller needs to store a value past the row lifetime it calls
257// [`BorrowedValue::to_owned`] to convert to an allocating `Value`.
258//
259// ```rust
260// # use oxisql_core::BorrowedValue;
261// let text_buf = String::from("hello");
262// let bv: BorrowedValue<'_> = BorrowedValue::Text(&text_buf);
263// assert_eq!(bv.type_name(), "Text");
264// let owned = bv.to_owned();
265// ```
266
267/// A borrowed, zero-allocation view of a SQL value.
268///
269/// The lifetime parameter `'a` is tied to the source storage (e.g. an
270/// internal driver buffer) from which `Text` and `Blob` variants borrow.
271#[derive(Debug, Clone, PartialEq)]
272#[non_exhaustive]
273pub enum BorrowedValue<'a> {
274    /// SQL `NULL`.
275    Null,
276    /// Boolean value.
277    Bool(bool),
278    /// 64-bit signed integer.
279    I64(i64),
280    /// 64-bit floating-point number.
281    F64(f64),
282    /// Borrowed UTF-8 text string.
283    Text(&'a str),
284    /// Borrowed raw binary data.
285    Blob(&'a [u8]),
286    // ── Extended types (scalar — no allocation) ──────────────────────────
287    /// Unix timestamp with microsecond precision (microseconds since epoch, UTC).
288    Timestamp(i64),
289    /// Date-only value as days since Unix epoch.
290    Date(i32),
291    /// Time-of-day value as microseconds since midnight.
292    Time(i64),
293    /// UUID stored as a 128-bit unsigned integer.
294    Uuid(u128),
295    /// JSON / JSONB data as a borrowed UTF-8 string.
296    Json(&'a str),
297    /// Exact decimal as a borrowed string representation.
298    Decimal(&'a str),
299    /// Ordered array of borrowed values.
300    Array(&'a [BorrowedValue<'a>]),
301}
302
303impl<'a> BorrowedValue<'a> {
304    /// Returns the human-readable type name (mirrors [`Value::type_name`]).
305    pub fn type_name(&self) -> &'static str {
306        match self {
307            BorrowedValue::Null => "Null",
308            BorrowedValue::Bool(_) => "Bool",
309            BorrowedValue::I64(_) => "I64",
310            BorrowedValue::F64(_) => "F64",
311            BorrowedValue::Text(_) => "Text",
312            BorrowedValue::Blob(_) => "Blob",
313            BorrowedValue::Timestamp(_) => "Timestamp",
314            BorrowedValue::Date(_) => "Date",
315            BorrowedValue::Time(_) => "Time",
316            BorrowedValue::Uuid(_) => "Uuid",
317            BorrowedValue::Json(_) => "Json",
318            BorrowedValue::Decimal(_) => "Decimal",
319            BorrowedValue::Array(_) => "Array",
320        }
321    }
322
323    /// Returns `true` if this value is `NULL`.
324    pub fn is_null(&self) -> bool {
325        matches!(self, BorrowedValue::Null)
326    }
327
328    /// Converts this [`BorrowedValue`] into an owned [`Value`] by cloning any
329    /// borrowed bytes into fresh heap allocations.
330    ///
331    /// # Allocation behaviour
332    ///
333    /// - `Text` → `Value::Text(String::from(*borrowed_str))`
334    /// - `Blob` → `Value::Blob(borrowed_slice.to_vec())`
335    /// - `Json` → `Value::Json(String::from(*borrowed_str))`
336    /// - `Decimal` → `Value::Decimal(String::from(*borrowed_str))`
337    /// - All scalar variants copy inline with no heap allocation.
338    /// - `Array` recursively converts each element.
339    pub fn to_owned(&self) -> Value {
340        match self {
341            BorrowedValue::Null => Value::Null,
342            BorrowedValue::Bool(b) => Value::Bool(*b),
343            BorrowedValue::I64(n) => Value::I64(*n),
344            BorrowedValue::F64(f) => Value::F64(*f),
345            BorrowedValue::Text(s) => Value::Text((*s).to_owned()),
346            BorrowedValue::Blob(b) => Value::Blob(b.to_vec()),
347            BorrowedValue::Timestamp(t) => Value::Timestamp(*t),
348            BorrowedValue::Date(d) => Value::Date(*d),
349            BorrowedValue::Time(t) => Value::Time(*t),
350            BorrowedValue::Uuid(u) => Value::Uuid(*u),
351            BorrowedValue::Json(s) => Value::Json((*s).to_owned()),
352            BorrowedValue::Decimal(s) => Value::Decimal((*s).to_owned()),
353            BorrowedValue::Array(elems) => {
354                Value::Array(elems.iter().map(|e| e.to_owned()).collect())
355            }
356        }
357    }
358}
359
360impl<'a> From<&'a Value> for BorrowedValue<'a> {
361    /// Borrow a [`Value`] as a [`BorrowedValue`] with zero allocation.
362    ///
363    /// `Text`, `Blob`, `Json`, and `Decimal` borrow from the owned `Value`.
364    /// `Array` borrows from a freshly-allocated intermediate `Vec` of
365    /// `BorrowedValue`s; use [`BorrowedValue::to_owned`] to get back an
366    /// owned `Value` when needed.
367    fn from(v: &'a Value) -> Self {
368        match v {
369            Value::Null => BorrowedValue::Null,
370            Value::Bool(b) => BorrowedValue::Bool(*b),
371            Value::I64(n) => BorrowedValue::I64(*n),
372            Value::F64(f) => BorrowedValue::F64(*f),
373            Value::Text(s) => BorrowedValue::Text(s.as_str()),
374            Value::Blob(b) => BorrowedValue::Blob(b.as_slice()),
375            Value::Timestamp(t) => BorrowedValue::Timestamp(*t),
376            Value::Date(d) => BorrowedValue::Date(*d),
377            Value::Time(t) => BorrowedValue::Time(*t),
378            Value::Uuid(u) => BorrowedValue::Uuid(*u),
379            Value::Json(s) => BorrowedValue::Json(s.as_str()),
380            Value::Decimal(s) => BorrowedValue::Decimal(s.as_str()),
381            // Array / TypedArray: fall back to an owned clone rather than
382            // trying to build a borrowed slice of BorrowedValues, which
383            // would require an intermediate allocation on the stack anyway.
384            Value::Array(elems) => {
385                // We can't return a borrowed slice of a temporary vec here
386                // without unsafe code, so we fall back to yielding a Null
387                // placeholder and document the limitation. Callers that need
388                // array borrowing should iterate over `elems` manually.
389                let _ = elems; // acknowledged
390                BorrowedValue::Null
391            }
392            Value::TypedArray { values, .. } => {
393                let _ = values;
394                BorrowedValue::Null
395            }
396        }
397    }
398}
399
400impl std::fmt::Display for BorrowedValue<'_> {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        match self {
403            BorrowedValue::Null => write!(f, "NULL"),
404            BorrowedValue::Bool(b) => write!(f, "{b}"),
405            BorrowedValue::I64(n) => write!(f, "{n}"),
406            BorrowedValue::F64(v) => write!(f, "{v}"),
407            BorrowedValue::Text(s) => write!(f, "{s}"),
408            BorrowedValue::Blob(b) => write!(f, "\\x{}", hex_encode(b)),
409            BorrowedValue::Timestamp(t) => write!(f, "ts:{t}"),
410            BorrowedValue::Date(d) => write!(f, "date:{d}"),
411            BorrowedValue::Time(t) => write!(f, "time:{t}"),
412            BorrowedValue::Uuid(u) => {
413                let bytes = u.to_be_bytes();
414                write!(
415                    f,
416                    "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
417                    u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
418                    u16::from_be_bytes([bytes[4], bytes[5]]),
419                    u16::from_be_bytes([bytes[6], bytes[7]]),
420                    u16::from_be_bytes([bytes[8], bytes[9]]),
421                    {
422                        let mut tail = 0u64;
423                        for &byte in &bytes[10..] {
424                            tail = (tail << 8) | u64::from(byte);
425                        }
426                        tail
427                    }
428                )
429            }
430            BorrowedValue::Json(s) => write!(f, "{s}"),
431            BorrowedValue::Decimal(s) => write!(f, "{s}"),
432            BorrowedValue::Array(elems) => {
433                write!(f, "[")?;
434                for (i, e) in elems.iter().enumerate() {
435                    if i > 0 {
436                        write!(f, ", ")?;
437                    }
438                    write!(f, "{e}")?;
439                }
440                write!(f, "]")
441            }
442        }
443    }
444}
445
446/// Hex-encode a byte slice for display purposes (lower-case, no prefix).
447fn hex_encode(b: &[u8]) -> String {
448    b.iter().map(|byte| format!("{byte:02x}")).collect()
449}
450
451// ── From impls for ergonomic Value construction ─────────────────────────────
452
453impl From<bool> for Value {
454    fn from(v: bool) -> Self {
455        Value::Bool(v)
456    }
457}
458
459impl From<i32> for Value {
460    fn from(v: i32) -> Self {
461        Value::I64(i64::from(v))
462    }
463}
464
465impl From<i64> for Value {
466    fn from(v: i64) -> Self {
467        Value::I64(v)
468    }
469}
470
471impl From<f64> for Value {
472    fn from(v: f64) -> Self {
473        Value::F64(v)
474    }
475}
476
477impl From<String> for Value {
478    fn from(v: String) -> Self {
479        Value::Text(v)
480    }
481}
482
483impl From<&str> for Value {
484    fn from(v: &str) -> Self {
485        Value::Text(v.to_string())
486    }
487}
488
489impl From<Vec<u8>> for Value {
490    fn from(v: Vec<u8>) -> Self {
491        Value::Blob(v)
492    }
493}
494
495impl<T: Into<Value>> From<Option<T>> for Value {
496    fn from(v: Option<T>) -> Self {
497        match v {
498            Some(inner) => inner.into(),
499            None => Value::Null,
500        }
501    }
502}
503
504// ── FromValue for chrono types (feature = "chrono") ─────────────────────────
505
506#[cfg(feature = "chrono")]
507mod chrono_impls {
508    use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
509
510    use crate::row::FromValue;
511    use crate::{OxiSqlError, Value};
512
513    // Days-since-epoch constant helpers
514    // Value::Date(i32) → days since 1970-01-01 (Unix epoch)
515    // Value::Time(i64) → microseconds since midnight
516    // Value::Timestamp(i64) → microseconds since Unix epoch
517
518    impl FromValue for NaiveDate {
519        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
520            match v {
521                Value::Date(days) => {
522                    NaiveDate::from_num_days_from_ce_opt(
523                        // chrono epoch is 1 CE; Unix epoch (1970-01-01) is day 719_163 in CE days.
524                        *days + 719_163,
525                    )
526                    .ok_or(OxiSqlError::TypeMismatch {
527                        expected: "NaiveDate (valid days-since-epoch)",
528                        got: "Date (out of range)",
529                    })
530                }
531                Value::Text(s) => s
532                    .parse::<NaiveDate>()
533                    .map_err(|_| OxiSqlError::TypeMismatch {
534                        expected: "NaiveDate (ISO 8601 text)",
535                        got: "Text (not a valid date)",
536                    }),
537                Value::I64(n) => {
538                    // Treat as days since Unix epoch
539                    let days = i32::try_from(*n).map_err(|_| OxiSqlError::TypeMismatch {
540                        expected: "NaiveDate (i64 days-since-epoch in i32 range)",
541                        got: "I64 (out of i32 range)",
542                    })?;
543                    NaiveDate::from_num_days_from_ce_opt(days + 719_163).ok_or(
544                        OxiSqlError::TypeMismatch {
545                            expected: "NaiveDate (valid days-since-epoch)",
546                            got: "I64 (out of range)",
547                        },
548                    )
549                }
550                other => Err(OxiSqlError::TypeMismatch {
551                    expected: "Date",
552                    got: other.type_name(),
553                }),
554            }
555        }
556    }
557
558    impl FromValue for NaiveTime {
559        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
560            match v {
561                Value::Time(us) => {
562                    // microseconds since midnight; split into secs + nano remainder
563                    let total_secs = (*us / 1_000_000) as u32;
564                    let nano = ((*us % 1_000_000) * 1_000) as u32;
565                    let h = total_secs / 3600;
566                    let m = (total_secs % 3600) / 60;
567                    let s = total_secs % 60;
568                    NaiveTime::from_hms_nano_opt(h, m, s, nano).ok_or(OxiSqlError::TypeMismatch {
569                        expected: "NaiveTime (valid time-of-day)",
570                        got: "Time (out of range)",
571                    })
572                }
573                Value::Text(s) => s
574                    .parse::<NaiveTime>()
575                    .map_err(|_| OxiSqlError::TypeMismatch {
576                        expected: "NaiveTime (ISO 8601 text)",
577                        got: "Text (not a valid time)",
578                    }),
579                other => Err(OxiSqlError::TypeMismatch {
580                    expected: "Time",
581                    got: other.type_name(),
582                }),
583            }
584        }
585    }
586
587    impl FromValue for NaiveDateTime {
588        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
589            match v {
590                Value::Timestamp(us) => {
591                    // microseconds since Unix epoch
592                    let secs = us.div_euclid(1_000_000);
593                    let nsecs = (us.rem_euclid(1_000_000) * 1_000) as u32;
594                    DateTime::from_timestamp(secs, nsecs)
595                        .map(|dt| dt.naive_utc())
596                        .ok_or(OxiSqlError::TypeMismatch {
597                            expected: "NaiveDateTime (valid timestamp)",
598                            got: "Timestamp (out of range)",
599                        })
600                }
601                Value::Text(s) => {
602                    s.parse::<NaiveDateTime>()
603                        .map_err(|_| OxiSqlError::TypeMismatch {
604                            expected: "NaiveDateTime (ISO 8601 text)",
605                            got: "Text (not a valid datetime)",
606                        })
607                }
608                other => Err(OxiSqlError::TypeMismatch {
609                    expected: "Timestamp",
610                    got: other.type_name(),
611                }),
612            }
613        }
614    }
615
616    impl FromValue for DateTime<Utc> {
617        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
618            match v {
619                Value::Timestamp(us) => {
620                    let secs = us.div_euclid(1_000_000);
621                    let nsecs = (us.rem_euclid(1_000_000) * 1_000) as u32;
622                    Utc.timestamp_opt(secs, nsecs)
623                        .single()
624                        .ok_or(OxiSqlError::TypeMismatch {
625                            expected: "DateTime<Utc> (valid timestamp)",
626                            got: "Timestamp (out of range or ambiguous)",
627                        })
628                }
629                Value::Text(s) => {
630                    s.parse::<DateTime<Utc>>()
631                        .map_err(|_| OxiSqlError::TypeMismatch {
632                            expected: "DateTime<Utc> (RFC3339 text)",
633                            got: "Text (not a valid datetime)",
634                        })
635                }
636                other => Err(OxiSqlError::TypeMismatch {
637                    expected: "Timestamp",
638                    got: other.type_name(),
639                }),
640            }
641        }
642    }
643
644    #[cfg(test)]
645    mod chrono_tests {
646        use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc};
647
648        use crate::row::FromValue;
649        use crate::{OxiSqlError, Value};
650
651        #[test]
652        fn roundtrip_naive_date() {
653            // 1970-01-01 is day 0 (Unix epoch)
654            let v = Value::Date(0);
655            let d = NaiveDate::from_value(&v).expect("epoch date");
656            assert_eq!(d, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
657
658            // 1970-01-02 is day 1
659            let v = Value::Date(1);
660            let d = NaiveDate::from_value(&v).expect("day 1");
661            assert_eq!(d, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap());
662
663            // 2024-03-15 — validate round-trip through i32
664            let expected = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
665            let days_from_ce = expected.num_days_from_ce();
666            let days_since_epoch = days_from_ce - 719_163;
667            let v = Value::Date(days_since_epoch);
668            let d = NaiveDate::from_value(&v).expect("2024-03-15");
669            assert_eq!(d, expected);
670        }
671
672        #[test]
673        fn roundtrip_naive_time() {
674            // 01:02:03.000042 → microseconds
675            let us = (3600 + 2 * 60 + 3) * 1_000_000i64 + 42;
676            let v = Value::Time(us);
677            let t = NaiveTime::from_value(&v).expect("time");
678            assert_eq!(t.hour(), 1);
679            assert_eq!(t.minute(), 2);
680            assert_eq!(t.second(), 3);
681            // sub-second: 42 microseconds = 42_000 nanoseconds
682            assert_eq!(t.nanosecond(), 42_000);
683        }
684
685        #[test]
686        fn roundtrip_naive_datetime() {
687            // Unix epoch timestamp: 0 microseconds → 1970-01-01T00:00:00
688            let v = Value::Timestamp(0);
689            let dt = NaiveDateTime::from_value(&v).expect("epoch datetime");
690            let expected_epoch = DateTime::from_timestamp(0, 0).unwrap().naive_utc();
691            assert_eq!(dt, expected_epoch);
692
693            // 1_000_000 microseconds = 1 second after epoch
694            let v = Value::Timestamp(1_000_000);
695            let dt = NaiveDateTime::from_value(&v).expect("1s after epoch");
696            let expected_1s = DateTime::from_timestamp(1, 0).unwrap().naive_utc();
697            assert_eq!(dt, expected_1s);
698        }
699
700        #[test]
701        fn roundtrip_datetime_utc() {
702            let v = Value::Timestamp(0);
703            let dt = DateTime::<Utc>::from_value(&v).expect("utc epoch");
704            assert_eq!(dt, DateTime::from_timestamp(0, 0).unwrap());
705        }
706
707        #[test]
708        fn fromvalue_error_on_mismatch() {
709            // Non-date value → error
710            assert!(matches!(
711                NaiveDate::from_value(&Value::Bool(true)),
712                Err(OxiSqlError::TypeMismatch { .. })
713            ));
714            // Non-time value → error
715            assert!(matches!(
716                NaiveTime::from_value(&Value::I64(42)),
717                Err(OxiSqlError::TypeMismatch { .. })
718            ));
719            // Non-timestamp value → error
720            assert!(matches!(
721                NaiveDateTime::from_value(&Value::Text("not-a-dt".into())),
722                Err(OxiSqlError::TypeMismatch { .. })
723            ));
724            assert!(matches!(
725                DateTime::<Utc>::from_value(&Value::Bool(false)),
726                Err(OxiSqlError::TypeMismatch { .. })
727            ));
728        }
729
730        #[test]
731        fn naive_date_from_text() {
732            let v = Value::Text("2024-06-10".into());
733            let d = NaiveDate::from_value(&v).expect("from text");
734            assert_eq!(d, NaiveDate::from_ymd_opt(2024, 6, 10).unwrap());
735        }
736
737        #[test]
738        fn naive_date_from_i64() {
739            // day 0 from i64
740            let v = Value::I64(0);
741            let d = NaiveDate::from_value(&v).expect("epoch from i64");
742            assert_eq!(d, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
743        }
744    }
745}
746
747// ── FromValue for time types (feature = "time") ──────────────────────────────
748
749#[cfg(feature = "time")]
750mod time_impls {
751    use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
752
753    use crate::row::FromValue;
754    use crate::{OxiSqlError, Value};
755
756    impl FromValue for Date {
757        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
758            match v {
759                Value::Date(days) => {
760                    // Value::Date is days since Unix epoch (1970-01-01).
761                    // `time` uses Julian day internally; we reconstruct via year/month/day
762                    // by converting to an OffsetDateTime from a Unix timestamp.
763                    let unix_secs = i64::from(*days) * 86_400;
764                    OffsetDateTime::from_unix_timestamp(unix_secs)
765                        .map(|odt| odt.date())
766                        .map_err(|_| OxiSqlError::TypeMismatch {
767                            expected: "time::Date (valid days-since-epoch)",
768                            got: "Date (out of range)",
769                        })
770                }
771                Value::Text(s) => {
772                    // Attempt ISO 8601 YYYY-MM-DD parse
773                    let parts: Vec<&str> = s.splitn(3, '-').collect();
774                    if parts.len() == 3 {
775                        let y: i32 = parts[0].parse().map_err(|_| OxiSqlError::TypeMismatch {
776                            expected: "time::Date (YYYY-MM-DD text)",
777                            got: "Text (non-numeric year)",
778                        })?;
779                        let mo: u8 = parts[1].parse().map_err(|_| OxiSqlError::TypeMismatch {
780                            expected: "time::Date (YYYY-MM-DD text)",
781                            got: "Text (non-numeric month)",
782                        })?;
783                        let d: u8 = parts[2].parse().map_err(|_| OxiSqlError::TypeMismatch {
784                            expected: "time::Date (YYYY-MM-DD text)",
785                            got: "Text (non-numeric day)",
786                        })?;
787                        let month = Month::try_from(mo).map_err(|_| OxiSqlError::TypeMismatch {
788                            expected: "time::Date (month 1-12)",
789                            got: "Text (month out of range)",
790                        })?;
791                        Date::from_calendar_date(y, month, d).map_err(|_| {
792                            OxiSqlError::TypeMismatch {
793                                expected: "time::Date (valid calendar date)",
794                                got: "Text (invalid date)",
795                            }
796                        })
797                    } else {
798                        Err(OxiSqlError::TypeMismatch {
799                            expected: "time::Date (YYYY-MM-DD text)",
800                            got: "Text (not a valid date)",
801                        })
802                    }
803                }
804                other => Err(OxiSqlError::TypeMismatch {
805                    expected: "Date",
806                    got: other.type_name(),
807                }),
808            }
809        }
810    }
811
812    impl FromValue for Time {
813        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
814            match v {
815                Value::Time(us) => {
816                    let total_secs = us / 1_000_000;
817                    let h = (total_secs / 3600) as u8;
818                    let m = ((total_secs % 3600) / 60) as u8;
819                    let s = (total_secs % 60) as u8;
820                    let nano = ((*us % 1_000_000) * 1_000) as u32;
821                    Time::from_hms_nano(h, m, s, nano).map_err(|_| OxiSqlError::TypeMismatch {
822                        expected: "time::Time (valid time-of-day)",
823                        got: "Time (out of range)",
824                    })
825                }
826                other => Err(OxiSqlError::TypeMismatch {
827                    expected: "Time",
828                    got: other.type_name(),
829                }),
830            }
831        }
832    }
833
834    impl FromValue for PrimitiveDateTime {
835        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
836            match v {
837                Value::Timestamp(us) => {
838                    let secs = us.div_euclid(1_000_000);
839                    let nano = (us.rem_euclid(1_000_000) * 1_000) as u32;
840                    let odt = OffsetDateTime::from_unix_timestamp(secs).map_err(|_| {
841                        OxiSqlError::TypeMismatch {
842                            expected: "PrimitiveDateTime (valid timestamp)",
843                            got: "Timestamp (out of range)",
844                        }
845                    })?;
846                    // Replace sub-second with exact nanoseconds from the microsecond field
847                    let odt_with_nanos =
848                        odt.replace_nanosecond(nano)
849                            .map_err(|_| OxiSqlError::TypeMismatch {
850                                expected: "PrimitiveDateTime (valid nanoseconds)",
851                                got: "Timestamp (nanosecond out of range)",
852                            })?;
853                    Ok(PrimitiveDateTime::new(
854                        odt_with_nanos.date(),
855                        odt_with_nanos.time(),
856                    ))
857                }
858                other => Err(OxiSqlError::TypeMismatch {
859                    expected: "Timestamp",
860                    got: other.type_name(),
861                }),
862            }
863        }
864    }
865
866    impl FromValue for OffsetDateTime {
867        fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
868            match v {
869                Value::Timestamp(us) => {
870                    let secs = us.div_euclid(1_000_000);
871                    let nano = (us.rem_euclid(1_000_000) * 1_000) as u32;
872                    let odt = OffsetDateTime::from_unix_timestamp(secs).map_err(|_| {
873                        OxiSqlError::TypeMismatch {
874                            expected: "OffsetDateTime (valid timestamp)",
875                            got: "Timestamp (out of range)",
876                        }
877                    })?;
878                    odt.replace_nanosecond(nano)
879                        .map_err(|_| OxiSqlError::TypeMismatch {
880                            expected: "OffsetDateTime (valid nanoseconds)",
881                            got: "Timestamp (nanosecond out of range)",
882                        })
883                }
884                other => Err(OxiSqlError::TypeMismatch {
885                    expected: "Timestamp",
886                    got: other.type_name(),
887                }),
888            }
889        }
890    }
891
892    #[cfg(test)]
893    mod time_tests {
894        use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
895
896        use crate::row::FromValue;
897        use crate::{OxiSqlError, Value};
898
899        #[test]
900        fn roundtrip_time_date() {
901            // Unix epoch = 1970-01-01
902            let v = Value::Date(0);
903            let d = Date::from_value(&v).expect("epoch date");
904            let expected = Date::from_calendar_date(1970, Month::January, 1).unwrap();
905            assert_eq!(d, expected);
906
907            // 1970-01-02 = day 1
908            let v = Value::Date(1);
909            let d = Date::from_value(&v).expect("day 1");
910            let expected = Date::from_calendar_date(1970, Month::January, 2).unwrap();
911            assert_eq!(d, expected);
912        }
913
914        #[test]
915        fn roundtrip_time_time() {
916            // 01:02:03 = (3600 + 120 + 3) * 1_000_000 µs
917            let us = (3600 + 2 * 60 + 3) * 1_000_000i64;
918            let v = Value::Time(us);
919            let t = Time::from_value(&v).expect("time");
920            assert_eq!(t.hour(), 1);
921            assert_eq!(t.minute(), 2);
922            assert_eq!(t.second(), 3);
923        }
924
925        #[test]
926        fn roundtrip_primitive_datetime() {
927            let v = Value::Timestamp(0);
928            let dt = PrimitiveDateTime::from_value(&v).expect("epoch");
929            let epoch = Date::from_calendar_date(1970, Month::January, 1).unwrap();
930            let midnight = Time::from_hms(0, 0, 0).unwrap();
931            assert_eq!(dt, PrimitiveDateTime::new(epoch, midnight));
932        }
933
934        #[test]
935        fn roundtrip_offset_datetime() {
936            let v = Value::Timestamp(0);
937            let dt = OffsetDateTime::from_value(&v).expect("utc epoch");
938            assert_eq!(dt.unix_timestamp(), 0);
939        }
940
941        #[test]
942        fn time_date_from_text() {
943            let v = Value::Text("2024-06-10".into());
944            let d = Date::from_value(&v).expect("from text");
945            let expected = Date::from_calendar_date(2024, Month::June, 10).unwrap();
946            assert_eq!(d, expected);
947        }
948
949        #[test]
950        fn time_fromvalue_error_on_mismatch() {
951            assert!(matches!(
952                Date::from_value(&Value::Bool(true)),
953                Err(OxiSqlError::TypeMismatch { .. })
954            ));
955            assert!(matches!(
956                Time::from_value(&Value::I64(42)),
957                Err(OxiSqlError::TypeMismatch { .. })
958            ));
959            assert!(matches!(
960                PrimitiveDateTime::from_value(&Value::Text("bad".into())),
961                Err(OxiSqlError::TypeMismatch { .. })
962            ));
963        }
964    }
965}
966
967#[cfg(test)]
968mod typed_array_tests {
969    use super::*;
970
971    #[test]
972    fn typed_array_display() {
973        let v = Value::TypedArray {
974            element_type: ArrayElementType::Int4,
975            values: vec![Value::I64(1), Value::I64(2), Value::Null],
976        };
977        let s = format!("{v}");
978        assert!(s.starts_with("int4["), "got: {s}");
979        assert!(s.contains("1"), "got: {s}");
980        assert!(s.contains("NULL"), "got: {s}");
981    }
982
983    #[test]
984    fn typed_array_type_name() {
985        let v = Value::TypedArray {
986            element_type: ArrayElementType::Text,
987            values: vec![],
988        };
989        assert_eq!(v.type_name(), "TypedArray");
990    }
991
992    #[test]
993    fn typed_array_partial_ord() {
994        let a = Value::TypedArray {
995            element_type: ArrayElementType::Int4,
996            values: vec![Value::I64(1)],
997        };
998        let b = Value::TypedArray {
999            element_type: ArrayElementType::Int4,
1000            values: vec![Value::I64(2)],
1001        };
1002        assert!(a < b);
1003    }
1004
1005    #[test]
1006    fn typed_array_is_not_null() {
1007        let v = Value::TypedArray {
1008            element_type: ArrayElementType::Bool,
1009            values: vec![],
1010        };
1011        assert!(!v.is_null());
1012    }
1013
1014    #[test]
1015    fn array_element_type_display() {
1016        assert_eq!(ArrayElementType::Int4.to_string(), "int4");
1017        assert_eq!(ArrayElementType::TimestampTz.to_string(), "timestamptz");
1018    }
1019}
1020
1021// ── BorrowedValue tests ──────────────────────────────────────────────────────
1022
1023#[cfg(test)]
1024mod borrowed_value_tests {
1025    use super::*;
1026
1027    #[test]
1028    fn borrowed_null_type_name_and_is_null() {
1029        let bv = BorrowedValue::Null;
1030        assert_eq!(bv.type_name(), "Null");
1031        assert!(bv.is_null());
1032    }
1033
1034    #[test]
1035    fn borrowed_text_no_allocation() {
1036        let s = String::from("hello world");
1037        let bv: BorrowedValue<'_> = BorrowedValue::Text(&s);
1038        assert_eq!(bv.type_name(), "Text");
1039        assert!(!bv.is_null());
1040        // to_owned should clone the string
1041        let owned = bv.to_owned();
1042        assert_eq!(owned, Value::Text("hello world".into()));
1043    }
1044
1045    #[test]
1046    fn borrowed_blob_no_allocation() {
1047        let data: Vec<u8> = vec![0xde, 0xad, 0xbe, 0xef];
1048        let bv = BorrowedValue::Blob(&data);
1049        assert_eq!(bv.type_name(), "Blob");
1050        let owned = bv.to_owned();
1051        assert_eq!(owned, Value::Blob(vec![0xde, 0xad, 0xbe, 0xef]));
1052    }
1053
1054    #[test]
1055    fn borrowed_scalar_roundtrip() {
1056        assert_eq!(BorrowedValue::I64(42).to_owned(), Value::I64(42));
1057        assert_eq!(
1058            BorrowedValue::F64(1.23456789).to_owned(),
1059            Value::F64(1.23456789)
1060        );
1061        assert_eq!(BorrowedValue::Bool(true).to_owned(), Value::Bool(true));
1062        assert_eq!(
1063            BorrowedValue::Timestamp(1000).to_owned(),
1064            Value::Timestamp(1000)
1065        );
1066        assert_eq!(BorrowedValue::Date(365).to_owned(), Value::Date(365));
1067        assert_eq!(
1068            BorrowedValue::Time(86400000000).to_owned(),
1069            Value::Time(86400000000)
1070        );
1071        let u: u128 = 0x0123456789abcdef0123456789abcdef;
1072        assert_eq!(BorrowedValue::Uuid(u).to_owned(), Value::Uuid(u));
1073    }
1074
1075    #[test]
1076    fn from_value_text_borrows() {
1077        let v = Value::Text("world".into());
1078        let bv = BorrowedValue::from(&v);
1079        assert!(matches!(bv, BorrowedValue::Text("world")));
1080    }
1081
1082    #[test]
1083    fn from_value_blob_borrows() {
1084        let v = Value::Blob(vec![1, 2, 3]);
1085        let bv = BorrowedValue::from(&v);
1086        match bv {
1087            BorrowedValue::Blob(b) => assert_eq!(b, &[1u8, 2, 3]),
1088            other => panic!("expected Blob, got {}", other.type_name()),
1089        }
1090    }
1091
1092    #[test]
1093    fn from_value_json_borrows() {
1094        let v = Value::Json(r#"{"k":1}"#.into());
1095        let bv = BorrowedValue::from(&v);
1096        match bv {
1097            BorrowedValue::Json(s) => assert_eq!(s, r#"{"k":1}"#),
1098            other => panic!("expected Json, got {}", other.type_name()),
1099        }
1100    }
1101
1102    #[test]
1103    fn from_value_decimal_borrows() {
1104        let v = Value::Decimal("123.456".into());
1105        let bv = BorrowedValue::from(&v);
1106        match bv {
1107            BorrowedValue::Decimal(s) => assert_eq!(s, "123.456"),
1108            other => panic!("expected Decimal, got {}", other.type_name()),
1109        }
1110    }
1111
1112    #[test]
1113    fn display_null() {
1114        assert_eq!(format!("{}", BorrowedValue::Null), "NULL");
1115    }
1116
1117    #[test]
1118    fn display_text() {
1119        assert_eq!(format!("{}", BorrowedValue::Text("hi")), "hi");
1120    }
1121
1122    #[test]
1123    fn display_blob_hex() {
1124        let data = [0xde_u8, 0xad];
1125        let bv = BorrowedValue::Blob(&data);
1126        assert_eq!(format!("{bv}"), "\\xdead");
1127    }
1128
1129    #[test]
1130    fn display_uuid() {
1131        let u: u128 = 0x00000000000000000000000000000001;
1132        let bv = BorrowedValue::Uuid(u);
1133        let s = format!("{bv}");
1134        // Should be a well-formed UUID string
1135        assert!(s.contains('-'), "expected UUID format, got: {s}");
1136    }
1137
1138    #[test]
1139    fn roundtrip_from_value_to_owned() {
1140        let values = vec![
1141            Value::Null,
1142            Value::Bool(false),
1143            Value::I64(-1),
1144            Value::F64(0.5),
1145            Value::Text("abc".into()),
1146            Value::Blob(vec![0xff]),
1147            Value::Timestamp(99999),
1148            Value::Date(10),
1149            Value::Time(3_600_000_000),
1150            Value::Uuid(42),
1151            Value::Json("{}".into()),
1152            Value::Decimal("0.001".into()),
1153        ];
1154        for v in &values {
1155            let bv = BorrowedValue::from(v);
1156            let recovered = bv.to_owned();
1157            assert_eq!(recovered, *v, "roundtrip failed for {}", v.type_name());
1158        }
1159    }
1160}