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
127/// Trait for types that can be deserialized from a row reference (zero-copy).
128///
129/// This trait uses lifetimes to enable borrowing string data directly
130/// from the row, avoiding allocations.
131pub trait FromRowRef<'a>: Sized {
132    /// Deserialize from a row reference.
133    fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
134}
135
136/// Trait for types that can be deserialized from a row (owning).
137///
138/// This is the traditional deserialization trait that takes ownership
139/// of all data.
140pub trait FromRow: Sized {
141    /// Deserialize from a row.
142    fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
143}
144
145// Blanket implementation: any FromRow can be used with any row
146impl<T: FromRow> FromRowRef<'_> for T {
147    fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
148        T::from_row(row)
149    }
150}
151
152/// A row iterator that yields zero-copy deserialized values.
153pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
154    rows: std::slice::Iter<'a, R>,
155    _marker: std::marker::PhantomData<T>,
156}
157
158impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
159    /// Create a new row iterator.
160    pub fn new(rows: &'a [R]) -> Self {
161        Self {
162            rows: rows.iter(),
163            _marker: std::marker::PhantomData,
164        }
165    }
166}
167
168impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
169    type Item = Result<T, RowError>;
170
171    fn next(&mut self) -> Option<Self::Item> {
172        self.rows.next().map(|row| T::from_row_ref(row))
173    }
174
175    fn size_hint(&self) -> (usize, Option<usize>) {
176        self.rows.size_hint()
177    }
178}
179
180impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
181
182/// A collected result that can either borrow or own data.
183///
184/// This is useful for caching query results while still supporting
185/// zero-copy deserialization for fresh queries.
186#[derive(Debug, Clone)]
187pub enum RowData<'a> {
188    /// Borrowed string data.
189    Borrowed(&'a str),
190    /// Owned string data.
191    Owned(String),
192}
193
194impl<'a> RowData<'a> {
195    /// Get the string value.
196    pub fn as_str(&self) -> &str {
197        match self {
198            Self::Borrowed(s) => s,
199            Self::Owned(s) => s,
200        }
201    }
202
203    /// Convert to owned data.
204    pub fn into_owned(self) -> String {
205        match self {
206            Self::Borrowed(s) => s.to_string(),
207            Self::Owned(s) => s,
208        }
209    }
210
211    /// Create borrowed data.
212    pub const fn borrowed(s: &'a str) -> Self {
213        Self::Borrowed(s)
214    }
215
216    /// Create owned data.
217    pub fn owned(s: impl Into<String>) -> Self {
218        Self::Owned(s.into())
219    }
220}
221
222impl<'a> From<&'a str> for RowData<'a> {
223    fn from(s: &'a str) -> Self {
224        Self::Borrowed(s)
225    }
226}
227
228impl From<String> for RowData<'static> {
229    fn from(s: String) -> Self {
230        Self::Owned(s)
231    }
232}
233
234impl<'a> AsRef<str> for RowData<'a> {
235    fn as_ref(&self) -> &str {
236        self.as_str()
237    }
238}
239
240/// Macro to implement FromRow for simple structs.
241///
242/// This generates efficient deserialization code that minimizes allocations.
243///
244/// # Example
245///
246/// ```rust,ignore
247/// use prax_query::impl_from_row;
248///
249/// struct User {
250///     id: i32,
251///     email: String,
252///     name: Option<String>,
253/// }
254///
255/// impl_from_row!(User {
256///     id: i32,
257///     email: String,
258///     name: Option<String>,
259/// });
260/// ```
261#[macro_export]
262macro_rules! impl_from_row {
263    ($type:ident { $($field:ident : i32),* $(,)? }) => {
264        impl $crate::row::FromRow for $type {
265            fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
266                Ok(Self {
267                    $(
268                        $field: row.get_i32(stringify!($field))?,
269                    )*
270                })
271            }
272        }
273    };
274    ($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
275        impl $crate::row::FromRow for $type {
276            fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
277                Ok(Self {
278                    $(
279                        $field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
280                    )*
281                })
282            }
283        }
284    };
285}
286
287/// Helper function for the impl_from_row macro.
288#[doc(hidden)]
289pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
290    T::from_column(row, column)
291}
292
293/// Trait for types that can be extracted from a column.
294pub trait FromColumn: Sized {
295    /// Extract value from a row column.
296    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
297}
298
299impl FromColumn for i32 {
300    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
301        row.get_i32(column)
302    }
303}
304
305impl FromColumn for i64 {
306    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
307        row.get_i64(column)
308    }
309}
310
311impl FromColumn for f64 {
312    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
313        row.get_f64(column)
314    }
315}
316
317impl FromColumn for bool {
318    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
319        row.get_bool(column)
320    }
321}
322
323impl FromColumn for String {
324    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
325        row.get_string(column)
326    }
327}
328
329impl FromColumn for Option<i32> {
330    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
331        row.get_i32_opt(column)
332    }
333}
334
335impl FromColumn for Option<i64> {
336    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
337        row.get_i64_opt(column)
338    }
339}
340
341impl FromColumn for Option<f64> {
342    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
343        row.get_f64_opt(column)
344    }
345}
346
347impl FromColumn for Option<bool> {
348    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
349        row.get_bool_opt(column)
350    }
351}
352
353impl FromColumn for Option<String> {
354    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
355        row.get_string_opt(column)
356    }
357}
358
359impl FromColumn for Vec<u8> {
360    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
361        row.get_bytes(column).map(|b| b.to_vec())
362    }
363}
364
365impl FromColumn for Option<Vec<u8>> {
366    fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
367        row.get_bytes_opt(column).map(|opt| opt.map(|b| b.to_vec()))
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    // Mock row for testing
376    struct MockRow {
377        data: std::collections::HashMap<String, String>,
378    }
379
380    impl RowRef for MockRow {
381        fn get_i32(&self, column: &str) -> Result<i32, RowError> {
382            self.data
383                .get(column)
384                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
385                .parse()
386                .map_err(|e| RowError::TypeConversion {
387                    column: column.to_string(),
388                    message: format!("{}", e),
389                })
390        }
391
392        fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
393            match self.data.get(column) {
394                Some(v) if v == "NULL" => Ok(None),
395                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
396                    column: column.to_string(),
397                    message: format!("{}", e),
398                }),
399                None => Ok(None),
400            }
401        }
402
403        fn get_i64(&self, column: &str) -> Result<i64, RowError> {
404            self.data
405                .get(column)
406                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
407                .parse()
408                .map_err(|e| RowError::TypeConversion {
409                    column: column.to_string(),
410                    message: format!("{}", e),
411                })
412        }
413
414        fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
415            match self.data.get(column) {
416                Some(v) if v == "NULL" => Ok(None),
417                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
418                    column: column.to_string(),
419                    message: format!("{}", e),
420                }),
421                None => Ok(None),
422            }
423        }
424
425        fn get_f64(&self, column: &str) -> Result<f64, RowError> {
426            self.data
427                .get(column)
428                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
429                .parse()
430                .map_err(|e| RowError::TypeConversion {
431                    column: column.to_string(),
432                    message: format!("{}", e),
433                })
434        }
435
436        fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
437            match self.data.get(column) {
438                Some(v) if v == "NULL" => Ok(None),
439                Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
440                    column: column.to_string(),
441                    message: format!("{}", e),
442                }),
443                None => Ok(None),
444            }
445        }
446
447        fn get_bool(&self, column: &str) -> Result<bool, RowError> {
448            let v = self
449                .data
450                .get(column)
451                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
452            match v.as_str() {
453                "true" | "t" | "1" => Ok(true),
454                "false" | "f" | "0" => Ok(false),
455                _ => Err(RowError::TypeConversion {
456                    column: column.to_string(),
457                    message: "invalid boolean".to_string(),
458                }),
459            }
460        }
461
462        fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
463            match self.data.get(column) {
464                Some(v) if v == "NULL" => Ok(None),
465                Some(v) => match v.as_str() {
466                    "true" | "t" | "1" => Ok(Some(true)),
467                    "false" | "f" | "0" => Ok(Some(false)),
468                    _ => Err(RowError::TypeConversion {
469                        column: column.to_string(),
470                        message: "invalid boolean".to_string(),
471                    }),
472                },
473                None => Ok(None),
474            }
475        }
476
477        fn get_str(&self, column: &str) -> Result<&str, RowError> {
478            self.data
479                .get(column)
480                .map(|s| s.as_str())
481                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
482        }
483
484        fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
485            match self.data.get(column) {
486                Some(v) if v == "NULL" => Ok(None),
487                Some(v) => Ok(Some(v.as_str())),
488                None => Ok(None),
489            }
490        }
491
492        fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
493            self.data
494                .get(column)
495                .map(|s| s.as_bytes())
496                .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
497        }
498
499        fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
500            match self.data.get(column) {
501                Some(v) if v == "NULL" => Ok(None),
502                Some(v) => Ok(Some(v.as_bytes())),
503                None => Ok(None),
504            }
505        }
506    }
507
508    #[test]
509    fn test_row_ref_get_i32() {
510        let mut data = std::collections::HashMap::new();
511        data.insert("id".to_string(), "42".to_string());
512        let row = MockRow { data };
513
514        assert_eq!(row.get_i32("id").unwrap(), 42);
515    }
516
517    #[test]
518    fn test_row_ref_get_str_zero_copy() {
519        let mut data = std::collections::HashMap::new();
520        data.insert("email".to_string(), "test@example.com".to_string());
521        let row = MockRow { data };
522
523        let email = row.get_str("email").unwrap();
524        assert_eq!(email, "test@example.com");
525        // Note: In a real implementation, this would be zero-copy
526        // borrowing directly from the row's buffer
527    }
528
529    #[test]
530    fn test_row_data() {
531        let borrowed: RowData = RowData::borrowed("hello");
532        assert_eq!(borrowed.as_str(), "hello");
533
534        let owned: RowData = RowData::owned("world".to_string());
535        assert_eq!(owned.as_str(), "world");
536    }
537}
538
539
540
541
542
543