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