Skip to main content

prax_query/
row.rs

1//! Zero-copy row deserialization traits and utilities.
2//!
3//! This module provides traits for efficient row deserialization that minimizes
4//! memory allocations by borrowing data directly from the database row.
5//!
6//! # Zero-Copy Deserialization
7//!
8//! The `FromRowRef` trait enables zero-copy deserialization for types that can
9//! borrow string data directly from the row:
10//!
11//! ```rust,ignore
12//! use prax_query::row::{FromRowRef, RowRef};
13//!
14//! struct UserRef<'a> {
15//!     id: i32,
16//!     email: &'a str,  // Borrowed from row
17//!     name: Option<&'a str>,
18//! }
19//!
20//! impl<'a> FromRowRef<'a> for UserRef<'a> {
21//!     fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError> {
22//!         Ok(Self {
23//!             id: row.get("id")?,
24//!             email: row.get_str("email")?,
25//!             name: row.get_str_opt("name")?,
26//!         })
27//!     }
28//! }
29//! ```
30//!
31//! # Performance
32//!
33//! Zero-copy deserialization can significantly reduce allocations:
34//! - String fields borrow directly from row buffer (no allocation)
35//! - Integer/float fields are copied (no difference)
36//! - Optional fields return `Option<&'a str>` instead of `Option<String>`
37
38use std::borrow::Cow;
39use std::fmt;
40
41/// Error type for row deserialization.
42#[derive(Debug, Clone)]
43pub enum RowError {
44    /// Column not found.
45    ColumnNotFound(String),
46    /// Type conversion error.
47    TypeConversion { column: String, message: String },
48    /// Null value in non-nullable column.
49    UnexpectedNull(String),
50}
51
52impl fmt::Display for RowError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::ColumnNotFound(col) => write!(f, "column '{}' not found", col),
56            Self::TypeConversion { column, message } => {
57                write!(f, "type conversion error for '{}': {}", column, message)
58            }
59            Self::UnexpectedNull(col) => write!(f, "unexpected null in column '{}'", col),
60        }
61    }
62}
63
64impl std::error::Error for RowError {}
65
66/// A database row that supports zero-copy access.
67///
68/// This trait is implemented by database-specific row types to enable
69/// efficient data extraction without unnecessary copying.
70pub trait RowRef {
71    /// Get an integer column value.
72    fn get_i32(&self, column: &str) -> Result<i32, RowError>;
73
74    /// Get an optional integer column value.
75    fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError>;
76
77    /// Get a 64-bit integer column value.
78    fn get_i64(&self, column: &str) -> Result<i64, RowError>;
79
80    /// Get an optional 64-bit integer column value.
81    fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError>;
82
83    /// Get a float column value.
84    fn get_f64(&self, column: &str) -> Result<f64, RowError>;
85
86    /// Get an optional float column value.
87    fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError>;
88
89    /// Get a boolean column value.
90    fn get_bool(&self, column: &str) -> Result<bool, RowError>;
91
92    /// Get an optional boolean column value.
93    fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError>;
94
95    /// Get a string column value as a borrowed reference (zero-copy).
96    ///
97    /// This is the key method for zero-copy deserialization. The returned
98    /// string slice borrows directly from the row's internal buffer.
99    fn get_str(&self, column: &str) -> Result<&str, RowError>;
100
101    /// Get an optional string column value as a borrowed reference.
102    fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError>;
103
104    /// Get a string column value as owned (for cases where ownership is needed).
105    fn get_string(&self, column: &str) -> Result<String, RowError> {
106        self.get_str(column).map(|s| s.to_string())
107    }
108
109    /// Get an optional string as owned.
110    fn get_string_opt(&self, column: &str) -> Result<Option<String>, RowError> {
111        self.get_str_opt(column)
112            .map(|opt| opt.map(|s| s.to_string()))
113    }
114
115    /// Get a bytes column value as a borrowed reference (zero-copy).
116    fn get_bytes(&self, column: &str) -> Result<&[u8], RowError>;
117
118    /// Get optional bytes as borrowed reference.
119    fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError>;
120
121    /// Get column value as a Cow, borrowing when possible.
122    fn get_cow_str(&self, column: &str) -> Result<Cow<'_, str>, RowError> {
123        self.get_str(column).map(Cow::Borrowed)
124    }
125
126    fn get_datetime_utc(&self, column: &str) -> Result<chrono::DateTime<chrono::Utc>, RowError> {
127        Err(unsupported_get(column, "datetime_utc"))
128    }
129    fn get_datetime_utc_opt(
130        &self,
131        column: &str,
132    ) -> Result<Option<chrono::DateTime<chrono::Utc>>, RowError> {
133        Err(unsupported_get(column, "datetime_utc_opt"))
134    }
135    fn get_naive_datetime(&self, column: &str) -> Result<chrono::NaiveDateTime, RowError> {
136        Err(unsupported_get(column, "naive_datetime"))
137    }
138    fn get_naive_datetime_opt(
139        &self,
140        column: &str,
141    ) -> Result<Option<chrono::NaiveDateTime>, RowError> {
142        Err(unsupported_get(column, "naive_datetime_opt"))
143    }
144    fn get_naive_date(&self, column: &str) -> Result<chrono::NaiveDate, RowError> {
145        Err(unsupported_get(column, "naive_date"))
146    }
147    fn get_naive_date_opt(&self, column: &str) -> Result<Option<chrono::NaiveDate>, RowError> {
148        Err(unsupported_get(column, "naive_date_opt"))
149    }
150    fn get_naive_time(&self, column: &str) -> Result<chrono::NaiveTime, RowError> {
151        Err(unsupported_get(column, "naive_time"))
152    }
153    fn get_naive_time_opt(&self, column: &str) -> Result<Option<chrono::NaiveTime>, RowError> {
154        Err(unsupported_get(column, "naive_time_opt"))
155    }
156    fn get_uuid(&self, column: &str) -> Result<uuid::Uuid, RowError> {
157        Err(unsupported_get(column, "uuid"))
158    }
159    fn get_uuid_opt(&self, column: &str) -> Result<Option<uuid::Uuid>, RowError> {
160        Err(unsupported_get(column, "uuid_opt"))
161    }
162    fn get_json(&self, column: &str) -> Result<serde_json::Value, RowError> {
163        Err(unsupported_get(column, "json"))
164    }
165    fn get_json_opt(&self, column: &str) -> Result<Option<serde_json::Value>, RowError> {
166        Err(unsupported_get(column, "json_opt"))
167    }
168    fn get_decimal(&self, column: &str) -> Result<rust_decimal::Decimal, RowError> {
169        Err(unsupported_get(column, "decimal"))
170    }
171    fn get_decimal_opt(&self, column: &str) -> Result<Option<rust_decimal::Decimal>, RowError> {
172        Err(unsupported_get(column, "decimal_opt"))
173    }
174
175    /// Get a pgvector column as a dense `Vec<f32>`.
176    ///
177    /// Default impl errors with `unsupported_get`; `prax-postgres`
178    /// overrides this on its `RowRef` impl to decode the on-wire
179    /// pgvector representation. Used by the blanket
180    /// `FromColumn for Vec<f32>` impl below so schema-generated
181    /// structs with a `Vector(N)` field compile out of the box.
182    fn get_vector(&self, column: &str) -> Result<Vec<f32>, RowError> {
183        Err(unsupported_get(column, "vector"))
184    }
185
186    /// Get a text-array column (`TEXT[]`, `VARCHAR[]`, ...) as `Vec<String>`.
187    ///
188    /// Default impl errors with `unsupported_get`; drivers with native
189    /// array support (`prax-postgres`) override it. Used by
190    /// `FromColumn for Vec<String>`, which is what schema-generated
191    /// structs use for `String[]` scalar-list fields.
192    fn get_string_array(&self, column: &str) -> Result<Vec<String>, RowError> {
193        Err(unsupported_get(column, "string_array"))
194    }
195
196    /// Check whether the named column is NULL in this row.
197    ///
198    /// The default implementation delegates to `get_str_opt` — every
199    /// driver backend already implements that one. Drivers with a
200    /// faster null probe (e.g. postgres's `row.try_get::<_, Option<&str>>`
201    /// avoids a full string allocation) can override this method.
202    ///
203    /// Used by the blanket `impl<T: FromColumn> FromColumn for Option<T>`
204    /// to dispatch between a nullable decode and the inner type's
205    /// non-null decode, so user-defined enums and other custom types
206    /// can round-trip through a nullable column without needing a
207    /// bespoke `FromColumn for Option<MyEnum>` (which the orphan rule
208    /// would forbid the consumer crate from writing).
209    fn is_null(&self, column: &str) -> Result<bool, RowError> {
210        self.get_str_opt(column).map(|opt| opt.is_none())
211    }
212}
213
214/// Build a default `TypeConversion` error for a `RowRef::get_*` method that a
215/// driver has not overridden. Keeps the error phrasing identical across every
216/// extended getter so a debug log like `"uuid not supported by this row type"`
217/// always looks the same no matter which getter a model hit.
218fn unsupported_get(column: &str, getter: &str) -> RowError {
219    RowError::TypeConversion {
220        column: column.to_string(),
221        message: format!("{getter} not supported by this row type"),
222    }
223}
224
225/// Map a driver-level error into a `RowError::TypeConversion` tagged with the
226/// column the caller asked for. Shared by every driver's `RowRef` bridge so
227/// the diagnostic message shape is identical across Postgres, SQLite, MySQL,
228/// and MSSQL. The happy path is an unchanged `Ok(value)`.
229pub fn into_row_error<T, E: std::fmt::Display>(
230    column: &str,
231    res: Result<T, E>,
232) -> Result<T, RowError> {
233    res.map_err(|e| RowError::TypeConversion {
234        column: column.to_string(),
235        message: e.to_string(),
236    })
237}
238
239/// Trait for types that can be deserialized from a row reference (zero-copy).
240///
241/// This trait uses lifetimes to enable borrowing string data directly
242/// from the row, avoiding allocations.
243pub trait FromRowRef<'a>: Sized {
244    /// Deserialize from a row reference.
245    fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
246}
247
248/// Trait for types that can be deserialized from a row (owning).
249///
250/// This is the traditional deserialization trait that takes ownership
251/// of all data.
252pub trait FromRow: Sized {
253    /// Deserialize from a row.
254    fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
255}
256
257// Blanket implementation: any FromRow can be used with any row
258impl<T: FromRow> FromRowRef<'_> for T {
259    fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
260        T::from_row(row)
261    }
262}
263
264/// A row iterator that yields zero-copy deserialized values.
265pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
266    rows: std::slice::Iter<'a, R>,
267    _marker: std::marker::PhantomData<T>,
268}
269
270impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
271    /// Create a new row iterator.
272    pub fn new(rows: &'a [R]) -> Self {
273        Self {
274            rows: rows.iter(),
275            _marker: std::marker::PhantomData,
276        }
277    }
278}
279
280impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
281    type Item = Result<T, RowError>;
282
283    fn next(&mut self) -> Option<Self::Item> {
284        self.rows.next().map(|row| T::from_row_ref(row))
285    }
286
287    fn size_hint(&self) -> (usize, Option<usize>) {
288        self.rows.size_hint()
289    }
290}
291
292impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
293
294/// A collected result that can either borrow or own data.
295///
296/// This is useful for caching query results while still supporting
297/// zero-copy deserialization for fresh queries.
298#[derive(Debug, Clone)]
299pub enum RowData<'a> {
300    /// Borrowed string data.
301    Borrowed(&'a str),
302    /// Owned string data.
303    Owned(String),
304}
305
306impl<'a> RowData<'a> {
307    /// Get the string value.
308    pub fn as_str(&self) -> &str {
309        match self {
310            Self::Borrowed(s) => s,
311            Self::Owned(s) => s,
312        }
313    }
314
315    /// Convert to owned data.
316    pub fn into_owned(self) -> String {
317        match self {
318            Self::Borrowed(s) => s.to_string(),
319            Self::Owned(s) => s,
320        }
321    }
322
323    /// Create borrowed data.
324    pub const fn borrowed(s: &'a str) -> Self {
325        Self::Borrowed(s)
326    }
327
328    /// Create owned data.
329    pub fn owned(s: impl Into<String>) -> Self {
330        Self::Owned(s.into())
331    }
332}
333
334impl<'a> From<&'a str> for RowData<'a> {
335    fn from(s: &'a str) -> Self {
336        Self::Borrowed(s)
337    }
338}
339
340impl From<String> for RowData<'static> {
341    fn from(s: String) -> Self {
342        Self::Owned(s)
343    }
344}
345
346impl<'a> AsRef<str> for RowData<'a> {
347    fn as_ref(&self) -> &str {
348        self.as_str()
349    }
350}
351
352/// Macro to implement FromRow for simple structs.
353///
354/// This generates efficient deserialization code that minimizes allocations.
355///
356/// # Example
357///
358/// ```rust,ignore
359/// use prax_query::impl_from_row;
360///
361/// struct User {
362///     id: i32,
363///     email: String,
364///     name: Option<String>,
365/// }
366///
367/// impl_from_row!(User {
368///     id: i32,
369///     email: String,
370///     name: Option<String>,
371/// });
372/// ```
373#[macro_export]
374macro_rules! impl_from_row {
375    ($type:ident { $($field:ident : i32),* $(,)? }) => {
376        impl $crate::row::FromRow for $type {
377            fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
378                Ok(Self {
379                    $(
380                        $field: row.get_i32(stringify!($field))?,
381                    )*
382                })
383            }
384        }
385    };
386    ($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
387        impl $crate::row::FromRow for $type {
388            fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
389                Ok(Self {
390                    $(
391                        $field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
392                    )*
393                })
394            }
395        }
396    };
397}
398
399/// Helper function for the impl_from_row macro.
400#[doc(hidden)]
401pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
402    T::from_column(row, column)
403}
404
405/// Trait for types that can be extracted from a column.
406pub trait FromColumn: Sized {
407    /// Extract value from a row column.
408    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
409}
410
411impl FromColumn for i32 {
412    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
413        row.get_i32(column)
414    }
415}
416
417impl FromColumn for i64 {
418    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
419        row.get_i64(column)
420    }
421}
422
423impl FromColumn for f64 {
424    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
425        row.get_f64(column)
426    }
427}
428
429impl FromColumn for bool {
430    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
431        row.get_bool(column)
432    }
433}
434
435impl FromColumn for String {
436    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
437        row.get_string(column)
438    }
439}
440
441impl FromColumn for Vec<u8> {
442    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
443        row.get_bytes(column).map(|b| b.to_vec())
444    }
445}
446
447impl FromColumn for chrono::DateTime<chrono::Utc> {
448    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
449        row.get_datetime_utc(column)
450    }
451}
452impl FromColumn for chrono::NaiveDateTime {
453    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
454        row.get_naive_datetime(column)
455    }
456}
457impl FromColumn for chrono::NaiveDate {
458    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
459        row.get_naive_date(column)
460    }
461}
462impl FromColumn for chrono::NaiveTime {
463    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
464        row.get_naive_time(column)
465    }
466}
467impl FromColumn for uuid::Uuid {
468    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
469        row.get_uuid(column)
470    }
471}
472impl FromColumn for serde_json::Value {
473    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
474        row.get_json(column)
475    }
476}
477impl FromColumn for rust_decimal::Decimal {
478    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
479        row.get_decimal(column)
480    }
481}
482
483/// Dense pgvector columns decode into `Vec<f32>`. The schema-generated
484/// client uses this for `Vector(N)` and `HalfVector(N)` scalar fields.
485/// The underlying driver implements `RowRef::get_vector` — drivers that
486/// don't have a pgvector binding will surface an unsupported error
487/// at query time rather than at compile time.
488impl FromColumn for Vec<f32> {
489    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
490        row.get_vector(column)
491    }
492}
493
494/// Text-array columns decode into `Vec<String>`. The schema-generated
495/// client uses this for `String[]` scalar-list fields. Drivers without
496/// array support surface an unsupported error at query time.
497impl FromColumn for Vec<String> {
498    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
499        row.get_string_array(column)
500    }
501}
502
503/// Blanket impl so every `FromColumn` type also satisfies
504/// `FromColumn` through `Option<T>` without each consumer needing a
505/// hand-written nullable wrapper — schema-generated enum types can't
506/// write their own `impl FromColumn for Option<MyEnum>` because of the
507/// orphan rule (both `Option` and `FromColumn` are foreign from the
508/// consumer crate's perspective).
509///
510/// Uses `RowRef::is_null` to short-circuit null rows to `None`; the
511/// non-null path delegates to `T::from_column`. Drivers that had a
512/// faster native `Option<primitive>` path previously now go through
513/// this blanket — the extra `is_null` round-trip is a small
514/// per-column cost in exchange for the orphan-rule unblock.
515impl<T: FromColumn> FromColumn for Option<T> {
516    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
517        if row.is_null(column)? {
518            Ok(None)
519        } else {
520            T::from_column(row, column).map(Some)
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    // Mock row for testing
530    struct MockRow {
531        data: std::collections::HashMap<String, String>,
532    }
533
534    impl RowRef for MockRow {
535        fn get_i32(&self, column: &str) -> Result<i32, RowError> {
536            self.data
537                .get(column)
538                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
539                .parse()
540                .map_err(|e| RowError::TypeConversion {
541                    column: column.to_string(),
542                    message: format!("{}", e),
543                })
544        }
545
546        fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
547            match self.data.get(column) {
548                Some(v) if v == "NULL" => Ok(None),
549                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
550                    column: column.to_string(),
551                    message: format!("{}", e),
552                }),
553                None => Ok(None),
554            }
555        }
556
557        fn get_i64(&self, column: &str) -> Result<i64, RowError> {
558            self.data
559                .get(column)
560                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
561                .parse()
562                .map_err(|e| RowError::TypeConversion {
563                    column: column.to_string(),
564                    message: format!("{}", e),
565                })
566        }
567
568        fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
569            match self.data.get(column) {
570                Some(v) if v == "NULL" => Ok(None),
571                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
572                    column: column.to_string(),
573                    message: format!("{}", e),
574                }),
575                None => Ok(None),
576            }
577        }
578
579        fn get_f64(&self, column: &str) -> Result<f64, RowError> {
580            self.data
581                .get(column)
582                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
583                .parse()
584                .map_err(|e| RowError::TypeConversion {
585                    column: column.to_string(),
586                    message: format!("{}", e),
587                })
588        }
589
590        fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
591            match self.data.get(column) {
592                Some(v) if v == "NULL" => Ok(None),
593                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
594                    column: column.to_string(),
595                    message: format!("{}", e),
596                }),
597                None => Ok(None),
598            }
599        }
600
601        fn get_bool(&self, column: &str) -> Result<bool, RowError> {
602            let v = self
603                .data
604                .get(column)
605                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
606            match v.as_str() {
607                "true" | "t" | "1" => Ok(true),
608                "false" | "f" | "0" => Ok(false),
609                _ => Err(RowError::TypeConversion {
610                    column: column.to_string(),
611                    message: "invalid boolean".to_string(),
612                }),
613            }
614        }
615
616        fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
617            match self.data.get(column) {
618                Some(v) if v == "NULL" => Ok(None),
619                Some(v) => match v.as_str() {
620                    "true" | "t" | "1" => Ok(Some(true)),
621                    "false" | "f" | "0" => Ok(Some(false)),
622                    _ => Err(RowError::TypeConversion {
623                        column: column.to_string(),
624                        message: "invalid boolean".to_string(),
625                    }),
626                },
627                None => Ok(None),
628            }
629        }
630
631        fn get_str(&self, column: &str) -> Result<&str, RowError> {
632            self.data
633                .get(column)
634                .map(|s| s.as_str())
635                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
636        }
637
638        fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
639            match self.data.get(column) {
640                Some(v) if v == "NULL" => Ok(None),
641                Some(v) => Ok(Some(v.as_str())),
642                None => Ok(None),
643            }
644        }
645
646        fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
647            self.data
648                .get(column)
649                .map(|s| s.as_bytes())
650                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
651        }
652
653        fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
654            match self.data.get(column) {
655                Some(v) if v == "NULL" => Ok(None),
656                Some(v) => Ok(Some(v.as_bytes())),
657                None => Ok(None),
658            }
659        }
660    }
661
662    #[test]
663    fn test_row_ref_get_i32() {
664        let mut data = std::collections::HashMap::new();
665        data.insert("id".to_string(), "42".to_string());
666        let row = MockRow { data };
667
668        assert_eq!(row.get_i32("id").unwrap(), 42);
669    }
670
671    #[test]
672    fn test_row_ref_get_str_zero_copy() {
673        let mut data = std::collections::HashMap::new();
674        data.insert("email".to_string(), "test@example.com".to_string());
675        let row = MockRow { data };
676
677        let email = row.get_str("email").unwrap();
678        assert_eq!(email, "test@example.com");
679        // Note: In a real implementation, this would be zero-copy
680        // borrowing directly from the row's buffer
681    }
682
683    #[test]
684    fn test_row_data() {
685        let borrowed: RowData = RowData::borrowed("hello");
686        assert_eq!(borrowed.as_str(), "hello");
687
688        let owned: RowData = RowData::owned("world".to_string());
689        assert_eq!(owned.as_str(), "world");
690    }
691
692    /// `MockRow` plus a `get_string_array` override that reads `"a,b"`
693    /// (empty string = empty array), like a driver with array support.
694    struct ArrayRow(MockRow);
695
696    impl RowRef for ArrayRow {
697        fn get_i32(&self, c: &str) -> Result<i32, RowError> {
698            self.0.get_i32(c)
699        }
700        fn get_i32_opt(&self, c: &str) -> Result<Option<i32>, RowError> {
701            self.0.get_i32_opt(c)
702        }
703        fn get_i64(&self, c: &str) -> Result<i64, RowError> {
704            self.0.get_i64(c)
705        }
706        fn get_i64_opt(&self, c: &str) -> Result<Option<i64>, RowError> {
707            self.0.get_i64_opt(c)
708        }
709        fn get_f64(&self, c: &str) -> Result<f64, RowError> {
710            self.0.get_f64(c)
711        }
712        fn get_f64_opt(&self, c: &str) -> Result<Option<f64>, RowError> {
713            self.0.get_f64_opt(c)
714        }
715        fn get_bool(&self, c: &str) -> Result<bool, RowError> {
716            self.0.get_bool(c)
717        }
718        fn get_bool_opt(&self, c: &str) -> Result<Option<bool>, RowError> {
719            self.0.get_bool_opt(c)
720        }
721        fn get_str(&self, c: &str) -> Result<&str, RowError> {
722            self.0.get_str(c)
723        }
724        fn get_str_opt(&self, c: &str) -> Result<Option<&str>, RowError> {
725            self.0.get_str_opt(c)
726        }
727        fn get_bytes(&self, c: &str) -> Result<&[u8], RowError> {
728            self.0.get_bytes(c)
729        }
730        fn get_bytes_opt(&self, c: &str) -> Result<Option<&[u8]>, RowError> {
731            self.0.get_bytes_opt(c)
732        }
733        fn get_string_array(&self, c: &str) -> Result<Vec<String>, RowError> {
734            let raw = self.get_str(c)?;
735            Ok(if raw.is_empty() {
736                vec![]
737            } else {
738                raw.split(',').map(str::to_string).collect()
739            })
740        }
741    }
742
743    fn array_row(pairs: &[(&str, &str)]) -> ArrayRow {
744        let data = pairs
745            .iter()
746            .map(|(k, v)| (k.to_string(), v.to_string()))
747            .collect();
748        ArrayRow(MockRow { data })
749    }
750
751    #[test]
752    fn vec_string_decodes_through_get_string_array() {
753        let row = array_row(&[("scopes", "read,write"), ("none", "")]);
754        assert_eq!(
755            Vec::<String>::from_column(&row, "scopes").unwrap(),
756            vec!["read".to_string(), "write".to_string()]
757        );
758        assert!(Vec::<String>::from_column(&row, "none").unwrap().is_empty());
759    }
760
761    #[test]
762    fn option_vec_string_maps_null_to_none() {
763        let row = array_row(&[("scopes", "NULL"), ("tags", "a")]);
764        assert_eq!(
765            Option::<Vec<String>>::from_column(&row, "scopes").unwrap(),
766            None
767        );
768        assert_eq!(
769            Option::<Vec<String>>::from_column(&row, "tags").unwrap(),
770            Some(vec!["a".to_string()])
771        );
772    }
773
774    #[test]
775    fn default_string_array_method_errors() {
776        let mut data = std::collections::HashMap::new();
777        data.insert("scopes".into(), "read".into());
778        let row = MockRow { data };
779        assert!(matches!(
780            Vec::<String>::from_column(&row, "scopes"),
781            Err(RowError::TypeConversion { message, .. }) if message.contains("string_array")
782        ));
783    }
784
785    #[test]
786    fn default_datetime_method_errors() {
787        let mut data = std::collections::HashMap::new();
788        data.insert("created_at".into(), "2026-04-27T00:00:00Z".into());
789        let row = MockRow { data };
790        assert!(matches!(
791            row.get_datetime_utc("created_at"),
792            Err(RowError::TypeConversion { .. })
793        ));
794    }
795}