facet_tokio_postgres/
lib.rs

1//! Deserialize tokio-postgres Rows into any type implementing Facet.
2//!
3//! This crate provides a bridge between tokio-postgres and facet, allowing you to
4//! deserialize database rows directly into Rust structs that implement `Facet`.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use facet::Facet;
10//! use facet_tokio_postgres::from_row;
11//!
12//! #[derive(Debug, Facet)]
13//! struct User {
14//!     id: i32,
15//!     name: String,
16//!     email: Option<String>,
17//! }
18//!
19//! // After executing a query...
20//! let row = client.query_one("SELECT id, name, email FROM users WHERE id = $1", &[&1]).await?;
21//! let user: User = from_row(&row)?;
22//! ```
23
24extern crate alloc;
25
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28
29use facet_core::{Facet, Shape, StructKind, Type, UserType};
30use facet_reflect::{Partial, ReflectError};
31use tokio_postgres::Row;
32
33/// Error type for Row deserialization.
34#[derive(Debug)]
35pub enum Error {
36    /// A required column was not found in the row
37    MissingColumn {
38        /// Name of the missing column
39        column: String,
40    },
41    /// The column type doesn't match the expected Rust type
42    TypeMismatch {
43        /// Name of the column
44        column: String,
45        /// Expected type
46        expected: &'static Shape,
47        /// Actual error from postgres
48        source: tokio_postgres::Error,
49    },
50    /// Error from facet reflection
51    Reflect(ReflectError),
52    /// The target type is not a struct
53    NotAStruct {
54        /// The shape we tried to deserialize into
55        shape: &'static Shape,
56    },
57    /// Unsupported field type
58    UnsupportedType {
59        /// Name of the field
60        field: String,
61        /// The shape of the field
62        shape: &'static Shape,
63    },
64}
65
66impl core::fmt::Display for Error {
67    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68        match self {
69            Error::MissingColumn { column } => write!(f, "missing column: {column}"),
70            Error::TypeMismatch {
71                column, expected, ..
72            } => {
73                write!(
74                    f,
75                    "type mismatch for column '{column}': expected {expected}"
76                )
77            }
78            Error::Reflect(e) => write!(f, "reflection error: {e}"),
79            Error::NotAStruct { shape } => {
80                write!(f, "cannot deserialize row into non-struct type: {shape}")
81            }
82            Error::UnsupportedType { field, shape } => {
83                write!(f, "unsupported type for field '{field}': {shape}")
84            }
85        }
86    }
87}
88
89impl std::error::Error for Error {
90    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
91        match self {
92            Error::TypeMismatch { source, .. } => Some(source),
93            Error::Reflect(e) => Some(e),
94            _ => None,
95        }
96    }
97}
98
99impl From<ReflectError> for Error {
100    fn from(e: ReflectError) -> Self {
101        Error::Reflect(e)
102    }
103}
104
105/// Result type for Row deserialization.
106pub type Result<T> = core::result::Result<T, Error>;
107
108/// Deserialize a tokio-postgres Row into any type implementing Facet.
109///
110/// The type must be a struct with named fields. Each field name is used to look up
111/// the corresponding column in the row.
112///
113/// # Example
114///
115/// ```ignore
116/// use facet::Facet;
117/// use facet_tokio_postgres::from_row;
118///
119/// #[derive(Debug, Facet)]
120/// struct User {
121///     id: i32,
122///     name: String,
123///     active: bool,
124/// }
125///
126/// let row = client.query_one("SELECT id, name, active FROM users LIMIT 1", &[]).await?;
127/// let user: User = from_row(&row)?;
128/// ```
129pub fn from_row<'facet, T: Facet<'facet>>(row: &Row) -> Result<T> {
130    let partial = Partial::alloc::<T>()?;
131    let partial = deserialize_row_into(row, partial, T::SHAPE)?;
132    let heap_value = partial.build()?;
133    Ok(heap_value.materialize()?)
134}
135
136/// Internal function to deserialize a row into a Partial.
137fn deserialize_row_into<'p>(
138    row: &Row,
139    partial: Partial<'p>,
140    shape: &'static Shape,
141) -> Result<Partial<'p>> {
142    let struct_def = match &shape.ty {
143        Type::User(UserType::Struct(s)) if s.kind == StructKind::Struct => s,
144        _ => {
145            return Err(Error::NotAStruct { shape });
146        }
147    };
148
149    let mut partial = partial;
150    let num_fields = struct_def.fields.len();
151    let mut fields_set = alloc::vec![false; num_fields];
152
153    for (idx, field) in struct_def.fields.iter().enumerate() {
154        let column_name = field.rename.unwrap_or(field.name);
155
156        // Check if column exists
157        let column_idx = match row.columns().iter().position(|c| c.name() == column_name) {
158            Some(idx) => idx,
159            None => {
160                // Try to set default for missing column
161                partial =
162                    partial
163                        .set_nth_field_to_default(idx)
164                        .map_err(|_| Error::MissingColumn {
165                            column: column_name.to_string(),
166                        })?;
167                fields_set[idx] = true;
168                continue;
169            }
170        };
171
172        partial = partial.begin_field(field.name)?;
173        partial = deserialize_column(row, column_idx, column_name, partial, field.shape())?;
174        partial = partial.end()?;
175        fields_set[idx] = true;
176    }
177
178    Ok(partial)
179}
180
181/// Deserialize a single column value into a Partial.
182fn deserialize_column<'p>(
183    row: &Row,
184    column_idx: usize,
185    column_name: &str,
186    partial: Partial<'p>,
187    shape: &'static Shape,
188) -> Result<Partial<'p>> {
189    use facet_core::{Def, NumericType, PrimitiveType};
190
191    let mut partial = partial;
192
193    // Handle Option types first
194    if let Def::Option(_) = &shape.def {
195        return deserialize_option_column(row, column_idx, column_name, partial, shape);
196    }
197
198    // Handle based on type
199    match &shape.ty {
200        // Signed integers
201        Type::Primitive(PrimitiveType::Numeric(NumericType::Integer { signed: true })) => {
202            match shape.type_identifier {
203                "i8" => {
204                    let val: i8 = get_column(row, column_idx, column_name, shape)?;
205                    partial = partial.set(val)?;
206                }
207                "i16" => {
208                    let val: i16 = get_column(row, column_idx, column_name, shape)?;
209                    partial = partial.set(val)?;
210                }
211                "i32" => {
212                    let val: i32 = get_column(row, column_idx, column_name, shape)?;
213                    partial = partial.set(val)?;
214                }
215                "i64" => {
216                    let val: i64 = get_column(row, column_idx, column_name, shape)?;
217                    partial = partial.set(val)?;
218                }
219                _ => {
220                    return Err(Error::UnsupportedType {
221                        field: column_name.to_string(),
222                        shape,
223                    });
224                }
225            }
226        }
227
228        // Unsigned integers (postgres doesn't have native unsigned, but we can try)
229        Type::Primitive(PrimitiveType::Numeric(NumericType::Integer { signed: false })) => {
230            // Postgres doesn't have unsigned types natively, so we read as signed and convert
231            match shape.type_identifier {
232                "u8" => {
233                    let val: i16 = get_column(row, column_idx, column_name, shape)?;
234                    partial = partial.set(val as u8)?;
235                }
236                "u16" => {
237                    let val: i32 = get_column(row, column_idx, column_name, shape)?;
238                    partial = partial.set(val as u16)?;
239                }
240                "u32" => {
241                    let val: i64 = get_column(row, column_idx, column_name, shape)?;
242                    partial = partial.set(val as u32)?;
243                }
244                "u64" => {
245                    // For u64, we might need to use BIGINT and hope it fits
246                    let val: i64 = get_column(row, column_idx, column_name, shape)?;
247                    partial = partial.set(val as u64)?;
248                }
249                _ => {
250                    return Err(Error::UnsupportedType {
251                        field: column_name.to_string(),
252                        shape,
253                    });
254                }
255            }
256        }
257
258        // Floats
259        Type::Primitive(PrimitiveType::Numeric(NumericType::Float)) => {
260            match shape.type_identifier {
261                "f32" => {
262                    let val: f32 = get_column(row, column_idx, column_name, shape)?;
263                    partial = partial.set(val)?;
264                }
265                "f64" => {
266                    let val: f64 = get_column(row, column_idx, column_name, shape)?;
267                    partial = partial.set(val)?;
268                }
269                _ => {
270                    return Err(Error::UnsupportedType {
271                        field: column_name.to_string(),
272                        shape,
273                    });
274                }
275            }
276        }
277
278        // Booleans
279        Type::Primitive(PrimitiveType::Boolean) => {
280            let val: bool = get_column(row, column_idx, column_name, shape)?;
281            partial = partial.set(val)?;
282        }
283
284        // Strings
285        Type::Primitive(PrimitiveType::Textual(_)) | Type::User(_)
286            if shape.type_identifier == "String" =>
287        {
288            let val: String = get_column(row, column_idx, column_name, shape)?;
289            partial = partial.set(val)?;
290        }
291
292        // Vec<u8> for bytea - check if it's a List of u8
293        _ if matches!(&shape.def, Def::List(_))
294            && shape
295                .inner
296                .is_some_and(|inner| inner.type_identifier == "u8") =>
297        {
298            let val: Vec<u8> = get_column(row, column_idx, column_name, shape)?;
299            partial = partial.set(val)?;
300        }
301
302        // rust_decimal::Decimal for NUMERIC columns
303        #[cfg(feature = "rust_decimal")]
304        _ if shape.type_identifier == "Decimal" => {
305            let val: rust_decimal::Decimal = get_column(row, column_idx, column_name, shape)?;
306            partial = partial.set(val)?;
307        }
308
309        // jiff::Timestamp for TIMESTAMPTZ columns
310        #[cfg(feature = "jiff02")]
311        _ if shape.type_identifier == "Timestamp" && shape.module_path == Some("jiff") => {
312            let val: jiff::Timestamp = get_column(row, column_idx, column_name, shape)?;
313            partial = partial.set(val)?;
314        }
315
316        // jiff::civil::DateTime for TIMESTAMP (without timezone) columns
317        #[cfg(feature = "jiff02")]
318        _ if shape.type_identifier == "DateTime" && shape.module_path == Some("jiff") => {
319            let val: jiff::civil::DateTime = get_column(row, column_idx, column_name, shape)?;
320            partial = partial.set(val)?;
321        }
322
323        // Fallback: try to use parse if the type supports it
324        _ => {
325            if shape.vtable.has_parse() {
326                // Try getting as string and parsing
327                let val: String = get_column(row, column_idx, column_name, shape)?;
328                partial = partial.parse_from_str(&val)?;
329            } else {
330                return Err(Error::UnsupportedType {
331                    field: column_name.to_string(),
332                    shape,
333                });
334            }
335        }
336    }
337
338    Ok(partial)
339}
340
341/// Deserialize an Option column.
342fn deserialize_option_column<'p>(
343    row: &Row,
344    column_idx: usize,
345    column_name: &str,
346    partial: Partial<'p>,
347    shape: &'static Shape,
348) -> Result<Partial<'p>> {
349    use facet_core::{NumericType, PrimitiveType};
350
351    let inner_shape = shape.inner.expect("Option must have inner shape");
352    let mut partial = partial;
353
354    // Try to get the value directly as Option<T> for the appropriate type
355    // This handles NULL detection properly for each type
356    macro_rules! try_option {
357        ($t:ty) => {{
358            let val: Option<$t> = get_column(row, column_idx, column_name, shape)?;
359            match val {
360                Some(v) => {
361                    partial = partial.begin_some()?;
362                    partial = partial.set(v)?;
363                    partial = partial.end()?;
364                }
365                None => {
366                    partial = partial.set_default()?;
367                }
368            }
369            return Ok(partial);
370        }};
371    }
372
373    // Match on inner type to get the right Option<T>
374    match &inner_shape.ty {
375        Type::Primitive(PrimitiveType::Numeric(NumericType::Integer { signed: true })) => {
376            match inner_shape.type_identifier {
377                "i8" => try_option!(i8),
378                "i16" => try_option!(i16),
379                "i32" => try_option!(i32),
380                "i64" => try_option!(i64),
381                _ => {}
382            }
383        }
384        Type::Primitive(PrimitiveType::Numeric(NumericType::Integer { signed: false })) => {
385            // Postgres doesn't have unsigned, read as next larger signed type
386            match inner_shape.type_identifier {
387                "u8" => {
388                    let val: Option<i16> = get_column(row, column_idx, column_name, shape)?;
389                    match val {
390                        Some(v) => {
391                            partial = partial.begin_some()?;
392                            partial = partial.set(v as u8)?;
393                            partial = partial.end()?;
394                        }
395                        None => {
396                            partial = partial.set_default()?;
397                        }
398                    }
399                    return Ok(partial);
400                }
401                "u16" => {
402                    let val: Option<i32> = get_column(row, column_idx, column_name, shape)?;
403                    match val {
404                        Some(v) => {
405                            partial = partial.begin_some()?;
406                            partial = partial.set(v as u16)?;
407                            partial = partial.end()?;
408                        }
409                        None => {
410                            partial = partial.set_default()?;
411                        }
412                    }
413                    return Ok(partial);
414                }
415                "u32" => {
416                    let val: Option<i64> = get_column(row, column_idx, column_name, shape)?;
417                    match val {
418                        Some(v) => {
419                            partial = partial.begin_some()?;
420                            partial = partial.set(v as u32)?;
421                            partial = partial.end()?;
422                        }
423                        None => {
424                            partial = partial.set_default()?;
425                        }
426                    }
427                    return Ok(partial);
428                }
429                "u64" => {
430                    let val: Option<i64> = get_column(row, column_idx, column_name, shape)?;
431                    match val {
432                        Some(v) => {
433                            partial = partial.begin_some()?;
434                            partial = partial.set(v as u64)?;
435                            partial = partial.end()?;
436                        }
437                        None => {
438                            partial = partial.set_default()?;
439                        }
440                    }
441                    return Ok(partial);
442                }
443                _ => {}
444            }
445        }
446        Type::Primitive(PrimitiveType::Numeric(NumericType::Float)) => {
447            match inner_shape.type_identifier {
448                "f32" => try_option!(f32),
449                "f64" => try_option!(f64),
450                _ => {}
451            }
452        }
453        Type::Primitive(PrimitiveType::Boolean) => try_option!(bool),
454        _ if inner_shape.type_identifier == "String" => try_option!(String),
455        #[cfg(feature = "rust_decimal")]
456        _ if inner_shape.type_identifier == "Decimal" => try_option!(rust_decimal::Decimal),
457        #[cfg(feature = "jiff02")]
458        _ if inner_shape.type_identifier == "Timestamp"
459            && inner_shape.module_path == Some("jiff") =>
460        {
461            try_option!(jiff::Timestamp)
462        }
463        #[cfg(feature = "jiff02")]
464        _ if inner_shape.type_identifier == "DateTime"
465            && inner_shape.module_path == Some("jiff") =>
466        {
467            try_option!(jiff::civil::DateTime)
468        }
469        _ => {}
470    }
471
472    // Fallback: try String and parse
473    if inner_shape.vtable.has_parse() {
474        let val: Option<String> = get_column(row, column_idx, column_name, shape)?;
475        match val {
476            Some(s) => {
477                partial = partial.begin_some()?;
478                partial = partial.parse_from_str(&s)?;
479                partial = partial.end()?;
480            }
481            None => {
482                partial = partial.set_default()?;
483            }
484        }
485        return Ok(partial);
486    }
487
488    Err(Error::UnsupportedType {
489        field: column_name.to_string(),
490        shape: inner_shape,
491    })
492}
493
494/// Get a column value with proper error handling.
495fn get_column<'a, T>(row: &'a Row, idx: usize, name: &str, shape: &'static Shape) -> Result<T>
496where
497    T: postgres_types::FromSql<'a>,
498{
499    row.try_get::<_, T>(idx).map_err(|e| Error::TypeMismatch {
500        column: name.to_string(),
501        expected: shape,
502        source: e,
503    })
504}