Skip to main content

oxisql_core/
row.rs

1//! [`Row`], [`RowSet`], and the [`FromValue`] extraction trait.
2
3use std::fmt;
4
5use crate::{OxiSqlError, Value};
6
7// ── FromValue trait ─────────────────────────────────────────────────────────
8
9/// Extract a typed value from a [`Value`] enum variant.
10///
11/// Provides type-safe conversions from `Value` to concrete Rust types.
12/// Returns [`OxiSqlError::TypeMismatch`] when the variant does not match
13/// the requested type.
14///
15/// # Null handling
16///
17/// - `FromValue` for `Option<T>` returns `Ok(None)` for `Value::Null`.
18/// - `FromValue` for non-`Option` types returns `TypeMismatch` for `Null`.
19pub trait FromValue: Sized {
20    /// Try to extract `Self` from a [`Value`].
21    fn from_value(v: &Value) -> Result<Self, OxiSqlError>;
22}
23
24impl FromValue for bool {
25    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
26        match v {
27            Value::Bool(b) => Ok(*b),
28            other => Err(OxiSqlError::TypeMismatch {
29                expected: "Bool",
30                got: other.type_name(),
31            }),
32        }
33    }
34}
35
36impl FromValue for i32 {
37    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
38        match v {
39            Value::I64(n) => i32::try_from(*n).map_err(|_| OxiSqlError::TypeMismatch {
40                expected: "I64 (within i32 range)",
41                got: "I64 (out of i32 range)",
42            }),
43            other => Err(OxiSqlError::TypeMismatch {
44                expected: "I64",
45                got: other.type_name(),
46            }),
47        }
48    }
49}
50
51impl FromValue for i64 {
52    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
53        match v {
54            Value::I64(n) => Ok(*n),
55            other => Err(OxiSqlError::TypeMismatch {
56                expected: "I64",
57                got: other.type_name(),
58            }),
59        }
60    }
61}
62
63impl FromValue for f64 {
64    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
65        match v {
66            Value::F64(n) => Ok(*n),
67            // Allow I64 -> f64 coercion for convenience
68            Value::I64(n) => Ok(*n as f64),
69            other => Err(OxiSqlError::TypeMismatch {
70                expected: "F64",
71                got: other.type_name(),
72            }),
73        }
74    }
75}
76
77impl FromValue for String {
78    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
79        match v {
80            Value::Text(s) => Ok(s.clone()),
81            Value::Json(s) => Ok(s.clone()),
82            Value::Decimal(s) => Ok(s.clone()),
83            // Format UUID as the standard hyphenated string representation.
84            Value::Uuid(_) => Ok(format!("{v}")),
85            other => Err(OxiSqlError::TypeMismatch {
86                expected: "Text",
87                got: other.type_name(),
88            }),
89        }
90    }
91}
92
93impl FromValue for Vec<u8> {
94    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
95        match v {
96            Value::Blob(b) => Ok(b.clone()),
97            other => Err(OxiSqlError::TypeMismatch {
98                expected: "Blob",
99                got: other.type_name(),
100            }),
101        }
102    }
103}
104
105impl<T: FromValue> FromValue for Option<T> {
106    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
107        if v.is_null() {
108            Ok(None)
109        } else {
110            T::from_value(v).map(Some)
111        }
112    }
113}
114
115impl FromValue for u128 {
116    fn from_value(v: &Value) -> Result<Self, OxiSqlError> {
117        match v {
118            Value::Uuid(u) => Ok(*u),
119            other => Err(OxiSqlError::TypeMismatch {
120                expected: "Uuid",
121                got: other.type_name(),
122            }),
123        }
124    }
125}
126
127// ── Row ─────────────────────────────────────────────────────────────────────
128
129/// A single row returned from a query, with named columns.
130#[derive(Debug, Clone)]
131pub struct Row {
132    columns: Vec<String>,
133    values: Vec<Value>,
134    index: std::collections::HashMap<String, usize>,
135}
136
137impl Row {
138    /// Construct a [`Row`] from parallel column-name and value vectors.
139    ///
140    /// # Panics
141    ///
142    /// Does not panic; callers are responsible for ensuring `columns` and
143    /// `values` have the same length.
144    pub fn new(columns: Vec<String>, values: Vec<Value>) -> Self {
145        let index = columns
146            .iter()
147            .enumerate()
148            .map(|(i, c)| (c.clone(), i))
149            .collect();
150        Self {
151            columns,
152            values,
153            index,
154        }
155    }
156
157    /// Look up a value by column name.  Returns `None` if the column does not
158    /// exist in this row.
159    pub fn get(&self, col: &str) -> Option<&Value> {
160        self.index.get(col).and_then(|&i| self.values.get(i))
161    }
162
163    /// Look up a value by zero-based column index.
164    pub fn get_by_index(&self, i: usize) -> Option<&Value> {
165        self.values.get(i)
166    }
167
168    /// Type-safe extraction by column name.
169    ///
170    /// Looks up the column and converts via [`FromValue`].  Returns
171    /// [`OxiSqlError::Other`] if the column does not exist, or
172    /// [`OxiSqlError::TypeMismatch`] if the value cannot be converted.
173    ///
174    /// # Example
175    ///
176    /// ```rust
177    /// # use oxisql_core::{Row, Value};
178    /// let row = Row::new(vec!["id".into()], vec![Value::I64(42)]);
179    /// let id: i64 = row.try_get("id").unwrap();
180    /// assert_eq!(id, 42);
181    /// ```
182    pub fn try_get<T: FromValue>(&self, col: &str) -> Result<T, OxiSqlError> {
183        let val = self
184            .get(col)
185            .ok_or_else(|| OxiSqlError::Other(format!("column '{col}' not found")))?;
186        T::from_value(val)
187    }
188
189    /// Type-safe extraction by column index.
190    ///
191    /// Like [`try_get`](Row::try_get) but uses a zero-based index.
192    pub fn try_get_by_index<T: FromValue>(&self, i: usize) -> Result<T, OxiSqlError> {
193        let val = self
194            .get_by_index(i)
195            .ok_or_else(|| OxiSqlError::Other(format!("column index {i} out of range")))?;
196        T::from_value(val)
197    }
198
199    /// Return the ordered column names for this row.
200    pub fn columns(&self) -> &[String] {
201        &self.columns
202    }
203
204    /// Return the ordered values for this row.
205    pub fn values(&self) -> &[Value] {
206        &self.values
207    }
208
209    /// Return the number of columns in this row.
210    pub fn column_count(&self) -> usize {
211        self.columns.len()
212    }
213
214    /// Check whether a column value is `NULL`.
215    ///
216    /// Returns `true` if the column exists and its value is `Value::Null`.
217    /// Returns `false` if the column does not exist or has a non-null value.
218    pub fn is_null(&self, col: &str) -> bool {
219        self.get(col).is_some_and(|v| v.is_null())
220    }
221
222    /// Consume the row and return its values, discarding column names.
223    pub fn into_values(self) -> Vec<Value> {
224        self.values
225    }
226}
227
228impl fmt::Display for Row {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "{{")?;
231        for (i, (col, val)) in self.columns.iter().zip(self.values.iter()).enumerate() {
232            if i > 0 {
233                write!(f, ", ")?;
234            }
235            write!(f, "{col}: {val}")?;
236        }
237        write!(f, "}}")
238    }
239}
240
241// ── RowSet ──────────────────────────────────────────────────────────────────
242
243/// A collection of [`Row`]s with schema metadata.
244///
245/// Wraps `Vec<Row>` and provides convenience methods for accessing results.
246#[derive(Debug, Clone)]
247pub struct RowSet {
248    /// The column names (shared across all rows).
249    columns: Vec<String>,
250    /// The rows in this result set.
251    rows: Vec<Row>,
252}
253
254impl RowSet {
255    /// Create a [`RowSet`] from rows.
256    ///
257    /// Column names are extracted from the first row.  If `rows` is empty,
258    /// columns will be empty.
259    pub fn from_rows(rows: Vec<Row>) -> Self {
260        let columns = rows
261            .first()
262            .map(|r| r.columns().to_vec())
263            .unwrap_or_default();
264        Self { columns, rows }
265    }
266
267    /// Create a [`RowSet`] with explicit column names.
268    pub fn new(columns: Vec<String>, rows: Vec<Row>) -> Self {
269        Self { columns, rows }
270    }
271
272    /// Return the column names for this result set.
273    pub fn columns(&self) -> &[String] {
274        &self.columns
275    }
276
277    /// Return the rows.
278    pub fn rows(&self) -> &[Row] {
279        &self.rows
280    }
281
282    /// Return the number of rows.
283    pub fn len(&self) -> usize {
284        self.rows.len()
285    }
286
287    /// Returns `true` if there are no rows.
288    pub fn is_empty(&self) -> bool {
289        self.rows.is_empty()
290    }
291
292    /// Return the number of columns.
293    pub fn column_count(&self) -> usize {
294        self.columns.len()
295    }
296
297    /// Consume the result set and return the inner rows.
298    pub fn into_rows(self) -> Vec<Row> {
299        self.rows
300    }
301}