Skip to main content

dactyl_db/
rows.rs

1//! Row projection returned by [`crate::query`] and [`crate::transaction`].
2//!
3//! # Named-column projection contract (dactyl #25)
4//!
5//! Each [`Row`] owns its column names and cell values as JSON. Named lookup is
6//! stable across SQLite and Neon because both adapters normalize cells into
7//! this shape before returning.
8//!
9//! | Concern | Semantics |
10//! |---|---|
11//! | Integer | JSON number that fits `i64` (`get_int` / `get::<i64>`) |
12//! | Real | JSON number (`get_real` / `get::<f64>`; integers are accepted as reals) |
13//! | Boolean | JSON `true`/`false`, or integer `0`/`1` via `get_bool` (SQLite stores bools as integers) |
14//! | Text | JSON string (`get_str` / `get_str_ref` / `get::<String>`) |
15//! | JSON / text | Low-level cell via `get_json` / `get_json_ref`; text payloads stay strings until the caller parses them |
16//! | SQL NULL | JSON `null`. Non-`Option` typed getters return [`DactylError::Conversion`]; `get::<Option<T>>` yields `None`. `is_null` / `get_json` surface null without converting. |
17//! | Missing column | [`DactylError::ColumnNotFound`] |
18//! | Duplicate aliases | Left-to-right **first match**. `select a as x, b as x` resolves `get("x")` to the first `x`. Positional indexes still reach later duplicates. |
19//! | Conversion failure | [`DactylError::Conversion`] with the column key and a reason string |
20//! | Ownership | `get`, `get_*` (except `*_ref`) return **owned** values independent of the row. `get_str_ref` / `get_json_ref` borrow from `&self` for the row lifetime. A `Row` outlives the adapter connection. |
21
22use crate::error::DactylError;
23use serde::{Deserialize, Serialize};
24
25/// A collection of result rows.
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
27pub struct Rows(pub Vec<Row>);
28
29impl Rows {
30    /// Borrow the rows as a slice.
31    pub fn as_slice(&self) -> &[Row] {
32        &self.0
33    }
34
35    /// Iterate over rows.
36    pub fn iter(&self) -> std::slice::Iter<'_, Row> {
37        self.0.iter()
38    }
39
40    /// Number of rows.
41    pub fn len(&self) -> usize {
42        self.0.len()
43    }
44
45    /// Whether the result is empty.
46    pub fn is_empty(&self) -> bool {
47        self.0.is_empty()
48    }
49}
50
51impl IntoIterator for Rows {
52    type Item = Row;
53    type IntoIter = std::vec::IntoIter<Row>;
54
55    fn into_iter(self) -> Self::IntoIter {
56        self.0.into_iter()
57    }
58}
59
60/// One result row. Carries the column names plus the per-cell JSON values.
61///
62/// The row **owns** both vectors. After `query` / `transaction` returns, the
63/// short-lived adapter is dropped; callers may keep `Row` values indefinitely.
64/// Borrowed accessors (`get_str_ref`, `get_json_ref`) are tied to `&self` only.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Row {
67    /// Column names, in the order the adapter emitted them.
68    ///
69    /// Duplicate names are allowed (SQL aliases). Named getters resolve the
70    /// **first** occurrence left-to-right; use a positional [`usize`] index to
71    /// reach a later duplicate.
72    pub columns: Vec<String>,
73    /// Per-cell values, parallel to `columns`.
74    pub values: Vec<serde_json::Value>,
75}
76
77/// A unified database parameter value.
78#[derive(Debug, Clone, PartialEq)]
79pub enum Parameter {
80    Null,
81    Bool(bool),
82    Integer(i64),
83    Real(f64),
84    Text(String),
85}
86
87impl serde::Serialize for Parameter {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: serde::Serializer,
91    {
92        match self {
93            Parameter::Null => serializer.serialize_unit(),
94            Parameter::Bool(b) => serializer.serialize_bool(*b),
95            Parameter::Integer(i) => serializer.serialize_i64(*i),
96            Parameter::Real(f) => serializer.serialize_f64(*f),
97            Parameter::Text(s) => serializer.serialize_str(s),
98        }
99    }
100}
101
102impl<'de> serde::Deserialize<'de> for Parameter {
103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104    where
105        D: serde::Deserializer<'de>,
106    {
107        struct ParameterVisitor;
108        impl<'de> serde::de::Visitor<'de> for ParameterVisitor {
109            type Value = Parameter;
110            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
111                formatter.write_str("a database parameter value")
112            }
113            fn visit_none<E>(self) -> Result<Self::Value, E>
114            where
115                E: serde::de::Error,
116            {
117                Ok(Parameter::Null)
118            }
119            fn visit_unit<E>(self) -> Result<Self::Value, E>
120            where
121                E: serde::de::Error,
122            {
123                Ok(Parameter::Null)
124            }
125            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
126            where
127                E: serde::de::Error,
128            {
129                Ok(Parameter::Bool(v))
130            }
131            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
132            where
133                E: serde::de::Error,
134            {
135                Ok(Parameter::Integer(v))
136            }
137            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
138            where
139                E: serde::de::Error,
140            {
141                Ok(Parameter::Integer(v as i64))
142            }
143            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
144            where
145                E: serde::de::Error,
146            {
147                Ok(Parameter::Real(v))
148            }
149            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
150            where
151                E: serde::de::Error,
152            {
153                Ok(Parameter::Text(v.to_string()))
154            }
155            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
156            where
157                E: serde::de::Error,
158            {
159                Ok(Parameter::Text(v))
160            }
161        }
162        deserializer.deserialize_any(ParameterVisitor)
163    }
164}
165
166impl From<i64> for Parameter {
167    fn from(v: i64) -> Self {
168        Parameter::Integer(v)
169    }
170}
171impl From<i32> for Parameter {
172    fn from(v: i32) -> Self {
173        Parameter::Integer(v as i64)
174    }
175}
176impl From<u32> for Parameter {
177    fn from(v: u32) -> Self {
178        Parameter::Integer(v as i64)
179    }
180}
181impl From<usize> for Parameter {
182    fn from(v: usize) -> Self {
183        Parameter::Integer(v as i64)
184    }
185}
186impl From<bool> for Parameter {
187    fn from(v: bool) -> Self {
188        Parameter::Bool(v)
189    }
190}
191impl From<f64> for Parameter {
192    fn from(v: f64) -> Self {
193        Parameter::Real(v)
194    }
195}
196impl From<String> for Parameter {
197    fn from(v: String) -> Self {
198        Parameter::Text(v)
199    }
200}
201impl From<&str> for Parameter {
202    fn from(v: &str) -> Self {
203        Parameter::Text(v.to_string())
204    }
205}
206
207impl From<Option<String>> for Parameter {
208    fn from(v: Option<String>) -> Self {
209        match v {
210            Some(s) => Parameter::Text(s),
211            None => Parameter::Null,
212        }
213    }
214}
215
216impl From<Option<&str>> for Parameter {
217    fn from(v: Option<&str>) -> Self {
218        match v {
219            Some(s) => Parameter::Text(s.to_string()),
220            None => Parameter::Null,
221        }
222    }
223}
224
225impl From<Option<i64>> for Parameter {
226    fn from(v: Option<i64>) -> Self {
227        match v {
228            Some(i) => Parameter::Integer(i),
229            None => Parameter::Null,
230        }
231    }
232}
233
234impl From<Option<bool>> for Parameter {
235    fn from(v: Option<bool>) -> Self {
236        match v {
237            Some(b) => Parameter::Bool(b),
238            None => Parameter::Null,
239        }
240    }
241}
242
243/// Helper trait for row indexing by position or column name.
244///
245/// Named indexes (`&str`, `String`) resolve **left-to-right first match** when
246/// a result set contains duplicate column aliases.
247pub trait RowIndex: std::fmt::Debug {
248    /// Return the resolved index in `row`, or `None` if the name is absent.
249    fn idx(&self, row: &Row) -> Option<usize>;
250}
251
252impl RowIndex for usize {
253    fn idx(&self, _row: &Row) -> Option<usize> {
254        Some(*self)
255    }
256}
257
258impl RowIndex for &str {
259    fn idx(&self, row: &Row) -> Option<usize> {
260        // First match wins for duplicate aliases (documented contract).
261        row.columns.iter().position(|c| c == self)
262    }
263}
264
265impl RowIndex for String {
266    fn idx(&self, row: &Row) -> Option<usize> {
267        row.columns.iter().position(|c| c == self)
268    }
269}
270
271impl Row {
272    /// Strict typed extraction via `serde` into an **owned** `T`.
273    ///
274    /// - Missing column → [`DactylError::ColumnNotFound`].
275    /// - SQL NULL into non-`Option` `T` → [`DactylError::Conversion`].
276    /// - SQL NULL into `Option<T>` → `Ok(None)`.
277    /// - Type mismatch → [`DactylError::Conversion`].
278    /// - Duplicate aliases → first matching column (left-to-right).
279    ///
280    /// Prefer [`Self::get_bool`] for portable bools: SQLite stores booleans as
281    /// integers `0`/`1`, which strict `get::<bool>` rejects. For lenient
282    /// portable shapes use [`Self::get_bool`] / [`Self::get_int`] /
283    /// [`Self::get_real`] / [`Self::get_str`] / [`Self::get_json`].
284    pub fn get<I: RowIndex, T: serde::de::DeserializeOwned>(
285        &self,
286        index: I,
287    ) -> Result<T, DactylError> {
288        let i = self.idx(&index)?;
289        let val = &self.values[i];
290        serde_json::from_value(val.clone()).map_err(|e| {
291            if val.is_null() {
292                DactylError::Conversion(format!(
293                    "column {:?} is NULL; use Option<T> with get for nullable columns",
294                    index
295                ))
296            } else {
297                DactylError::Conversion(format!(
298                    "failed to convert column {:?} to target type: {}",
299                    index, e
300                ))
301            }
302        })
303    }
304
305    /// Alias for [`Self::get`]. Named for callers who prefer a `try_*` style.
306    pub fn try_get<I: RowIndex, T: serde::de::DeserializeOwned>(
307        &self,
308        index: I,
309    ) -> Result<T, DactylError> {
310        self.get(index)
311    }
312
313    /// Whether the cell is SQL NULL (`serde_json::Value::Null`).
314    ///
315    /// Missing column → [`DactylError::ColumnNotFound`].
316    pub fn is_null<I: RowIndex>(&self, index: I) -> Result<bool, DactylError> {
317        let i = self.idx(&index)?;
318        Ok(self.values[i].is_null())
319    }
320
321    /// Lenient **owned** `bool`: accepts JSON `true`/`false` or integer `0`/`1`.
322    ///
323    /// NULL → [`DactylError::Conversion`].
324    pub fn get_bool<I: RowIndex>(&self, index: I) -> Result<bool, DactylError> {
325        let i = self.idx(&index)?;
326        match &self.values[i] {
327            serde_json::Value::Null => Err(Self::null_err(&index)),
328            serde_json::Value::Bool(b) => Ok(*b),
329            serde_json::Value::Number(n) if n.as_i64() == Some(0) => Ok(false),
330            serde_json::Value::Number(n) if n.as_i64() == Some(1) => Ok(true),
331            other => Err(DactylError::Conversion(format!(
332                "cannot read {other:?} as bool at column {:?}",
333                index
334            ))),
335        }
336    }
337
338    /// Lenient **owned** `i64`: accepts JSON integers (not fractional reals).
339    ///
340    /// NULL → [`DactylError::Conversion`].
341    pub fn get_int<I: RowIndex>(&self, index: I) -> Result<i64, DactylError> {
342        let i = self.idx(&index)?;
343        match &self.values[i] {
344            serde_json::Value::Null => Err(Self::null_err(&index)),
345            serde_json::Value::Number(n) => n.as_i64().ok_or_else(|| {
346                DactylError::Conversion(format!("value is not i64 at column {:?}", index))
347            }),
348            other => Err(DactylError::Conversion(format!(
349                "cannot read {other:?} as i64 at column {:?}",
350                index
351            ))),
352        }
353    }
354
355    /// Lenient **owned** `f64`: accepts any JSON number (integers and reals).
356    ///
357    /// NULL → [`DactylError::Conversion`].
358    pub fn get_real<I: RowIndex>(&self, index: I) -> Result<f64, DactylError> {
359        let i = self.idx(&index)?;
360        match &self.values[i] {
361            serde_json::Value::Null => Err(Self::null_err(&index)),
362            serde_json::Value::Number(n) => n.as_f64().ok_or_else(|| {
363                DactylError::Conversion(format!("value is not f64 at column {:?}", index))
364            }),
365            other => Err(DactylError::Conversion(format!(
366                "cannot read {other:?} as f64 at column {:?}",
367                index
368            ))),
369        }
370    }
371
372    /// Lenient **owned** `String`: accepts JSON string (clones the cell).
373    ///
374    /// NULL → [`DactylError::Conversion`]. Prefer [`Self::get_str_ref`] to borrow.
375    pub fn get_str<I: RowIndex>(&self, index: I) -> Result<String, DactylError> {
376        self.get_str_ref(index).map(str::to_owned)
377    }
378
379    /// Borrowed `&str` tied to the row lifetime. Accepts JSON string only.
380    ///
381    /// NULL → [`DactylError::Conversion`]. The reference is valid while `self` lives.
382    pub fn get_str_ref<I: RowIndex>(&self, index: I) -> Result<&str, DactylError> {
383        let i = self.idx(&index)?;
384        match &self.values[i] {
385            serde_json::Value::Null => Err(Self::null_err(&index)),
386            serde_json::Value::String(s) => Ok(s.as_str()),
387            other => Err(DactylError::Conversion(format!(
388                "cannot read {other:?} as str at column {:?}",
389                index
390            ))),
391        }
392    }
393
394    /// Owned clone of the raw JSON cell (including `Null`).
395    pub fn get_json<I: RowIndex>(&self, index: I) -> Result<serde_json::Value, DactylError> {
396        self.get_json_ref(index).cloned()
397    }
398
399    /// Borrowed raw JSON cell tied to the row lifetime (including `Null`).
400    pub fn get_json_ref<I: RowIndex>(&self, index: I) -> Result<&serde_json::Value, DactylError> {
401        let i = self.idx(&index)?;
402        Ok(&self.values[i])
403    }
404
405    fn null_err<I: std::fmt::Debug>(index: &I) -> DactylError {
406        DactylError::Conversion(format!(
407            "column {:?} is NULL; use Option<T> with get for nullable columns",
408            index
409        ))
410    }
411
412    fn idx<I: RowIndex>(&self, index: &I) -> Result<usize, DactylError> {
413        index
414            .idx(self)
415            .ok_or_else(|| DactylError::ColumnNotFound(format!("{:?}", index)))
416            .and_then(|i| {
417                if i < self.values.len() {
418                    Ok(i)
419                } else {
420                    Err(DactylError::ColumnNotFound(format!(
421                        "index {i} out of bounds"
422                    )))
423                }
424            })
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use serde_json::json;
432
433    fn sample_row() -> Row {
434        Row {
435            columns: vec![
436                "id".into(),
437                "flag".into(),
438                "ratio".into(),
439                "label".into(),
440                "payload".into(),
441                "nullable".into(),
442                "name".into(), // first of duplicate alias pair
443                "name".into(), // second alias — positional only via index
444            ],
445            values: vec![
446                json!(42),
447                json!(true),
448                json!(1.5),
449                json!("hello"),
450                json!({"k": 1}),
451                json!(null),
452                json!("first"),
453                json!("second"),
454            ],
455        }
456    }
457
458    #[test]
459    fn scalar_matrix_strict_and_lenient() {
460        let row = sample_row();
461
462        assert_eq!(row.get::<_, i64>("id").unwrap(), 42);
463        assert_eq!(row.try_get::<_, i64>("id").unwrap(), 42);
464        assert_eq!(row.get_int("id").unwrap(), 42);
465        assert_eq!(row.get_real("id").unwrap(), 42.0);
466
467        assert!(row.get_bool("flag").unwrap());
468        assert!(row.get::<_, bool>("flag").unwrap());
469
470        assert_eq!(row.get::<_, f64>("ratio").unwrap(), 1.5);
471        assert_eq!(row.get_real("ratio").unwrap(), 1.5);
472        assert!(matches!(
473            row.get_int("ratio"),
474            Err(DactylError::Conversion(_))
475        ));
476
477        assert_eq!(row.get_str("label").unwrap(), "hello");
478        assert_eq!(row.get_str_ref("label").unwrap(), "hello");
479        assert_eq!(row.get::<_, String>("label").unwrap(), "hello");
480
481        let payload = row.get_json("payload").unwrap();
482        assert_eq!(payload, json!({"k": 1}));
483        assert_eq!(row.get_json_ref("payload").unwrap(), &json!({"k": 1}));
484        #[derive(Deserialize)]
485        struct Payload {
486            k: i64,
487        }
488        assert_eq!(row.get::<_, Payload>("payload").unwrap().k, 1);
489    }
490
491    #[test]
492    fn sqlite_style_bool_as_integer() {
493        let row = Row {
494            columns: vec!["flag".into(), "off".into()],
495            values: vec![json!(1), json!(0)],
496        };
497        assert!(row.get_bool("flag").unwrap());
498        assert!(!row.get_bool("off").unwrap());
499        // Strict serde bool rejects integer encoding.
500        assert!(matches!(
501            row.get::<_, bool>("flag"),
502            Err(DactylError::Conversion(_))
503        ));
504    }
505
506    #[test]
507    fn null_semantics() {
508        let row = sample_row();
509        assert!(row.is_null("nullable").unwrap());
510        assert!(!row.is_null("id").unwrap());
511        assert!(row.get::<_, Option<i64>>("nullable").unwrap().is_none());
512        assert!(row.get::<_, Option<String>>("nullable").unwrap().is_none());
513
514        let null_errs: Vec<DactylError> = vec![
515            row.get::<_, i64>("nullable").unwrap_err(),
516            row.get_int("nullable").unwrap_err(),
517            row.get_bool("nullable").unwrap_err(),
518            row.get_real("nullable").unwrap_err(),
519            row.get_str("nullable").unwrap_err(),
520            row.get_str_ref("nullable").unwrap_err(),
521        ];
522        for err in null_errs {
523            match err {
524                DactylError::Conversion(msg) => {
525                    assert!(msg.contains("NULL"), "expected NULL hint, got {msg}");
526                }
527                other => panic!("expected Conversion for NULL, got {other:?}"),
528            }
529        }
530
531        assert_eq!(row.get_json("nullable").unwrap(), json!(null));
532        assert!(row.get_json_ref("nullable").unwrap().is_null());
533    }
534
535    #[test]
536    fn missing_column_is_column_not_found() {
537        let row = sample_row();
538        assert!(matches!(
539            row.get::<_, i64>("nope"),
540            Err(DactylError::ColumnNotFound(_))
541        ));
542        assert!(matches!(
543            row.get_int("nope"),
544            Err(DactylError::ColumnNotFound(_))
545        ));
546        assert!(matches!(
547            row.is_null("nope"),
548            Err(DactylError::ColumnNotFound(_))
549        ));
550        assert!(matches!(
551            row.get_json_ref("nope"),
552            Err(DactylError::ColumnNotFound(_))
553        ));
554        assert!(matches!(
555            row.get::<_, i64>(99usize),
556            Err(DactylError::ColumnNotFound(_))
557        ));
558    }
559
560    #[test]
561    fn conversion_failures() {
562        let row = sample_row();
563        assert!(matches!(
564            row.get::<_, bool>("label"),
565            Err(DactylError::Conversion(_))
566        ));
567        assert!(matches!(
568            row.get_int("label"),
569            Err(DactylError::Conversion(_))
570        ));
571        assert!(matches!(row.get_str("id"), Err(DactylError::Conversion(_))));
572        assert!(matches!(
573            row.get_bool("ratio"),
574            Err(DactylError::Conversion(_))
575        ));
576    }
577
578    #[test]
579    fn duplicate_alias_first_match_and_positional() {
580        let row = sample_row();
581        // Named lookup: left-to-right first match.
582        assert_eq!(row.get_str("name").unwrap(), "first");
583        assert_eq!(row.get_str_ref("name").unwrap(), "first");
584        assert_eq!(row.get_int(0usize).unwrap(), 42);
585        // Positional index reaches the second "name" column.
586        let second_name_idx = row
587            .columns
588            .iter()
589            .enumerate()
590            .filter(|(_, c)| *c == "name")
591            .nth(1)
592            .map(|(i, _)| i)
593            .expect("second name");
594        assert_eq!(row.get_str(second_name_idx).unwrap(), "second");
595        assert_eq!(row.get_json_ref(second_name_idx).unwrap(), &json!("second"));
596    }
597
598    #[test]
599    fn borrowed_values_tied_to_row_lifetime() {
600        let row = sample_row();
601        let s: &str = row.get_str_ref("label").unwrap();
602        let j: &serde_json::Value = row.get_json_ref("payload").unwrap();
603        // Still usable while `row` is in scope (compile-time lifetime proof
604        // plus runtime equality).
605        assert_eq!(s, "hello");
606        assert_eq!(j["k"], 1);
607        // Owned getters return independent values.
608        let owned = row.get_str("label").unwrap();
609        drop(row);
610        assert_eq!(owned, "hello");
611    }
612}