Skip to main content

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