Skip to main content

dactyl_db/
rows.rs

1//! Row projection returned by [`crate::read`].
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, PartialEq)]
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 `read` 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, PartialEq)]
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    Blob(Vec<u8>),
86}
87
88impl serde::Serialize for Parameter {
89    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
90    where
91        S: serde::Serializer,
92    {
93        match self {
94            Parameter::Null => serializer.serialize_unit(),
95            Parameter::Bool(b) => serializer.serialize_bool(*b),
96            Parameter::Integer(i) => serializer.serialize_i64(*i),
97            Parameter::Real(f) => serializer.serialize_f64(*f),
98            Parameter::Text(s) => serializer.serialize_str(s),
99            Parameter::Blob(bytes) => serializer.serialize_bytes(bytes),
100        }
101    }
102}
103
104impl<'de> serde::Deserialize<'de> for Parameter {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: serde::Deserializer<'de>,
108    {
109        struct ParameterVisitor;
110        impl<'de> serde::de::Visitor<'de> for ParameterVisitor {
111            type Value = Parameter;
112            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
113                formatter.write_str("a database parameter value")
114            }
115            fn visit_none<E>(self) -> Result<Self::Value, E>
116            where
117                E: serde::de::Error,
118            {
119                Ok(Parameter::Null)
120            }
121            fn visit_unit<E>(self) -> Result<Self::Value, E>
122            where
123                E: serde::de::Error,
124            {
125                Ok(Parameter::Null)
126            }
127            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
128            where
129                E: serde::de::Error,
130            {
131                Ok(Parameter::Bool(v))
132            }
133            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
134            where
135                E: serde::de::Error,
136            {
137                Ok(Parameter::Integer(v))
138            }
139            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
140            where
141                E: serde::de::Error,
142            {
143                Ok(Parameter::Integer(v as i64))
144            }
145            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
146            where
147                E: serde::de::Error,
148            {
149                Ok(Parameter::Real(v))
150            }
151            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
152            where
153                E: serde::de::Error,
154            {
155                Ok(Parameter::Text(v.to_string()))
156            }
157            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
158            where
159                E: serde::de::Error,
160            {
161                Ok(Parameter::Text(v))
162            }
163            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
164            where
165                E: serde::de::Error,
166            {
167                Ok(Parameter::Blob(v.to_vec()))
168            }
169            fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
170            where
171                E: serde::de::Error,
172            {
173                Ok(Parameter::Blob(v))
174            }
175            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
176            where
177                A: serde::de::SeqAccess<'de>,
178            {
179                let mut bytes = Vec::new();
180                while let Some(value) = seq.next_element::<u64>()? {
181                    let byte = u8::try_from(value).map_err(|_| {
182                        serde::de::Error::custom("blob parameter values must fit in a byte")
183                    })?;
184                    bytes.push(byte);
185                }
186                Ok(Parameter::Blob(bytes))
187            }
188        }
189        deserializer.deserialize_any(ParameterVisitor)
190    }
191}
192
193impl From<i64> for Parameter {
194    fn from(v: i64) -> Self {
195        Parameter::Integer(v)
196    }
197}
198impl From<i32> for Parameter {
199    fn from(v: i32) -> Self {
200        Parameter::Integer(v as i64)
201    }
202}
203impl From<u32> for Parameter {
204    fn from(v: u32) -> Self {
205        Parameter::Integer(v as i64)
206    }
207}
208impl From<usize> for Parameter {
209    fn from(v: usize) -> Self {
210        Parameter::Integer(v as i64)
211    }
212}
213impl From<bool> for Parameter {
214    fn from(v: bool) -> Self {
215        Parameter::Bool(v)
216    }
217}
218impl From<f64> for Parameter {
219    fn from(v: f64) -> Self {
220        Parameter::Real(v)
221    }
222}
223impl From<String> for Parameter {
224    fn from(v: String) -> Self {
225        Parameter::Text(v)
226    }
227}
228impl From<&str> for Parameter {
229    fn from(v: &str) -> Self {
230        Parameter::Text(v.to_string())
231    }
232}
233
234impl From<Vec<u8>> for Parameter {
235    fn from(v: Vec<u8>) -> Self {
236        Parameter::Blob(v)
237    }
238}
239
240impl From<Option<String>> for Parameter {
241    fn from(v: Option<String>) -> Self {
242        match v {
243            Some(s) => Parameter::Text(s),
244            None => Parameter::Null,
245        }
246    }
247}
248
249impl From<Option<&str>> for Parameter {
250    fn from(v: Option<&str>) -> Self {
251        match v {
252            Some(s) => Parameter::Text(s.to_string()),
253            None => Parameter::Null,
254        }
255    }
256}
257
258impl From<Option<i64>> for Parameter {
259    fn from(v: Option<i64>) -> Self {
260        match v {
261            Some(i) => Parameter::Integer(i),
262            None => Parameter::Null,
263        }
264    }
265}
266
267impl From<Option<bool>> for Parameter {
268    fn from(v: Option<bool>) -> Self {
269        match v {
270            Some(b) => Parameter::Bool(b),
271            None => Parameter::Null,
272        }
273    }
274}
275
276/// Helper trait for row indexing by position or column name.
277///
278/// Named indexes (`&str`, `String`) resolve **left-to-right first match** when
279/// a result set contains duplicate column aliases.
280pub trait RowIndex: std::fmt::Debug {
281    /// Return the resolved index in `row`, or `None` if the name is absent.
282    fn idx(&self, row: &Row) -> Option<usize>;
283}
284
285impl RowIndex for usize {
286    fn idx(&self, _row: &Row) -> Option<usize> {
287        Some(*self)
288    }
289}
290
291impl RowIndex for &str {
292    fn idx(&self, row: &Row) -> Option<usize> {
293        // First match wins for duplicate aliases (documented contract).
294        row.columns.iter().position(|c| c == self)
295    }
296}
297
298impl RowIndex for String {
299    fn idx(&self, row: &Row) -> Option<usize> {
300        row.columns.iter().position(|c| c == self)
301    }
302}
303
304impl Row {
305    /// Strict typed extraction via `serde` into an **owned** `T`.
306    ///
307    /// - Missing column → [`DactylError::ColumnNotFound`].
308    /// - SQL NULL into non-`Option` `T` → [`DactylError::Conversion`].
309    /// - SQL NULL into `Option<T>` → `Ok(None)`.
310    /// - Type mismatch → [`DactylError::Conversion`].
311    /// - Duplicate aliases → first matching column (left-to-right).
312    ///
313    /// Prefer [`Self::get_bool`] for portable bools: SQLite stores booleans as
314    /// integers `0`/`1`, which strict `get::<bool>` rejects. For lenient
315    /// portable shapes use [`Self::get_bool`] / [`Self::get_int`] /
316    /// [`Self::get_real`] / [`Self::get_str`] / [`Self::get_json`].
317    pub fn get<I: RowIndex, T: serde::de::DeserializeOwned>(
318        &self,
319        index: I,
320    ) -> Result<T, DactylError> {
321        let i = self.idx(&index)?;
322        let val = &self.values[i];
323        serde_json::from_value(val.clone()).map_err(|e| {
324            if val.is_null() {
325                DactylError::Conversion(format!(
326                    "column {:?} is NULL; use Option<T> with get for nullable columns",
327                    index
328                ))
329            } else {
330                DactylError::Conversion(format!(
331                    "failed to convert column {:?} to target type: {}",
332                    index, e
333                ))
334            }
335        })
336    }
337
338    /// Alias for [`Self::get`]. Named for callers who prefer a `try_*` style.
339    pub fn try_get<I: RowIndex, T: serde::de::DeserializeOwned>(
340        &self,
341        index: I,
342    ) -> Result<T, DactylError> {
343        self.get(index)
344    }
345
346    /// Whether the cell is SQL NULL (`serde_json::Value::Null`).
347    ///
348    /// Missing column → [`DactylError::ColumnNotFound`].
349    pub fn is_null<I: RowIndex>(&self, index: I) -> Result<bool, DactylError> {
350        let i = self.idx(&index)?;
351        Ok(self.values[i].is_null())
352    }
353
354    /// Lenient **owned** `bool`: accepts JSON `true`/`false` or integer `0`/`1`.
355    ///
356    /// NULL → [`DactylError::Conversion`].
357    pub fn get_bool<I: RowIndex>(&self, index: I) -> Result<bool, DactylError> {
358        let i = self.idx(&index)?;
359        match &self.values[i] {
360            serde_json::Value::Null => Err(Self::null_err(&index)),
361            serde_json::Value::Bool(b) => Ok(*b),
362            serde_json::Value::Number(n) if n.as_i64() == Some(0) => Ok(false),
363            serde_json::Value::Number(n) if n.as_i64() == Some(1) => Ok(true),
364            other => Err(DactylError::Conversion(format!(
365                "cannot read {other:?} as bool at column {:?}",
366                index
367            ))),
368        }
369    }
370
371    /// Lenient **owned** `i64`: accepts JSON integers (not fractional reals).
372    ///
373    /// NULL → [`DactylError::Conversion`].
374    pub fn get_int<I: RowIndex>(&self, index: I) -> Result<i64, DactylError> {
375        let i = self.idx(&index)?;
376        match &self.values[i] {
377            serde_json::Value::Null => Err(Self::null_err(&index)),
378            serde_json::Value::Number(n) => n.as_i64().ok_or_else(|| {
379                DactylError::Conversion(format!("value is not i64 at column {:?}", index))
380            }),
381            other => Err(DactylError::Conversion(format!(
382                "cannot read {other:?} as i64 at column {:?}",
383                index
384            ))),
385        }
386    }
387
388    /// Lenient **owned** `f64`: accepts any JSON number (integers and reals).
389    ///
390    /// NULL → [`DactylError::Conversion`].
391    pub fn get_real<I: RowIndex>(&self, index: I) -> Result<f64, DactylError> {
392        let i = self.idx(&index)?;
393        match &self.values[i] {
394            serde_json::Value::Null => Err(Self::null_err(&index)),
395            serde_json::Value::Number(n) => n.as_f64().ok_or_else(|| {
396                DactylError::Conversion(format!("value is not f64 at column {:?}", index))
397            }),
398            other => Err(DactylError::Conversion(format!(
399                "cannot read {other:?} as f64 at column {:?}",
400                index
401            ))),
402        }
403    }
404
405    /// Lenient **owned** `String`: accepts JSON string (clones the cell).
406    ///
407    /// NULL → [`DactylError::Conversion`]. Prefer [`Self::get_str_ref`] to borrow.
408    pub fn get_str<I: RowIndex>(&self, index: I) -> Result<String, DactylError> {
409        self.get_str_ref(index).map(str::to_owned)
410    }
411
412    /// Borrowed `&str` tied to the row lifetime. Accepts JSON string only.
413    ///
414    /// NULL → [`DactylError::Conversion`]. The reference is valid while `self` lives.
415    pub fn get_str_ref<I: RowIndex>(&self, index: I) -> Result<&str, DactylError> {
416        let i = self.idx(&index)?;
417        match &self.values[i] {
418            serde_json::Value::Null => Err(Self::null_err(&index)),
419            serde_json::Value::String(s) => Ok(s.as_str()),
420            other => Err(DactylError::Conversion(format!(
421                "cannot read {other:?} as str at column {:?}",
422                index
423            ))),
424        }
425    }
426
427    /// Owned clone of the raw JSON cell (including `Null`).
428    pub fn get_json<I: RowIndex>(&self, index: I) -> Result<serde_json::Value, DactylError> {
429        self.get_json_ref(index).cloned()
430    }
431
432    /// Borrowed raw JSON cell tied to the row lifetime (including `Null`).
433    pub fn get_json_ref<I: RowIndex>(&self, index: I) -> Result<&serde_json::Value, DactylError> {
434        let i = self.idx(&index)?;
435        Ok(&self.values[i])
436    }
437
438    fn null_err<I: std::fmt::Debug>(index: &I) -> DactylError {
439        DactylError::Conversion(format!(
440            "column {:?} is NULL; use Option<T> with get for nullable columns",
441            index
442        ))
443    }
444
445    fn idx<I: RowIndex>(&self, index: &I) -> Result<usize, DactylError> {
446        index
447            .idx(self)
448            .ok_or_else(|| DactylError::ColumnNotFound(format!("{:?}", index)))
449            .and_then(|i| {
450                if i < self.values.len() {
451                    Ok(i)
452                } else {
453                    Err(DactylError::ColumnNotFound(format!(
454                        "index {i} out of bounds"
455                    )))
456                }
457            })
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use serde_json::json;
465
466    fn sample_row() -> Row {
467        Row {
468            columns: vec![
469                "id".into(),
470                "flag".into(),
471                "ratio".into(),
472                "label".into(),
473                "payload".into(),
474                "nullable".into(),
475                "name".into(), // first of duplicate alias pair
476                "name".into(), // second alias — positional only via index
477            ],
478            values: vec![
479                json!(42),
480                json!(true),
481                json!(1.5),
482                json!("hello"),
483                json!({"k": 1}),
484                json!(null),
485                json!("first"),
486                json!("second"),
487            ],
488        }
489    }
490
491    #[test]
492    fn scalar_matrix_strict_and_lenient() {
493        let row = sample_row();
494
495        assert_eq!(row.get::<_, i64>("id").unwrap(), 42);
496        assert_eq!(row.try_get::<_, i64>("id").unwrap(), 42);
497        assert_eq!(row.get_int("id").unwrap(), 42);
498        assert_eq!(row.get_real("id").unwrap(), 42.0);
499
500        assert!(row.get_bool("flag").unwrap());
501        assert!(row.get::<_, bool>("flag").unwrap());
502
503        assert_eq!(row.get::<_, f64>("ratio").unwrap(), 1.5);
504        assert_eq!(row.get_real("ratio").unwrap(), 1.5);
505        assert!(matches!(
506            row.get_int("ratio"),
507            Err(DactylError::Conversion(_))
508        ));
509
510        assert_eq!(row.get_str("label").unwrap(), "hello");
511        assert_eq!(row.get_str_ref("label").unwrap(), "hello");
512        assert_eq!(row.get::<_, String>("label").unwrap(), "hello");
513
514        let payload = row.get_json("payload").unwrap();
515        assert_eq!(payload, json!({"k": 1}));
516        assert_eq!(row.get_json_ref("payload").unwrap(), &json!({"k": 1}));
517        #[derive(Deserialize)]
518        struct Payload {
519            k: i64,
520        }
521        assert_eq!(row.get::<_, Payload>("payload").unwrap().k, 1);
522    }
523
524    #[test]
525    fn sqlite_style_bool_as_integer() {
526        let row = Row {
527            columns: vec!["flag".into(), "off".into()],
528            values: vec![json!(1), json!(0)],
529        };
530        assert!(row.get_bool("flag").unwrap());
531        assert!(!row.get_bool("off").unwrap());
532        // Strict serde bool rejects integer encoding.
533        assert!(matches!(
534            row.get::<_, bool>("flag"),
535            Err(DactylError::Conversion(_))
536        ));
537    }
538
539    #[test]
540    fn null_semantics() {
541        let row = sample_row();
542        assert!(row.is_null("nullable").unwrap());
543        assert!(!row.is_null("id").unwrap());
544        assert!(row.get::<_, Option<i64>>("nullable").unwrap().is_none());
545        assert!(row.get::<_, Option<String>>("nullable").unwrap().is_none());
546
547        let null_errs: Vec<DactylError> = vec![
548            row.get::<_, i64>("nullable").unwrap_err(),
549            row.get_int("nullable").unwrap_err(),
550            row.get_bool("nullable").unwrap_err(),
551            row.get_real("nullable").unwrap_err(),
552            row.get_str("nullable").unwrap_err(),
553            row.get_str_ref("nullable").unwrap_err(),
554        ];
555        for err in null_errs {
556            match err {
557                DactylError::Conversion(msg) => {
558                    assert!(msg.contains("NULL"), "expected NULL hint, got {msg}");
559                }
560                other => panic!("expected Conversion for NULL, got {other:?}"),
561            }
562        }
563
564        assert_eq!(row.get_json("nullable").unwrap(), json!(null));
565        assert!(row.get_json_ref("nullable").unwrap().is_null());
566    }
567
568    #[test]
569    fn missing_column_is_column_not_found() {
570        let row = sample_row();
571        assert!(matches!(
572            row.get::<_, i64>("nope"),
573            Err(DactylError::ColumnNotFound(_))
574        ));
575        assert!(matches!(
576            row.get_int("nope"),
577            Err(DactylError::ColumnNotFound(_))
578        ));
579        assert!(matches!(
580            row.is_null("nope"),
581            Err(DactylError::ColumnNotFound(_))
582        ));
583        assert!(matches!(
584            row.get_json_ref("nope"),
585            Err(DactylError::ColumnNotFound(_))
586        ));
587        assert!(matches!(
588            row.get::<_, i64>(99usize),
589            Err(DactylError::ColumnNotFound(_))
590        ));
591    }
592
593    #[test]
594    fn conversion_failures() {
595        let row = sample_row();
596        assert!(matches!(
597            row.get::<_, bool>("label"),
598            Err(DactylError::Conversion(_))
599        ));
600        assert!(matches!(
601            row.get_int("label"),
602            Err(DactylError::Conversion(_))
603        ));
604        assert!(matches!(row.get_str("id"), Err(DactylError::Conversion(_))));
605        assert!(matches!(
606            row.get_bool("ratio"),
607            Err(DactylError::Conversion(_))
608        ));
609    }
610
611    #[test]
612    fn duplicate_alias_first_match_and_positional() {
613        let row = sample_row();
614        // Named lookup: left-to-right first match.
615        assert_eq!(row.get_str("name").unwrap(), "first");
616        assert_eq!(row.get_str_ref("name").unwrap(), "first");
617        assert_eq!(row.get_int(0usize).unwrap(), 42);
618        // Positional index reaches the second "name" column.
619        let second_name_idx = row
620            .columns
621            .iter()
622            .enumerate()
623            .filter(|(_, c)| *c == "name")
624            .nth(1)
625            .map(|(i, _)| i)
626            .expect("second name");
627        assert_eq!(row.get_str(second_name_idx).unwrap(), "second");
628        assert_eq!(row.get_json_ref(second_name_idx).unwrap(), &json!("second"));
629    }
630
631    #[test]
632    fn blob_parameters_round_trip_through_json_arrays() {
633        let encoded = serde_json::to_value(Parameter::Blob(vec![1, 2, 3])).unwrap();
634        assert_eq!(encoded, json!([1, 2, 3]));
635        assert_eq!(
636            serde_json::from_value::<Parameter>(encoded).unwrap(),
637            Parameter::Blob(vec![1, 2, 3])
638        );
639    }
640
641    #[test]
642    fn borrowed_values_tied_to_row_lifetime() {
643        let row = sample_row();
644        let s: &str = row.get_str_ref("label").unwrap();
645        let j: &serde_json::Value = row.get_json_ref("payload").unwrap();
646        // Still usable while `row` is in scope (compile-time lifetime proof
647        // plus runtime equality).
648        assert_eq!(s, "hello");
649        assert_eq!(j["k"], 1);
650        // Owned getters return independent values.
651        let owned = row.get_str("label").unwrap();
652        drop(row);
653        assert_eq!(owned, "hello");
654    }
655}