Skip to main content

prax_sqlite/
row_ref.rs

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