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            Err(e) => Err(e),
59        }
60    }
61
62    /// Get the number of columns.
63    pub fn len(&self) -> usize {
64        match self {
65            #[cfg(feature = "postgres")]
66            Self::Postgres(row) => row.len(),
67            #[cfg(feature = "mysql")]
68            Self::MySql(row) => row.len(),
69            #[cfg(feature = "sqlite")]
70            Self::Sqlite(row) => row.len(),
71        }
72    }
73
74    /// Check if the row is empty.
75    pub fn is_empty(&self) -> bool {
76        self.len() == 0
77    }
78
79    /// Convert the row to a JSON value.
80    pub fn to_json(&self) -> SqlxResult<JsonValue> {
81        match self {
82            #[cfg(feature = "postgres")]
83            Self::Postgres(row) => row_to_json_pg(row),
84            #[cfg(feature = "mysql")]
85            Self::MySql(row) => row_to_json_mysql(row),
86            #[cfg(feature = "sqlite")]
87            Self::Sqlite(row) => row_to_json_sqlite(row),
88        }
89    }
90}
91
92/// Trait for decoding values from a row by index.
93pub trait SqlxDecode: Sized {
94    /// Decode a value from a row at the given index.
95    fn decode_from_row(row: &SqlxRow, index: usize) -> SqlxResult<Self>;
96}
97
98/// Trait for decoding values from a row by column name.
99pub trait SqlxDecodeNamed: Sized {
100    /// Decode a value from a row by column name.
101    fn decode_by_name(row: &SqlxRow, name: &str) -> SqlxResult<Self>;
102}
103
104// Implement SqlxDecode for common types
105macro_rules! impl_decode {
106    ($ty:ty) => {
107        impl SqlxDecode for $ty {
108            fn decode_from_row(row: &SqlxRow, index: usize) -> SqlxResult<Self> {
109                match row {
110                    #[cfg(feature = "postgres")]
111                    SqlxRow::Postgres(r) => r.try_get(index).map_err(SqlxError::from),
112                    #[cfg(feature = "mysql")]
113                    SqlxRow::MySql(r) => r.try_get(index).map_err(SqlxError::from),
114                    #[cfg(feature = "sqlite")]
115                    SqlxRow::Sqlite(r) => r.try_get(index).map_err(SqlxError::from),
116                }
117            }
118        }
119
120        impl SqlxDecodeNamed for $ty {
121            fn decode_by_name(row: &SqlxRow, name: &str) -> SqlxResult<Self> {
122                match row {
123                    #[cfg(feature = "postgres")]
124                    SqlxRow::Postgres(r) => r.try_get(name).map_err(SqlxError::from),
125                    #[cfg(feature = "mysql")]
126                    SqlxRow::MySql(r) => r.try_get(name).map_err(SqlxError::from),
127                    #[cfg(feature = "sqlite")]
128                    SqlxRow::Sqlite(r) => r.try_get(name).map_err(SqlxError::from),
129                }
130            }
131        }
132    };
133}
134
135impl_decode!(i32);
136impl_decode!(i64);
137impl_decode!(f32);
138impl_decode!(f64);
139impl_decode!(bool);
140impl_decode!(String);
141impl_decode!(Vec<u8>);
142
143// Helper functions to convert rows to JSON
144#[cfg(feature = "postgres")]
145fn row_to_json_pg(row: &sqlx::postgres::PgRow) -> SqlxResult<JsonValue> {
146    use sqlx::Column;
147    let mut obj = serde_json::Map::new();
148    for (i, col) in row.columns().iter().enumerate() {
149        let name = col.name().to_string();
150        let value: Option<JsonValue> = row.try_get(i).ok();
151        obj.insert(name, value.unwrap_or(JsonValue::Null));
152    }
153    Ok(JsonValue::Object(obj))
154}
155
156#[cfg(feature = "mysql")]
157fn row_to_json_mysql(row: &sqlx::mysql::MySqlRow) -> SqlxResult<JsonValue> {
158    use sqlx::Column;
159    let mut obj = serde_json::Map::new();
160    for (i, col) in row.columns().iter().enumerate() {
161        let name = col.name().to_string();
162        let value: Option<JsonValue> = row.try_get(i).ok();
163        obj.insert(name, value.unwrap_or(JsonValue::Null));
164    }
165    Ok(JsonValue::Object(obj))
166}
167
168#[cfg(feature = "sqlite")]
169fn row_to_json_sqlite(row: &sqlx::sqlite::SqliteRow) -> SqlxResult<JsonValue> {
170    use sqlx::Column;
171    let mut obj = serde_json::Map::new();
172    for (i, col) in row.columns().iter().enumerate() {
173        let name = col.name().to_string();
174        // SQLite doesn't directly support JSON, so we try common types
175        if let Ok(v) = row.try_get::<String, _>(i) {
176            obj.insert(name, JsonValue::String(v));
177        } else if let Ok(v) = row.try_get::<i64, _>(i) {
178            obj.insert(name, JsonValue::Number(v.into()));
179        } else if let Ok(v) = row.try_get::<f64, _>(i) {
180            if let Some(n) = serde_json::Number::from_f64(v) {
181                obj.insert(name, JsonValue::Number(n));
182            } else {
183                obj.insert(name, JsonValue::Null);
184            }
185        } else if let Ok(v) = row.try_get::<bool, _>(i) {
186            obj.insert(name, JsonValue::Bool(v));
187        } else {
188            obj.insert(name, JsonValue::Null);
189        }
190    }
191    Ok(JsonValue::Object(obj))
192}
193
194/// Trait for converting SQL rows to model types.
195pub trait FromSqlxRow: Sized {
196    /// Convert a row to a model instance.
197    fn from_row(row: SqlxRow) -> SqlxResult<Self>;
198}