Skip to main content

modo/db/
from_row.rs

1use std::collections::HashMap;
2
3use crate::error::{Error, Result};
4
5/// Trait for converting a `libsql::Row` into a Rust struct.
6///
7/// Implement this per struct, choosing positional (`row.get(idx)`) or
8/// name-based ([`ColumnMap`]) access for each column.
9pub trait FromRow: Sized {
10    /// Convert a row into `Self`.
11    ///
12    /// # Errors
13    ///
14    /// Returns an error if a column is missing or has an incompatible type.
15    fn from_row(row: &libsql::Row) -> Result<Self>;
16}
17
18/// Column name to index lookup built from a single row's column metadata.
19///
20/// Construct one inside your [`FromRow`] implementation to access columns by
21/// name instead of positional index.
22pub struct ColumnMap {
23    map: HashMap<String, i32>,
24}
25
26impl ColumnMap {
27    /// Build lookup from a row's column metadata.
28    pub fn from_row(row: &libsql::Row) -> Self {
29        let count = row.column_count();
30        let mut map = HashMap::with_capacity(count as usize);
31        for i in 0..count {
32            if let Some(name) = row.column_name(i) {
33                map.insert(name.to_string(), i);
34            }
35        }
36        Self { map }
37    }
38
39    /// Look up the column index by name.
40    ///
41    /// Returns the zero-based column index, or an error if the column is not found.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the column name does not exist in the row.
46    pub fn index(&self, name: &str) -> Result<i32> {
47        self.map
48            .get(name)
49            .copied()
50            .ok_or_else(|| Error::internal(format!("column not found: {name}")))
51    }
52
53    /// Get a typed value by column name.
54    ///
55    /// Looks up the column index by name and extracts the raw `libsql::Value`,
56    /// then converts it via the [`FromValue`] trait.
57    /// Supported types: `String`, `i32`, `i64`, `u32`, `u64`, `f64`, `bool`,
58    /// `Vec<u8>`, `Option<T>`, and `libsql::Value`.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the column is not found or the value cannot be
63    /// converted to `T`.
64    pub fn get<T: FromValue>(&self, row: &libsql::Row, name: &str) -> Result<T> {
65        let idx = self.index(name)?;
66        let val = row.get_value(idx).map_err(Error::from)?;
67        T::from_value(val)
68    }
69}
70
71/// Converts a `libsql::Value` into a concrete Rust type.
72///
73/// This trait mirrors the sealed `FromValue` inside libsql, providing the same
74/// conversions for use with [`ColumnMap::get`].
75///
76/// Implemented for: `String`, `i32`, `i64`, `u32`, `u64`, `f64`, `bool`,
77/// `Vec<u8>`, `Option<T>` (where `T: FromValue`), and `libsql::Value`.
78pub trait FromValue: Sized {
79    /// Convert a value into `Self`.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error on type mismatch or unexpected null.
84    fn from_value(val: libsql::Value) -> Result<Self>;
85}
86
87impl FromValue for libsql::Value {
88    fn from_value(val: libsql::Value) -> Result<Self> {
89        Ok(val)
90    }
91}
92
93impl FromValue for String {
94    fn from_value(val: libsql::Value) -> Result<Self> {
95        match val {
96            libsql::Value::Text(s) => Ok(s),
97            libsql::Value::Null => Err(Error::internal("unexpected null value")),
98            _ => Err(Error::internal("invalid column type: expected text")),
99        }
100    }
101}
102
103impl FromValue for i32 {
104    fn from_value(val: libsql::Value) -> Result<Self> {
105        match val {
106            libsql::Value::Integer(i) => {
107                i32::try_from(i).map_err(|_| Error::internal("integer out of i32 range"))
108            }
109            libsql::Value::Null => Err(Error::internal("unexpected null value")),
110            _ => Err(Error::internal("invalid column type: expected integer")),
111        }
112    }
113}
114
115impl FromValue for u32 {
116    fn from_value(val: libsql::Value) -> Result<Self> {
117        match val {
118            libsql::Value::Integer(i) => {
119                u32::try_from(i).map_err(|_| Error::internal("integer out of u32 range"))
120            }
121            libsql::Value::Null => Err(Error::internal("unexpected null value")),
122            _ => Err(Error::internal("invalid column type: expected integer")),
123        }
124    }
125}
126
127impl FromValue for i64 {
128    fn from_value(val: libsql::Value) -> Result<Self> {
129        match val {
130            libsql::Value::Integer(i) => Ok(i),
131            libsql::Value::Null => Err(Error::internal("unexpected null value")),
132            _ => Err(Error::internal("invalid column type: expected integer")),
133        }
134    }
135}
136
137impl FromValue for u64 {
138    fn from_value(val: libsql::Value) -> Result<Self> {
139        match val {
140            libsql::Value::Integer(i) => {
141                u64::try_from(i).map_err(|_| Error::internal("integer out of u64 range"))
142            }
143            libsql::Value::Null => Err(Error::internal("unexpected null value")),
144            _ => Err(Error::internal("invalid column type: expected integer")),
145        }
146    }
147}
148
149impl FromValue for f64 {
150    fn from_value(val: libsql::Value) -> Result<Self> {
151        match val {
152            libsql::Value::Real(f) => Ok(f),
153            libsql::Value::Integer(i) => Ok(i as f64),
154            libsql::Value::Null => Err(Error::internal("unexpected null value")),
155            _ => Err(Error::internal("invalid column type: expected real")),
156        }
157    }
158}
159
160impl FromValue for bool {
161    fn from_value(val: libsql::Value) -> Result<Self> {
162        match val {
163            libsql::Value::Integer(0) => Ok(false),
164            libsql::Value::Integer(_) => Ok(true),
165            libsql::Value::Null => Err(Error::internal("unexpected null value")),
166            _ => Err(Error::internal("invalid column type: expected integer")),
167        }
168    }
169}
170
171impl FromValue for Vec<u8> {
172    fn from_value(val: libsql::Value) -> Result<Self> {
173        match val {
174            libsql::Value::Blob(b) => Ok(b),
175            libsql::Value::Null => Err(Error::internal("unexpected null value")),
176            _ => Err(Error::internal("invalid column type: expected blob")),
177        }
178    }
179}
180
181impl<T: FromValue> FromValue for Option<T> {
182    fn from_value(val: libsql::Value) -> Result<Self> {
183        match val {
184            libsql::Value::Null => Ok(None),
185            other => T::from_value(other).map(Some),
186        }
187    }
188}