Skip to main content

prax_sqlite/
row_ref.rs

1//! Bridge between rusqlite rows and prax_query::row::RowRef.
2
3use std::collections::HashMap;
4
5use prax_query::row::{RowError, RowRef};
6use rusqlite::Row;
7use rusqlite::types::{Value, ValueRef};
8
9pub struct SqliteRowRef {
10    values: HashMap<String, Value>,
11}
12
13impl SqliteRowRef {
14    pub fn from_rusqlite(row: &Row<'_>) -> Result<Self, RowError> {
15        let stmt = row.as_ref();
16        let mut values = HashMap::with_capacity(stmt.column_count());
17        for i in 0..stmt.column_count() {
18            let name = stmt
19                .column_name(i)
20                .map_err(|e| RowError::TypeConversion {
21                    column: i.to_string(),
22                    message: e.to_string(),
23                })?
24                .to_string();
25            let v: Value = match row.get_ref(i).map_err(|e| RowError::TypeConversion {
26                column: name.clone(),
27                message: e.to_string(),
28            })? {
29                ValueRef::Null => Value::Null,
30                ValueRef::Integer(i) => Value::Integer(i),
31                ValueRef::Real(f) => Value::Real(f),
32                ValueRef::Text(b) => Value::Text(String::from_utf8_lossy(b).into_owned()),
33                ValueRef::Blob(b) => Value::Blob(b.to_vec()),
34            };
35            values.insert(name, v);
36        }
37        Ok(Self { values })
38    }
39
40    fn tc(column: &str, msg: impl Into<String>) -> RowError {
41        RowError::TypeConversion {
42            column: column.into(),
43            message: msg.into(),
44        }
45    }
46}
47
48impl RowRef for SqliteRowRef {
49    fn get_i32(&self, c: &str) -> Result<i32, RowError> {
50        match self
51            .values
52            .get(c)
53            .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
54        {
55            Value::Integer(i) => i32::try_from(*i).map_err(|_| Self::tc(c, "i64 overflow")),
56            Value::Null => Err(RowError::UnexpectedNull(c.into())),
57            _ => Err(Self::tc(c, "not an integer")),
58        }
59    }
60    fn get_i32_opt(&self, c: &str) -> Result<Option<i32>, RowError> {
61        match self.values.get(c) {
62            None => Err(RowError::ColumnNotFound(c.into())),
63            Some(Value::Null) => Ok(None),
64            Some(Value::Integer(i)) => i32::try_from(*i)
65                .map(Some)
66                .map_err(|_| Self::tc(c, "overflow")),
67            Some(_) => Err(Self::tc(c, "not an integer")),
68        }
69    }
70    fn get_i64(&self, c: &str) -> Result<i64, RowError> {
71        match self
72            .values
73            .get(c)
74            .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
75        {
76            Value::Integer(i) => Ok(*i),
77            Value::Null => Err(RowError::UnexpectedNull(c.into())),
78            _ => Err(Self::tc(c, "not an integer")),
79        }
80    }
81    fn get_i64_opt(&self, c: &str) -> Result<Option<i64>, RowError> {
82        match self.values.get(c) {
83            None => Err(RowError::ColumnNotFound(c.into())),
84            Some(Value::Null) => Ok(None),
85            Some(Value::Integer(i)) => Ok(Some(*i)),
86            Some(_) => Err(Self::tc(c, "not an integer")),
87        }
88    }
89    fn get_f64(&self, c: &str) -> Result<f64, RowError> {
90        match self
91            .values
92            .get(c)
93            .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
94        {
95            Value::Real(f) => Ok(*f),
96            Value::Integer(i) => Ok(*i as f64),
97            Value::Null => Err(RowError::UnexpectedNull(c.into())),
98            _ => Err(Self::tc(c, "not a number")),
99        }
100    }
101    fn get_f64_opt(&self, c: &str) -> Result<Option<f64>, RowError> {
102        match self.values.get(c) {
103            None => Err(RowError::ColumnNotFound(c.into())),
104            Some(Value::Null) => Ok(None),
105            Some(Value::Real(f)) => Ok(Some(*f)),
106            Some(Value::Integer(i)) => Ok(Some(*i as f64)),
107            Some(_) => Err(Self::tc(c, "not a number")),
108        }
109    }
110    fn get_bool(&self, c: &str) -> Result<bool, RowError> {
111        self.get_i64(c).map(|i| i != 0)
112    }
113    fn get_bool_opt(&self, c: &str) -> Result<Option<bool>, RowError> {
114        self.get_i64_opt(c).map(|o| o.map(|i| i != 0))
115    }
116    fn get_str(&self, c: &str) -> Result<&str, RowError> {
117        match self
118            .values
119            .get(c)
120            .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
121        {
122            Value::Text(s) => Ok(s.as_str()),
123            Value::Null => Err(RowError::UnexpectedNull(c.into())),
124            _ => Err(Self::tc(c, "not text")),
125        }
126    }
127    fn get_str_opt(&self, c: &str) -> Result<Option<&str>, RowError> {
128        match self.values.get(c) {
129            None => Err(RowError::ColumnNotFound(c.into())),
130            Some(Value::Null) => Ok(None),
131            Some(Value::Text(s)) => Ok(Some(s.as_str())),
132            Some(_) => Err(Self::tc(c, "not text")),
133        }
134    }
135    fn get_bytes(&self, c: &str) -> Result<&[u8], RowError> {
136        match self
137            .values
138            .get(c)
139            .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
140        {
141            Value::Blob(b) => Ok(b.as_slice()),
142            Value::Text(s) => Ok(s.as_bytes()),
143            Value::Null => Err(RowError::UnexpectedNull(c.into())),
144            _ => Err(Self::tc(c, "not blob")),
145        }
146    }
147    fn get_bytes_opt(&self, c: &str) -> Result<Option<&[u8]>, RowError> {
148        match self.values.get(c) {
149            None => Err(RowError::ColumnNotFound(c.into())),
150            Some(Value::Null) => Ok(None),
151            Some(Value::Blob(b)) => Ok(Some(b.as_slice())),
152            Some(Value::Text(s)) => Ok(Some(s.as_bytes())),
153            Some(_) => Err(Self::tc(c, "not blob")),
154        }
155    }
156    fn get_datetime_utc(&self, c: &str) -> Result<chrono::DateTime<chrono::Utc>, RowError> {
157        let s = self.get_str(c)?;
158        chrono::DateTime::parse_from_rfc3339(s)
159            .map(|d| d.with_timezone(&chrono::Utc))
160            .map_err(|e| Self::tc(c, e.to_string()))
161    }
162    fn get_datetime_utc_opt(
163        &self,
164        c: &str,
165    ) -> Result<Option<chrono::DateTime<chrono::Utc>>, RowError> {
166        match self.get_str_opt(c)? {
167            None => Ok(None),
168            Some(s) => chrono::DateTime::parse_from_rfc3339(s)
169                .map(|d| Some(d.with_timezone(&chrono::Utc)))
170                .map_err(|e| Self::tc(c, e.to_string())),
171        }
172    }
173    fn get_naive_datetime(&self, c: &str) -> Result<chrono::NaiveDateTime, RowError> {
174        let s = self.get_str(c)?;
175        // Accept RFC3339 (strip tz to naive) OR naive "YYYY-MM-DD HH:MM:SS[.fff]" / "YYYY-MM-DDTHH:MM:SS[.fff]" formats.
176        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
177            return Ok(dt.naive_utc());
178        }
179        for fmt in [
180            "%Y-%m-%dT%H:%M:%S%.f",
181            "%Y-%m-%dT%H:%M:%S",
182            "%Y-%m-%d %H:%M:%S%.f",
183            "%Y-%m-%d %H:%M:%S",
184        ] {
185            if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, fmt) {
186                return Ok(dt);
187            }
188        }
189        Err(Self::tc(
190            c,
191            format!("unrecognized naive datetime format: {s}"),
192        ))
193    }
194    fn get_naive_datetime_opt(&self, c: &str) -> Result<Option<chrono::NaiveDateTime>, RowError> {
195        match self.get_str_opt(c)? {
196            None => Ok(None),
197            Some(_) => self.get_naive_datetime(c).map(Some),
198        }
199    }
200    fn get_naive_date(&self, c: &str) -> Result<chrono::NaiveDate, RowError> {
201        let s = self.get_str(c)?;
202        chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| Self::tc(c, e.to_string()))
203    }
204    fn get_naive_date_opt(&self, c: &str) -> Result<Option<chrono::NaiveDate>, RowError> {
205        match self.get_str_opt(c)? {
206            None => Ok(None),
207            Some(s) => chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
208                .map(Some)
209                .map_err(|e| Self::tc(c, e.to_string())),
210        }
211    }
212    fn get_naive_time(&self, c: &str) -> Result<chrono::NaiveTime, RowError> {
213        let s = self.get_str(c)?;
214        for fmt in ["%H:%M:%S%.f", "%H:%M:%S"] {
215            if let Ok(t) = chrono::NaiveTime::parse_from_str(s, fmt) {
216                return Ok(t);
217            }
218        }
219        Err(Self::tc(c, format!("unrecognized naive time format: {s}")))
220    }
221    fn get_naive_time_opt(&self, c: &str) -> Result<Option<chrono::NaiveTime>, RowError> {
222        match self.get_str_opt(c)? {
223            None => Ok(None),
224            Some(_) => self.get_naive_time(c).map(Some),
225        }
226    }
227    fn get_uuid(&self, c: &str) -> Result<uuid::Uuid, RowError> {
228        uuid::Uuid::parse_str(self.get_str(c)?).map_err(|e| Self::tc(c, e.to_string()))
229    }
230    fn get_uuid_opt(&self, c: &str) -> Result<Option<uuid::Uuid>, RowError> {
231        match self.get_str_opt(c)? {
232            None => Ok(None),
233            Some(s) => uuid::Uuid::parse_str(s)
234                .map(Some)
235                .map_err(|e| Self::tc(c, e.to_string())),
236        }
237    }
238    fn get_json(&self, c: &str) -> Result<serde_json::Value, RowError> {
239        serde_json::from_str(self.get_str(c)?).map_err(|e| Self::tc(c, e.to_string()))
240    }
241    fn get_json_opt(&self, c: &str) -> Result<Option<serde_json::Value>, RowError> {
242        match self.get_str_opt(c)? {
243            None => Ok(None),
244            Some(s) => serde_json::from_str(s)
245                .map(Some)
246                .map_err(|e| Self::tc(c, e.to_string())),
247        }
248    }
249    fn get_decimal(&self, c: &str) -> Result<rust_decimal::Decimal, RowError> {
250        self.get_str(c)?
251            .parse::<rust_decimal::Decimal>()
252            .map_err(|e| Self::tc(c, e.to_string()))
253    }
254    fn get_decimal_opt(&self, c: &str) -> Result<Option<rust_decimal::Decimal>, RowError> {
255        match self.get_str_opt(c)? {
256            None => Ok(None),
257            Some(s) => s
258                .parse::<rust_decimal::Decimal>()
259                .map(Some)
260                .map_err(|e| Self::tc(c, e.to_string())),
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use rusqlite::Connection;
269
270    #[test]
271    fn materializes_row_from_rusqlite() {
272        let conn = Connection::open_in_memory().unwrap();
273        let mut stmt = conn.prepare("SELECT 42 AS n, 'hello' AS s").unwrap();
274        let mut rows = stmt.query([]).unwrap();
275        let row = rows.next().unwrap().unwrap();
276        let r = SqliteRowRef::from_rusqlite(row).unwrap();
277        assert_eq!(r.get_i32("n").unwrap(), 42);
278        assert_eq!(r.get_str("s").unwrap(), "hello");
279    }
280
281    #[test]
282    fn materializes_naive_temporal_values() {
283        use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
284        let conn = Connection::open_in_memory().unwrap();
285        let mut stmt = conn
286            .prepare("SELECT '2026-04-27T15:30:45' AS dt, '2026-04-27' AS d, '15:30:45' AS t")
287            .unwrap();
288        let mut rows = stmt.query([]).unwrap();
289        let row = rows.next().unwrap().unwrap();
290        let r = SqliteRowRef::from_rusqlite(row).unwrap();
291        assert_eq!(
292            r.get_naive_datetime("dt").unwrap(),
293            NaiveDateTime::parse_from_str("2026-04-27 15:30:45", "%Y-%m-%d %H:%M:%S").unwrap()
294        );
295        assert_eq!(
296            r.get_naive_date("d").unwrap(),
297            NaiveDate::from_ymd_opt(2026, 4, 27).unwrap()
298        );
299        assert_eq!(
300            r.get_naive_time("t").unwrap(),
301            NaiveTime::from_hms_opt(15, 30, 45).unwrap()
302        );
303    }
304
305    #[test]
306    fn opt_methods_distinguish_missing_column_from_null() {
307        let conn = Connection::open_in_memory().unwrap();
308        let mut stmt = conn
309            .prepare("SELECT 42 AS present, NULL AS nulled")
310            .unwrap();
311        let mut rows = stmt.query([]).unwrap();
312        let row = rows.next().unwrap().unwrap();
313        let r = SqliteRowRef::from_rusqlite(row).unwrap();
314
315        // Present column with a value → Ok(Some(_)).
316        assert_eq!(r.get_i32_opt("present").unwrap(), Some(42));
317
318        // Present column whose value is NULL → Ok(None).
319        assert_eq!(r.get_i32_opt("nulled").unwrap(), None);
320
321        // Absent column (typo / not in the SELECT list) → Err(ColumnNotFound).
322        let err = r.get_i32_opt("missing").unwrap_err();
323        assert!(
324            matches!(err, RowError::ColumnNotFound(ref col) if col == "missing"),
325            "expected ColumnNotFound for absent column, got {err:?}",
326        );
327    }
328}