Skip to main content

azul_core/
db.rs

1//! POD types for the SQL database surface (SUPER_PLAN_2 §4 P4.3).
2//!
3//! Engine-agnostic: the public API is SQL strings plus typed value arrays,
4//! so the engine (bundled SQLite via `rusqlite`) stays fully hidden behind
5//! the `db-sqlite` feature in `azul-dll`. The handle type (`Db`, wrapping a
6//! `rusqlite::Connection`) lives in the dll — like `App` — because it
7//! carries an engine resource; these param/result *data* types live here in
8//! `azul-core` (no engine dep) so they're always present and codegen-able.
9//!
10//! Shape: `db.execute(sql, params: DbValueVec) -> rows_affected` and
11//! `db.query(sql, params) -> DbRows`. `DbValue` maps onto SQLite's five
12//! storage classes.
13
14use azul_css::{AzString, StringVec, U8Vec};
15
16/// A single SQL value — a bound statement parameter or a result cell.
17/// Mirrors SQLite's storage classes (Null / Integer / Real / Text / Blob)
18/// but names nothing engine-specific.
19#[repr(C, u8)]
20#[derive(Debug, Clone, PartialEq)]
21pub enum DbValue {
22    /// SQL `NULL`.
23    Null,
24    /// 64-bit signed integer.
25    Integer(i64),
26    /// 64-bit IEEE float.
27    Real(f64),
28    /// UTF-8 text.
29    Text(AzString),
30    /// Raw bytes.
31    Blob(U8Vec),
32}
33
34impl DbValue {
35    pub fn is_null(&self) -> bool {
36        matches!(self, DbValue::Null)
37    }
38    pub fn as_integer(&self) -> Option<i64> {
39        if let DbValue::Integer(i) = self {
40            Some(*i)
41        } else {
42            None
43        }
44    }
45    pub fn as_real(&self) -> Option<f64> {
46        if let DbValue::Real(r) = self {
47            Some(*r)
48        } else {
49            None
50        }
51    }
52    pub fn as_text(&self) -> Option<&AzString> {
53        if let DbValue::Text(t) = self {
54            Some(t)
55        } else {
56            None
57        }
58    }
59}
60
61impl_vec!(
62    DbValue,
63    DbValueVec,
64    DbValueVecDestructor,
65    DbValueVecDestructorType,
66    DbValueVecSlice,
67    OptionDbValue
68);
69impl_vec_debug!(DbValue, DbValueVec);
70impl_vec_clone!(DbValue, DbValueVec, DbValueVecDestructor);
71impl_vec_partialeq!(DbValue, DbValueVec);
72impl_option!(DbValue, OptionDbValue, copy = false, [Debug, Clone, PartialEq]);
73
74/// The result of `db.query(...)` — a column-named, row-major value grid.
75/// Flat (not nested vectors) for a simple FFI shape: cell `(row, col)` is
76/// `values[row * num_columns + col]`.
77#[repr(C)]
78#[derive(Debug, Clone, PartialEq)]
79pub struct DbRows {
80    /// Column names; `len()` is the number of columns.
81    pub columns: StringVec,
82    /// All cells, row-major. `len()` is `num_rows * num_columns`.
83    pub values: DbValueVec,
84}
85
86impl DbRows {
87    /// Number of result columns.
88    pub fn num_columns(&self) -> usize {
89        self.columns.as_ref().len()
90    }
91    /// Number of result rows (`0` when there are no columns).
92    pub fn num_rows(&self) -> usize {
93        let cols = self.num_columns();
94        if cols == 0 {
95            0
96        } else {
97            self.values.as_ref().len() / cols
98        }
99    }
100    /// The cell at `(row, col)`, or `None` if out of range.
101    pub fn get(&self, row: usize, col: usize) -> Option<&DbValue> {
102        let cols = self.num_columns();
103        if col >= cols {
104            return None;
105        }
106        self.values.as_ref().get(row * cols + col)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn dbvalue_accessors() {
116        assert!(DbValue::Null.is_null());
117        assert_eq!(DbValue::Integer(7).as_integer(), Some(7));
118        assert_eq!(DbValue::Real(1.5).as_real(), Some(1.5));
119        assert_eq!(
120            DbValue::Text(AzString::from_const_str("hi")).as_text().map(|s| s.as_str()),
121            Some("hi")
122        );
123        // Wrong-variant accessors return None.
124        assert_eq!(DbValue::Null.as_integer(), None);
125        assert!(!DbValue::Integer(0).is_null());
126    }
127
128    #[test]
129    fn dbrows_indexing() {
130        // 2 columns × 2 rows.
131        let columns = StringVec::from_vec(vec![
132            AzString::from_const_str("id"),
133            AzString::from_const_str("name"),
134        ]);
135        let values = DbValueVec::from_vec(vec![
136            DbValue::Integer(1),
137            DbValue::Text(AzString::from_const_str("alice")),
138            DbValue::Integer(2),
139            DbValue::Text(AzString::from_const_str("bob")),
140        ]);
141        let rows = DbRows { columns, values };
142
143        assert_eq!(rows.num_columns(), 2);
144        assert_eq!(rows.num_rows(), 2);
145        assert_eq!(rows.get(0, 0).and_then(|v| v.as_integer()), Some(1));
146        assert_eq!(
147            rows.get(1, 1).and_then(|v| v.as_text()).map(|s| s.as_str()),
148            Some("bob")
149        );
150        // Out-of-range column / row → None.
151        assert!(rows.get(0, 2).is_none());
152        assert!(rows.get(2, 0).is_none());
153    }
154
155    #[test]
156    fn dbrows_empty() {
157        let rows = DbRows {
158            columns: StringVec::from_vec(vec![]),
159            values: DbValueVec::from_vec(vec![]),
160        };
161        assert_eq!(rows.num_columns(), 0);
162        assert_eq!(rows.num_rows(), 0);
163        assert!(rows.get(0, 0).is_none());
164    }
165}