Skip to main content

prax_postgres/
row_ref.rs

1//! Bridge between `tokio_postgres::Row` and `prax_query::row::RowRef`.
2//!
3//! `RowRef` is defined in `prax-query` and `Row` is defined in `tokio-postgres`;
4//! both are foreign to this crate, so Rust's orphan rules forbid a direct
5//! `impl RowRef for Row`. We wrap the row in a `#[repr(transparent)]` newtype
6//! (`PgRow`) and implement the trait on the wrapper.
7//!
8//! ## Dual API surface
9//!
10//! `PgRow` derefs to the wrapped `Row`, so callers can use either API:
11//! * The generic `RowRef` interface (`pg_row.get_i32("id")`) — portable across
12//!   drivers and what generated `FromRow` impls use.
13//! * The native `tokio_postgres::Row` interface (`pg_row.try_get::<_, i32>("id")`)
14//!   — full access to the driver's type system for columns `RowRef` does not
15//!   model (arrays, range types, etc.).
16//!
17//! ## `rust_decimal` support
18//!
19//! `tokio-postgres` 0.7 ships no `with-rust_decimal-*` feature gate of its
20//! own; instead the `rust_decimal` crate's `db-tokio-postgres` feature
21//! (enabled in this crate's `Cargo.toml`) provides the `FromSql`/`ToSql`
22//! impls bridging NUMERIC and `rust_decimal::Decimal`. `PgRow::get_decimal`
23//! and `PgRow::get_decimal_opt` therefore decode NUMERIC columns directly
24//! through the driver, the same way `decode_aggregate_cell` reads AVG()
25//! results.
26
27use std::error::Error as StdError;
28
29use prax_query::row::{RowError, RowRef, into_row_error};
30use tokio_postgres::Row;
31use tokio_postgres::types::{FromSql, Kind, Type};
32
33/// `FromSql` shim that accepts any column type and decodes its raw
34/// bytes as UTF-8.
35///
36/// Used to read postgres `ENUM` columns into a Rust `String`, since
37/// `&str: FromSql` only `accepts` TEXT/VARCHAR/BPCHAR/NAME/UNKNOWN
38/// (plus a few citext-shaped names). User-defined enums encode as
39/// raw UTF-8 on the wire — the same shape `text_from_sql` would
40/// produce — so this just runs the bytes through `str::from_utf8`.
41struct AnyText(String);
42
43impl<'a> FromSql<'a> for AnyText {
44    fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn StdError + Sync + Send>> {
45        Ok(AnyText(std::str::from_utf8(raw)?.to_owned()))
46    }
47    fn accepts(_ty: &Type) -> bool {
48        true
49    }
50}
51
52/// `FromSql` shim used exclusively for null-probe logic.
53///
54/// `NullProbe` accepts every Postgres wire type and discards the bytes
55/// entirely — we only care whether the column is NULL, not what the
56/// value is. This avoids the UTF-8 conversion failure that `AnyText`
57/// would incur on binary-encoded types (UUID, INTEGER, BYTEA, etc.)
58/// when probing a non-null column.
59///
60/// `Option<NullProbe>` deserialises to `None` on NULL and `Some(NullProbe)`
61/// on any non-null value regardless of the column's OID, making it the
62/// correct type for `RowRef::is_null` overrides on drivers where the
63/// default `get_str_opt` fallback would reject non-text columns.
64struct NullProbe;
65
66impl<'a> FromSql<'a> for NullProbe {
67    fn from_sql(_ty: &Type, _raw: &'a [u8]) -> Result<Self, Box<dyn StdError + Sync + Send>> {
68        Ok(NullProbe)
69    }
70    fn accepts(_ty: &Type) -> bool {
71        true
72    }
73}
74
75/// Newtype wrapper around `tokio_postgres::Row` that implements
76/// `prax_query::row::RowRef`.
77///
78/// The inner row is private; use [`PgRow::into_inner`] when ownership is
79/// needed (e.g., forwarding to a tokio-postgres API that consumes `Row`).
80/// For read-only access, the `Deref<Target = Row>` impl lets you call
81/// any `Row` method directly.
82#[repr(transparent)]
83pub struct PgRow(Row);
84
85impl PgRow {
86    /// Move the wrapped `tokio_postgres::Row` out of this wrapper.
87    pub fn into_inner(self) -> Row {
88        self.0
89    }
90}
91
92impl std::ops::Deref for PgRow {
93    type Target = Row;
94    fn deref(&self) -> &Self::Target {
95        &self.0
96    }
97}
98
99impl From<Row> for PgRow {
100    fn from(row: Row) -> Self {
101        PgRow(row)
102    }
103}
104
105impl RowRef for PgRow {
106    fn get_i32(&self, c: &str) -> Result<i32, RowError> {
107        into_row_error(c, self.try_get::<_, i32>(c))
108    }
109    fn get_i32_opt(&self, c: &str) -> Result<Option<i32>, RowError> {
110        into_row_error(c, self.try_get::<_, Option<i32>>(c))
111    }
112    fn get_i64(&self, c: &str) -> Result<i64, RowError> {
113        into_row_error(c, self.try_get::<_, i64>(c))
114    }
115    fn get_i64_opt(&self, c: &str) -> Result<Option<i64>, RowError> {
116        into_row_error(c, self.try_get::<_, Option<i64>>(c))
117    }
118    fn get_f64(&self, c: &str) -> Result<f64, RowError> {
119        into_row_error(c, self.try_get::<_, f64>(c))
120    }
121    fn get_f64_opt(&self, c: &str) -> Result<Option<f64>, RowError> {
122        into_row_error(c, self.try_get::<_, Option<f64>>(c))
123    }
124    fn get_bool(&self, c: &str) -> Result<bool, RowError> {
125        into_row_error(c, self.try_get::<_, bool>(c))
126    }
127    fn get_bool_opt(&self, c: &str) -> Result<Option<bool>, RowError> {
128        into_row_error(c, self.try_get::<_, Option<bool>>(c))
129    }
130    fn get_str(&self, c: &str) -> Result<&str, RowError> {
131        into_row_error(c, self.try_get::<_, &str>(c))
132    }
133    fn get_str_opt(&self, c: &str) -> Result<Option<&str>, RowError> {
134        into_row_error(c, self.try_get::<_, Option<&str>>(c))
135    }
136    /// Override the trait default (which is `get_str + to_string`) so
137    /// we can decode columns whose Postgres-side type is *not* TEXT but
138    /// whose Rust-side codegen has emitted `pub field: String`:
139    ///
140    /// * `UUID` columns — emitted as `String` for Prisma `String @db.Uuid`
141    ///   fields. We decode via `uuid::Uuid::from_sql` and stringify.
142    /// * User-defined `ENUM` columns — also emitted as `String` via
143    ///   `FromColumn`'s `get_string` call (see codegen for `enum`
144    ///   variants). We decode via `AnyText` since `&str: FromSql` does
145    ///   not accept enum types.
146    ///
147    /// Without this override, the row decode fails with `"error
148    /// deserializing column N"` and the whole query bubbles up a
149    /// `[P6003] type conversion error`.
150    ///
151    /// A matching override on `get_string_opt` covers nullable variants.
152    fn get_string(&self, c: &str) -> Result<String, RowError> {
153        let columns = self.0.columns();
154        if let Some(col) = columns.iter().find(|col| col.name() == c) {
155            let ty = col.type_();
156            if *ty == Type::UUID {
157                return into_row_error(c, self.try_get::<_, ::uuid::Uuid>(c))
158                    .map(|u| u.to_string());
159            }
160            if matches!(ty.kind(), Kind::Enum(_)) {
161                return into_row_error(c, self.try_get::<_, AnyText>(c)).map(|t| t.0);
162            }
163        }
164        into_row_error(c, self.try_get::<_, &str>(c)).map(|s| s.to_string())
165    }
166    fn get_string_opt(&self, c: &str) -> Result<Option<String>, RowError> {
167        let columns = self.0.columns();
168        if let Some(col) = columns.iter().find(|col| col.name() == c) {
169            let ty = col.type_();
170            if *ty == Type::UUID {
171                return into_row_error(c, self.try_get::<_, Option<::uuid::Uuid>>(c))
172                    .map(|opt| opt.map(|u| u.to_string()));
173            }
174            if matches!(ty.kind(), Kind::Enum(_)) {
175                return into_row_error(c, self.try_get::<_, Option<AnyText>>(c))
176                    .map(|opt| opt.map(|t| t.0));
177            }
178        }
179        into_row_error(c, self.try_get::<_, Option<&str>>(c)).map(|opt| opt.map(|s| s.to_string()))
180    }
181    /// Override the trait default (which falls back to `get_str_opt`, and
182    /// therefore fails for non-TEXT column types like INTEGER, UUID, etc.)
183    /// with a type-agnostic null probe.
184    ///
185    /// `NullProbe: FromSql` accepts every Postgres wire type and discards
186    /// the payload entirely, so `Option<NullProbe>` deserialises to `None`
187    /// on NULL and `Some(NullProbe)` on any non-null value — regardless of
188    /// the column's OID — without attempting any byte interpretation. This
189    /// is exactly what the blanket `impl<T: FromColumn> FromColumn for
190    /// Option<T>` needs to short-circuit nullable columns of any type.
191    fn is_null(&self, c: &str) -> Result<bool, RowError> {
192        into_row_error(c, self.try_get::<_, Option<NullProbe>>(c)).map(|opt| opt.is_none())
193    }
194
195    fn get_bytes(&self, c: &str) -> Result<&[u8], RowError> {
196        into_row_error(c, self.try_get::<_, &[u8]>(c))
197    }
198    fn get_bytes_opt(&self, c: &str) -> Result<Option<&[u8]>, RowError> {
199        into_row_error(c, self.try_get::<_, Option<&[u8]>>(c))
200    }
201    fn get_datetime_utc(&self, c: &str) -> Result<chrono::DateTime<chrono::Utc>, RowError> {
202        into_row_error(c, self.try_get::<_, chrono::DateTime<chrono::Utc>>(c))
203    }
204    fn get_datetime_utc_opt(
205        &self,
206        c: &str,
207    ) -> Result<Option<chrono::DateTime<chrono::Utc>>, RowError> {
208        into_row_error(
209            c,
210            self.try_get::<_, Option<chrono::DateTime<chrono::Utc>>>(c),
211        )
212    }
213    fn get_naive_datetime(&self, c: &str) -> Result<chrono::NaiveDateTime, RowError> {
214        into_row_error(c, self.try_get::<_, chrono::NaiveDateTime>(c))
215    }
216    fn get_naive_datetime_opt(&self, c: &str) -> Result<Option<chrono::NaiveDateTime>, RowError> {
217        into_row_error(c, self.try_get::<_, Option<chrono::NaiveDateTime>>(c))
218    }
219    fn get_naive_date(&self, c: &str) -> Result<chrono::NaiveDate, RowError> {
220        into_row_error(c, self.try_get::<_, chrono::NaiveDate>(c))
221    }
222    fn get_naive_date_opt(&self, c: &str) -> Result<Option<chrono::NaiveDate>, RowError> {
223        into_row_error(c, self.try_get::<_, Option<chrono::NaiveDate>>(c))
224    }
225    fn get_naive_time(&self, c: &str) -> Result<chrono::NaiveTime, RowError> {
226        into_row_error(c, self.try_get::<_, chrono::NaiveTime>(c))
227    }
228    fn get_naive_time_opt(&self, c: &str) -> Result<Option<chrono::NaiveTime>, RowError> {
229        into_row_error(c, self.try_get::<_, Option<chrono::NaiveTime>>(c))
230    }
231    fn get_uuid(&self, c: &str) -> Result<uuid::Uuid, RowError> {
232        into_row_error(c, self.try_get::<_, uuid::Uuid>(c))
233    }
234    fn get_uuid_opt(&self, c: &str) -> Result<Option<uuid::Uuid>, RowError> {
235        into_row_error(c, self.try_get::<_, Option<uuid::Uuid>>(c))
236    }
237    fn get_json(&self, c: &str) -> Result<serde_json::Value, RowError> {
238        into_row_error(c, self.try_get::<_, serde_json::Value>(c))
239    }
240    fn get_json_opt(&self, c: &str) -> Result<Option<serde_json::Value>, RowError> {
241        into_row_error(c, self.try_get::<_, Option<serde_json::Value>>(c))
242    }
243    fn get_decimal(&self, c: &str) -> Result<rust_decimal::Decimal, RowError> {
244        into_row_error(c, self.try_get::<_, rust_decimal::Decimal>(c))
245    }
246    fn get_decimal_opt(&self, c: &str) -> Result<Option<rust_decimal::Decimal>, RowError> {
247        into_row_error(c, self.try_get::<_, Option<rust_decimal::Decimal>>(c))
248    }
249}