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