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
176/// Build a default `TypeConversion` error for a `RowRef::get_*` method that a
177/// driver has not overridden. Keeps the error phrasing identical across every
178/// extended getter so a debug log like `"uuid not supported by this row type"`
179/// always looks the same no matter which getter a model hit.
180fn unsupported_get(column: &str, getter: &str) -> RowError {
181    RowError::TypeConversion {
182        column: column.to_string(),
183        message: format!("{getter} not supported by this row type"),
184    }
185}
186
187/// Map a driver-level error into a `RowError::TypeConversion` tagged with the
188/// column the caller asked for. Shared by every driver's `RowRef` bridge so
189/// the diagnostic message shape is identical across Postgres, SQLite, MySQL,
190/// and MSSQL. The happy path is an unchanged `Ok(value)`.
191pub fn into_row_error<T, E: std::fmt::Display>(
192    column: &str,
193    res: Result<T, E>,
194) -> Result<T, RowError> {
195    res.map_err(|e| RowError::TypeConversion {
196        column: column.to_string(),
197        message: e.to_string(),
198    })
199}
200
201/// Trait for types that can be deserialized from a row reference (zero-copy).
202///
203/// This trait uses lifetimes to enable borrowing string data directly
204/// from the row, avoiding allocations.
205pub trait FromRowRef<'a>: Sized {
206    /// Deserialize from a row reference.
207    fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
208}
209
210/// Trait for types that can be deserialized from a row (owning).
211///
212/// This is the traditional deserialization trait that takes ownership
213/// of all data.
214pub trait FromRow: Sized {
215    /// Deserialize from a row.
216    fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
217}
218
219// Blanket implementation: any FromRow can be used with any row
220impl<T: FromRow> FromRowRef<'_> for T {
221    fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
222        T::from_row(row)
223    }
224}
225
226/// A row iterator that yields zero-copy deserialized values.
227pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
228    rows: std::slice::Iter<'a, R>,
229    _marker: std::marker::PhantomData<T>,
230}
231
232impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
233    /// Create a new row iterator.
234    pub fn new(rows: &'a [R]) -> Self {
235        Self {
236            rows: rows.iter(),
237            _marker: std::marker::PhantomData,
238        }
239    }
240}
241
242impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
243    type Item = Result<T, RowError>;
244
245    fn next(&mut self) -> Option<Self::Item> {
246        self.rows.next().map(|row| T::from_row_ref(row))
247    }
248
249    fn size_hint(&self) -> (usize, Option<usize>) {
250        self.rows.size_hint()
251    }
252}
253
254impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
255
256/// A collected result that can either borrow or own data.
257///
258/// This is useful for caching query results while still supporting
259/// zero-copy deserialization for fresh queries.
260#[derive(Debug, Clone)]
261pub enum RowData<'a> {
262    /// Borrowed string data.
263    Borrowed(&'a str),
264    /// Owned string data.
265    Owned(String),
266}
267
268impl<'a> RowData<'a> {
269    /// Get the string value.
270    pub fn as_str(&self) -> &str {
271        match self {
272            Self::Borrowed(s) => s,
273            Self::Owned(s) => s,
274        }
275    }
276
277    /// Convert to owned data.
278    pub fn into_owned(self) -> String {
279        match self {
280            Self::Borrowed(s) => s.to_string(),
281            Self::Owned(s) => s,
282        }
283    }
284
285    /// Create borrowed data.
286    pub const fn borrowed(s: &'a str) -> Self {
287        Self::Borrowed(s)
288    }
289
290    /// Create owned data.
291    pub fn owned(s: impl Into<String>) -> Self {
292        Self::Owned(s.into())
293    }
294}
295
296impl<'a> From<&'a str> for RowData<'a> {
297    fn from(s: &'a str) -> Self {
298        Self::Borrowed(s)
299    }
300}
301
302impl From<String> for RowData<'static> {
303    fn from(s: String) -> Self {
304        Self::Owned(s)
305    }
306}
307
308impl<'a> AsRef<str> for RowData<'a> {
309    fn as_ref(&self) -> &str {
310        self.as_str()
311    }
312}
313
314/// Macro to implement FromRow for simple structs.
315///
316/// This generates efficient deserialization code that minimizes allocations.
317///
318/// # Example
319///
320/// ```rust,ignore
321/// use prax_query::impl_from_row;
322///
323/// struct User {
324///     id: i32,
325///     email: String,
326///     name: Option<String>,
327/// }
328///
329/// impl_from_row!(User {
330///     id: i32,
331///     email: String,
332///     name: Option<String>,
333/// });
334/// ```
335#[macro_export]
336macro_rules! impl_from_row {
337    ($type:ident { $($field:ident : i32),* $(,)? }) => {
338        impl $crate::row::FromRow for $type {
339            fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
340                Ok(Self {
341                    $(
342                        $field: row.get_i32(stringify!($field))?,
343                    )*
344                })
345            }
346        }
347    };
348    ($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
349        impl $crate::row::FromRow for $type {
350            fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
351                Ok(Self {
352                    $(
353                        $field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
354                    )*
355                })
356            }
357        }
358    };
359}
360
361/// Helper function for the impl_from_row macro.
362#[doc(hidden)]
363pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
364    T::from_column(row, column)
365}
366
367/// Trait for types that can be extracted from a column.
368pub trait FromColumn: Sized {
369    /// Extract value from a row column.
370    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
371}
372
373impl FromColumn for i32 {
374    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
375        row.get_i32(column)
376    }
377}
378
379impl FromColumn for i64 {
380    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
381        row.get_i64(column)
382    }
383}
384
385impl FromColumn for f64 {
386    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
387        row.get_f64(column)
388    }
389}
390
391impl FromColumn for bool {
392    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
393        row.get_bool(column)
394    }
395}
396
397impl FromColumn for String {
398    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
399        row.get_string(column)
400    }
401}
402
403impl FromColumn for Option<i32> {
404    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
405        row.get_i32_opt(column)
406    }
407}
408
409impl FromColumn for Option<i64> {
410    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
411        row.get_i64_opt(column)
412    }
413}
414
415impl FromColumn for Option<f64> {
416    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
417        row.get_f64_opt(column)
418    }
419}
420
421impl FromColumn for Option<bool> {
422    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
423        row.get_bool_opt(column)
424    }
425}
426
427impl FromColumn for Option<String> {
428    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
429        row.get_string_opt(column)
430    }
431}
432
433impl FromColumn for Vec<u8> {
434    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
435        row.get_bytes(column).map(|b| b.to_vec())
436    }
437}
438
439impl FromColumn for Option<Vec<u8>> {
440    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
441        row.get_bytes_opt(column).map(|opt| opt.map(|b| b.to_vec()))
442    }
443}
444
445impl FromColumn for chrono::DateTime<chrono::Utc> {
446    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
447        row.get_datetime_utc(column)
448    }
449}
450impl FromColumn for Option<chrono::DateTime<chrono::Utc>> {
451    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
452        row.get_datetime_utc_opt(column)
453    }
454}
455impl FromColumn for chrono::NaiveDateTime {
456    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
457        row.get_naive_datetime(column)
458    }
459}
460impl FromColumn for Option<chrono::NaiveDateTime> {
461    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
462        row.get_naive_datetime_opt(column)
463    }
464}
465impl FromColumn for chrono::NaiveDate {
466    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
467        row.get_naive_date(column)
468    }
469}
470impl FromColumn for Option<chrono::NaiveDate> {
471    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
472        row.get_naive_date_opt(column)
473    }
474}
475impl FromColumn for chrono::NaiveTime {
476    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
477        row.get_naive_time(column)
478    }
479}
480impl FromColumn for Option<chrono::NaiveTime> {
481    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
482        row.get_naive_time_opt(column)
483    }
484}
485impl FromColumn for uuid::Uuid {
486    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
487        row.get_uuid(column)
488    }
489}
490impl FromColumn for Option<uuid::Uuid> {
491    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
492        row.get_uuid_opt(column)
493    }
494}
495impl FromColumn for serde_json::Value {
496    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
497        row.get_json(column)
498    }
499}
500impl FromColumn for Option<serde_json::Value> {
501    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
502        row.get_json_opt(column)
503    }
504}
505impl FromColumn for rust_decimal::Decimal {
506    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
507        row.get_decimal(column)
508    }
509}
510impl FromColumn for Option<rust_decimal::Decimal> {
511    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
512        row.get_decimal_opt(column)
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    // Mock row for testing
521    struct MockRow {
522        data: std::collections::HashMap<String, String>,
523    }
524
525    impl RowRef for MockRow {
526        fn get_i32(&self, column: &str) -> Result<i32, RowError> {
527            self.data
528                .get(column)
529                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
530                .parse()
531                .map_err(|e| RowError::TypeConversion {
532                    column: column.to_string(),
533                    message: format!("{}", e),
534                })
535        }
536
537        fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
538            match self.data.get(column) {
539                Some(v) if v == "NULL" => Ok(None),
540                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
541                    column: column.to_string(),
542                    message: format!("{}", e),
543                }),
544                None => Ok(None),
545            }
546        }
547
548        fn get_i64(&self, column: &str) -> Result<i64, RowError> {
549            self.data
550                .get(column)
551                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
552                .parse()
553                .map_err(|e| RowError::TypeConversion {
554                    column: column.to_string(),
555                    message: format!("{}", e),
556                })
557        }
558
559        fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
560            match self.data.get(column) {
561                Some(v) if v == "NULL" => Ok(None),
562                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
563                    column: column.to_string(),
564                    message: format!("{}", e),
565                }),
566                None => Ok(None),
567            }
568        }
569
570        fn get_f64(&self, column: &str) -> Result<f64, RowError> {
571            self.data
572                .get(column)
573                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
574                .parse()
575                .map_err(|e| RowError::TypeConversion {
576                    column: column.to_string(),
577                    message: format!("{}", e),
578                })
579        }
580
581        fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
582            match self.data.get(column) {
583                Some(v) if v == "NULL" => Ok(None),
584                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
585                    column: column.to_string(),
586                    message: format!("{}", e),
587                }),
588                None => Ok(None),
589            }
590        }
591
592        fn get_bool(&self, column: &str) -> Result<bool, RowError> {
593            let v = self
594                .data
595                .get(column)
596                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
597            match v.as_str() {
598                "true" | "t" | "1" => Ok(true),
599                "false" | "f" | "0" => Ok(false),
600                _ => Err(RowError::TypeConversion {
601                    column: column.to_string(),
602                    message: "invalid boolean".to_string(),
603                }),
604            }
605        }
606
607        fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
608            match self.data.get(column) {
609                Some(v) if v == "NULL" => Ok(None),
610                Some(v) => match v.as_str() {
611                    "true" | "t" | "1" => Ok(Some(true)),
612                    "false" | "f" | "0" => Ok(Some(false)),
613                    _ => Err(RowError::TypeConversion {
614                        column: column.to_string(),
615                        message: "invalid boolean".to_string(),
616                    }),
617                },
618                None => Ok(None),
619            }
620        }
621
622        fn get_str(&self, column: &str) -> Result<&str, RowError> {
623            self.data
624                .get(column)
625                .map(|s| s.as_str())
626                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
627        }
628
629        fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
630            match self.data.get(column) {
631                Some(v) if v == "NULL" => Ok(None),
632                Some(v) => Ok(Some(v.as_str())),
633                None => Ok(None),
634            }
635        }
636
637        fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
638            self.data
639                .get(column)
640                .map(|s| s.as_bytes())
641                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
642        }
643
644        fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
645            match self.data.get(column) {
646                Some(v) if v == "NULL" => Ok(None),
647                Some(v) => Ok(Some(v.as_bytes())),
648                None => Ok(None),
649            }
650        }
651    }
652
653    #[test]
654    fn test_row_ref_get_i32() {
655        let mut data = std::collections::HashMap::new();
656        data.insert("id".to_string(), "42".to_string());
657        let row = MockRow { data };
658
659        assert_eq!(row.get_i32("id").unwrap(), 42);
660    }
661
662    #[test]
663    fn test_row_ref_get_str_zero_copy() {
664        let mut data = std::collections::HashMap::new();
665        data.insert("email".to_string(), "test@example.com".to_string());
666        let row = MockRow { data };
667
668        let email = row.get_str("email").unwrap();
669        assert_eq!(email, "test@example.com");
670        // Note: In a real implementation, this would be zero-copy
671        // borrowing directly from the row's buffer
672    }
673
674    #[test]
675    fn test_row_data() {
676        let borrowed: RowData = RowData::borrowed("hello");
677        assert_eq!(borrowed.as_str(), "hello");
678
679        let owned: RowData = RowData::owned("world".to_string());
680        assert_eq!(owned.as_str(), "world");
681    }
682
683    #[test]
684    fn default_datetime_method_errors() {
685        let mut data = std::collections::HashMap::new();
686        data.insert("created_at".into(), "2026-04-27T00:00:00Z".into());
687        let row = MockRow { data };
688        assert!(matches!(
689            row.get_datetime_utc("created_at"),
690            Err(RowError::TypeConversion { .. })
691        ));
692    }
693}