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

            
14
use 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)]
21
pub 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

            
34
impl DbValue {
35
2
    pub fn is_null(&self) -> bool {
36
2
        matches!(self, DbValue::Null)
37
2
    }
38
3
    pub fn as_integer(&self) -> Option<i64> {
39
3
        if let DbValue::Integer(i) = self {
40
2
            Some(*i)
41
        } else {
42
1
            None
43
        }
44
3
    }
45
1
    pub fn as_real(&self) -> Option<f64> {
46
1
        if let DbValue::Real(r) = self {
47
1
            Some(*r)
48
        } else {
49
            None
50
        }
51
1
    }
52
2
    pub fn as_text(&self) -> Option<&AzString> {
53
2
        if let DbValue::Text(t) = self {
54
2
            Some(t)
55
        } else {
56
            None
57
        }
58
2
    }
59
}
60

            
61
impl_vec!(
62
    DbValue,
63
    DbValueVec,
64
    DbValueVecDestructor,
65
    DbValueVecDestructorType,
66
    DbValueVecSlice,
67
    OptionDbValue
68
);
69
impl_vec_debug!(DbValue, DbValueVec);
70
impl_vec_clone!(DbValue, DbValueVec, DbValueVecDestructor);
71
impl_vec_partialeq!(DbValue, DbValueVec);
72
impl_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)]
79
pub 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

            
86
impl DbRows {
87
    /// Number of result columns.
88
9
    pub fn num_columns(&self) -> usize {
89
9
        self.columns.as_ref().len()
90
9
    }
91
    /// Number of result rows (`0` when there are no columns).
92
2
    pub fn num_rows(&self) -> usize {
93
2
        let cols = self.num_columns();
94
2
        if cols == 0 {
95
1
            0
96
        } else {
97
1
            self.values.as_ref().len() / cols
98
        }
99
2
    }
100
    /// The cell at `(row, col)`, or `None` if out of range.
101
5
    pub fn get(&self, row: usize, col: usize) -> Option<&DbValue> {
102
5
        let cols = self.num_columns();
103
5
        if col >= cols {
104
2
            return None;
105
3
        }
106
3
        self.values.as_ref().get(row * cols + col)
107
5
    }
108
}
109

            
110
#[cfg(test)]
111
mod tests {
112
    use super::*;
113

            
114
    #[test]
115
1
    fn dbvalue_accessors() {
116
1
        assert!(DbValue::Null.is_null());
117
1
        assert_eq!(DbValue::Integer(7).as_integer(), Some(7));
118
1
        assert_eq!(DbValue::Real(1.5).as_real(), Some(1.5));
119
1
        assert_eq!(
120
1
            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
1
        assert_eq!(DbValue::Null.as_integer(), None);
125
1
        assert!(!DbValue::Integer(0).is_null());
126
1
    }
127

            
128
    #[test]
129
1
    fn dbrows_indexing() {
130
        // 2 columns × 2 rows.
131
1
        let columns = StringVec::from_vec(vec![
132
1
            AzString::from_const_str("id"),
133
1
            AzString::from_const_str("name"),
134
        ]);
135
1
        let values = DbValueVec::from_vec(vec![
136
1
            DbValue::Integer(1),
137
1
            DbValue::Text(AzString::from_const_str("alice")),
138
1
            DbValue::Integer(2),
139
1
            DbValue::Text(AzString::from_const_str("bob")),
140
        ]);
141
1
        let rows = DbRows { columns, values };
142

            
143
1
        assert_eq!(rows.num_columns(), 2);
144
1
        assert_eq!(rows.num_rows(), 2);
145
1
        assert_eq!(rows.get(0, 0).and_then(|v| v.as_integer()), Some(1));
146
1
        assert_eq!(
147
1
            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
1
        assert!(rows.get(0, 2).is_none());
152
1
        assert!(rows.get(2, 0).is_none());
153
1
    }
154

            
155
    #[test]
156
1
    fn dbrows_empty() {
157
1
        let rows = DbRows {
158
1
            columns: StringVec::from_vec(vec![]),
159
1
            values: DbValueVec::from_vec(vec![]),
160
1
        };
161
1
        assert_eq!(rows.num_columns(), 0);
162
1
        assert_eq!(rows.num_rows(), 0);
163
1
        assert!(rows.get(0, 0).is_none());
164
1
    }
165
}