Skip to main content

prax_sqlx/
row.rs

1//! Row types and conversion utilities for SQLx.
2
3use crate::config::DatabaseBackend;
4use crate::error::{SqlxError, SqlxResult};
5use serde_json::Value as JsonValue;
6use sqlx::Row;
7
8/// A generic row wrapper that works across databases.
9pub enum SqlxRow {
10    /// PostgreSQL row
11    #[cfg(feature = "postgres")]
12    Postgres(sqlx::postgres::PgRow),
13    /// MySQL row
14    #[cfg(feature = "mysql")]
15    MySql(sqlx::mysql::MySqlRow),
16    /// SQLite row
17    #[cfg(feature = "sqlite")]
18    Sqlite(sqlx::sqlite::SqliteRow),
19}
20
21impl SqlxRow {
22    /// Get the database backend type.
23    pub fn backend(&self) -> DatabaseBackend {
24        match self {
25            #[cfg(feature = "postgres")]
26            Self::Postgres(_) => DatabaseBackend::Postgres,
27            #[cfg(feature = "mysql")]
28            Self::MySql(_) => DatabaseBackend::MySql,
29            #[cfg(feature = "sqlite")]
30            Self::Sqlite(_) => DatabaseBackend::Sqlite,
31        }
32    }
33
34    /// Get a column value by index.
35    pub fn get<T>(&self, index: usize) -> SqlxResult<T>
36    where
37        T: SqlxDecode,
38    {
39        T::decode_from_row(self, index)
40    }
41
42    /// Get a column value by name.
43    pub fn get_by_name<T>(&self, name: &str) -> SqlxResult<T>
44    where
45        T: SqlxDecodeNamed,
46    {
47        T::decode_by_name(self, name)
48    }
49
50    /// Try to get a nullable column value by index.
51    pub fn try_get<T>(&self, index: usize) -> SqlxResult<Option<T>>
52    where
53        T: SqlxDecode,
54    {
55        match T::decode_from_row(self, index) {
56            Ok(v) => Ok(Some(v)),
57            Err(SqlxError::Sqlx(sqlx::Error::ColumnNotFound(_))) => Ok(None),
58            // Decoding a SQL NULL into a non-`Option` type surfaces as a
59            // `ColumnDecode` wrapping `UnexpectedNullError`; treat it as "no
60            // value" rather than a hard error.
61            Err(SqlxError::Sqlx(sqlx::Error::ColumnDecode { ref source, .. }))
62                if source.is::<sqlx::error::UnexpectedNullError>() =>
63            {
64                Ok(None)
65            }
66            Err(e) => Err(e),
67        }
68    }
69
70    /// Get the number of columns.
71    pub fn len(&self) -> usize {
72        match self {
73            #[cfg(feature = "postgres")]
74            Self::Postgres(row) => row.len(),
75            #[cfg(feature = "mysql")]
76            Self::MySql(row) => row.len(),
77            #[cfg(feature = "sqlite")]
78            Self::Sqlite(row) => row.len(),
79        }
80    }
81
82    /// Check if the row is empty.
83    pub fn is_empty(&self) -> bool {
84        self.len() == 0
85    }
86
87    /// Convert the row to a JSON value.
88    pub fn to_json(&self) -> SqlxResult<JsonValue> {
89        match self {
90            #[cfg(feature = "postgres")]
91            Self::Postgres(row) => row_to_json_pg(row),
92            #[cfg(feature = "mysql")]
93            Self::MySql(row) => row_to_json_mysql(row),
94            #[cfg(feature = "sqlite")]
95            Self::Sqlite(row) => row_to_json_sqlite(row),
96        }
97    }
98}
99
100/// Trait for decoding values from a row by index.
101pub trait SqlxDecode: Sized {
102    /// Decode a value from a row at the given index.
103    fn decode_from_row(row: &SqlxRow, index: usize) -> SqlxResult<Self>;
104}
105
106/// Trait for decoding values from a row by column name.
107pub trait SqlxDecodeNamed: Sized {
108    /// Decode a value from a row by column name.
109    fn decode_by_name(row: &SqlxRow, name: &str) -> SqlxResult<Self>;
110}
111
112// Implement SqlxDecode for common types
113macro_rules! impl_decode {
114    ($ty:ty) => {
115        impl SqlxDecode for $ty {
116            fn decode_from_row(row: &SqlxRow, index: usize) -> SqlxResult<Self> {
117                match row {
118                    #[cfg(feature = "postgres")]
119                    SqlxRow::Postgres(r) => r.try_get(index).map_err(SqlxError::from),
120                    #[cfg(feature = "mysql")]
121                    SqlxRow::MySql(r) => r.try_get(index).map_err(SqlxError::from),
122                    #[cfg(feature = "sqlite")]
123                    SqlxRow::Sqlite(r) => r.try_get(index).map_err(SqlxError::from),
124                }
125            }
126        }
127
128        impl SqlxDecodeNamed for $ty {
129            fn decode_by_name(row: &SqlxRow, name: &str) -> SqlxResult<Self> {
130                match row {
131                    #[cfg(feature = "postgres")]
132                    SqlxRow::Postgres(r) => r.try_get(name).map_err(SqlxError::from),
133                    #[cfg(feature = "mysql")]
134                    SqlxRow::MySql(r) => r.try_get(name).map_err(SqlxError::from),
135                    #[cfg(feature = "sqlite")]
136                    SqlxRow::Sqlite(r) => r.try_get(name).map_err(SqlxError::from),
137                }
138            }
139        }
140    };
141}
142
143impl_decode!(i32);
144impl_decode!(i64);
145impl_decode!(f32);
146impl_decode!(f64);
147impl_decode!(bool);
148impl_decode!(String);
149impl_decode!(Vec<u8>);
150
151// Helper functions to convert rows to JSON.
152//
153// Each cell is probed against a series of candidate types, mirroring the
154// probe order used by `row_ref.rs`. sqlx rejects type-incompatible decodes
155// before looking at the value, so probes simply fall through until one
156// matches. A probe returning `Ok(None)` means the cell is a genuine SQL
157// NULL, which is the only case that becomes a JSON null (besides values of
158// types we do not recognise).
159
160/// Map an `f64` to a JSON number; NaN and infinities have no JSON
161/// counterpart and become null.
162fn f64_to_json(f: f64) -> JsonValue {
163    serde_json::Number::from_f64(f).map_or(JsonValue::Null, JsonValue::Number)
164}
165
166/// Map a decimal to its JSON representation.
167#[cfg(any(feature = "postgres", feature = "mysql"))]
168fn decimal_to_json(d: rust_decimal::Decimal) -> JsonValue {
169    serde_json::to_value(d).unwrap_or(JsonValue::Null)
170}
171
172/// Encode raw bytes as a base64 string; binary data has no JSON counterpart.
173fn bytes_to_json(bytes: Vec<u8>) -> JsonValue {
174    use prax_query::base64::Engine as _;
175    JsonValue::String(prax_query::base64::engine::general_purpose::STANDARD.encode(bytes))
176}
177
178#[cfg(feature = "postgres")]
179fn row_to_json_pg(row: &sqlx::postgres::PgRow) -> SqlxResult<JsonValue> {
180    use sqlx::Column;
181    let mut obj = serde_json::Map::new();
182    for (i, col) in row.columns().iter().enumerate() {
183        let name = col.name().to_string();
184        obj.insert(name, pg_cell_to_json(row, i));
185    }
186    Ok(JsonValue::Object(obj))
187}
188
189#[cfg(feature = "postgres")]
190fn pg_cell_to_json(row: &sqlx::postgres::PgRow, i: usize) -> JsonValue {
191    if let Ok(Some(s)) = row.try_get::<Option<String>, _>(i) {
192        return JsonValue::String(s);
193    }
194    if let Ok(Some(b)) = row.try_get::<Option<bool>, _>(i) {
195        return JsonValue::Bool(b);
196    }
197    if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(i) {
198        return JsonValue::Number(n.into());
199    }
200    if let Ok(Some(n)) = row.try_get::<Option<i32>, _>(i) {
201        return JsonValue::Number(n.into());
202    }
203    if let Ok(Some(n)) = row.try_get::<Option<i16>, _>(i) {
204        return JsonValue::Number(n.into());
205    }
206    if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(i) {
207        return f64_to_json(f);
208    }
209    if let Ok(Some(f)) = row.try_get::<Option<f32>, _>(i) {
210        return f64_to_json(f64::from(f));
211    }
212    if let Ok(Some(d)) = row.try_get::<Option<rust_decimal::Decimal>, _>(i) {
213        return decimal_to_json(d);
214    }
215    if let Ok(Some(ts)) = row.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(i) {
216        return JsonValue::String(ts.to_rfc3339());
217    }
218    if let Ok(Some(ts)) = row.try_get::<Option<chrono::NaiveDateTime>, _>(i) {
219        return JsonValue::String(ts.to_string());
220    }
221    if let Ok(Some(d)) = row.try_get::<Option<chrono::NaiveDate>, _>(i) {
222        return JsonValue::String(d.to_string());
223    }
224    if let Ok(Some(t)) = row.try_get::<Option<chrono::NaiveTime>, _>(i) {
225        return JsonValue::String(t.to_string());
226    }
227    if let Ok(Some(u)) = row.try_get::<Option<uuid::Uuid>, _>(i) {
228        return JsonValue::String(u.to_string());
229    }
230    if let Ok(Some(j)) = row.try_get::<Option<JsonValue>, _>(i) {
231        return j;
232    }
233    if let Ok(Some(b)) = row.try_get::<Option<Vec<u8>>, _>(i) {
234        return bytes_to_json(b);
235    }
236    JsonValue::Null
237}
238
239#[cfg(feature = "mysql")]
240fn row_to_json_mysql(row: &sqlx::mysql::MySqlRow) -> SqlxResult<JsonValue> {
241    use sqlx::Column;
242    let mut obj = serde_json::Map::new();
243    for (i, col) in row.columns().iter().enumerate() {
244        let name = col.name().to_string();
245        obj.insert(name, mysql_cell_to_json(row, i));
246    }
247    Ok(JsonValue::Object(obj))
248}
249
250#[cfg(feature = "mysql")]
251fn mysql_cell_to_json(row: &sqlx::mysql::MySqlRow, i: usize) -> JsonValue {
252    if let Ok(Some(s)) = row.try_get::<Option<String>, _>(i) {
253        return JsonValue::String(s);
254    }
255    if let Ok(Some(b)) = row.try_get::<Option<bool>, _>(i) {
256        return JsonValue::Bool(b);
257    }
258    if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(i) {
259        return JsonValue::Number(n.into());
260    }
261    if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(i) {
262        return f64_to_json(f);
263    }
264    if let Ok(Some(d)) = row.try_get::<Option<rust_decimal::Decimal>, _>(i) {
265        return decimal_to_json(d);
266    }
267    if let Ok(Some(ts)) = row.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(i) {
268        return JsonValue::String(ts.to_rfc3339());
269    }
270    if let Ok(Some(ts)) = row.try_get::<Option<chrono::NaiveDateTime>, _>(i) {
271        return JsonValue::String(ts.to_string());
272    }
273    if let Ok(Some(d)) = row.try_get::<Option<chrono::NaiveDate>, _>(i) {
274        return JsonValue::String(d.to_string());
275    }
276    if let Ok(Some(t)) = row.try_get::<Option<chrono::NaiveTime>, _>(i) {
277        return JsonValue::String(t.to_string());
278    }
279    if let Ok(Some(j)) = row.try_get::<Option<JsonValue>, _>(i) {
280        return j;
281    }
282    if let Ok(Some(b)) = row.try_get::<Option<Vec<u8>>, _>(i) {
283        return bytes_to_json(b);
284    }
285    JsonValue::Null
286}
287
288#[cfg(feature = "sqlite")]
289fn row_to_json_sqlite(row: &sqlx::sqlite::SqliteRow) -> SqlxResult<JsonValue> {
290    use sqlx::Column;
291    let mut obj = serde_json::Map::new();
292    for (i, col) in row.columns().iter().enumerate() {
293        let name = col.name().to_string();
294        obj.insert(name, sqlite_cell_to_json(row, i));
295    }
296    Ok(JsonValue::Object(obj))
297}
298
299#[cfg(feature = "sqlite")]
300fn sqlite_cell_to_json(row: &sqlx::sqlite::SqliteRow, i: usize) -> JsonValue {
301    // SQLite is dynamically typed, so the value's storage class drives the
302    // decode; there are no separate JSON or datetime storage classes (those
303    // come back as TEXT and are caught by the String probe).
304    if let Ok(Some(s)) = row.try_get::<Option<String>, _>(i) {
305        return JsonValue::String(s);
306    }
307    if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(i) {
308        return JsonValue::Number(n.into());
309    }
310    if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(i) {
311        return f64_to_json(f);
312    }
313    if let Ok(Some(b)) = row.try_get::<Option<bool>, _>(i) {
314        return JsonValue::Bool(b);
315    }
316    if let Ok(Some(b)) = row.try_get::<Option<Vec<u8>>, _>(i) {
317        return bytes_to_json(b);
318    }
319    JsonValue::Null
320}
321
322/// Trait for converting SQL rows to model types.
323pub trait FromSqlxRow: Sized {
324    /// Convert a row to a model instance.
325    fn from_row(row: SqlxRow) -> SqlxResult<Self>;
326}