1use 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 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 assert_eq!(r.get_i32_opt("present").unwrap(), Some(42));
317
318 assert_eq!(r.get_i32_opt("nulled").unwrap(), None);
320
321 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}