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