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    #[must_use] pub const fn is_null(&self) -> bool {
36        matches!(self, Self::Null)
37    }
38    #[must_use] pub const fn as_integer(&self) -> Option<i64> {
39        if let Self::Integer(i) = self {
40            Some(*i)
41        } else {
42            None
43        }
44    }
45    #[must_use] pub const fn as_real(&self) -> Option<f64> {
46        if let Self::Real(r) = self {
47            Some(*r)
48        } else {
49            None
50        }
51    }
52    #[must_use] pub const fn as_text(&self) -> Option<&AzString> {
53        if let Self::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    #[must_use] 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    #[must_use] 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    #[must_use] 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(AzString::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(DbValue::as_integer), Some(1));
146        assert_eq!(
147            rows.get(1, 1).and_then(|v| v.as_text()).map(AzString::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}
166
167#[cfg(test)]
168mod autotest_generated {
169    //! Adversarial tests generated for the DB POD surface.
170    //!
171    //! Targets: malformed/ragged grids, i64/f64 extremes (MIN/MAX/NaN/±0/±inf),
172    //! Unicode + huge text, wrong-variant getters, and — the headline case —
173    //! index overflow in `DbRows::get` (`row * cols + col`), which panics under
174    //! the default overflow-checked test profile.
175    use super::*;
176
177    // ---- helpers -----------------------------------------------------------
178
179    fn cols(names: &[&'static str]) -> StringVec {
180        StringVec::from_vec(names.iter().copied().map(AzString::from_const_str).collect())
181    }
182
183    /// 2 columns × 2 rows of integers: [[10,11],[20,21]].
184    fn grid_2x2() -> DbRows {
185        DbRows {
186            columns: cols(&["a", "b"]),
187            values: DbValueVec::from_vec(vec![
188                DbValue::Integer(10),
189                DbValue::Integer(11),
190                DbValue::Integer(20),
191                DbValue::Integer(21),
192            ]),
193        }
194    }
195
196    /// Every non-`Null` / non-matching variant, for wrong-variant getter tests.
197    fn all_variants() -> Vec<DbValue> {
198        vec![
199            DbValue::Null,
200            DbValue::Integer(0),
201            DbValue::Real(0.0),
202            DbValue::Text(AzString::from_const_str("t")),
203            DbValue::Blob(U8Vec::from_vec(vec![1, 2, 3])),
204        ]
205    }
206
207    // ---- DbValue::is_null (predicate) --------------------------------------
208
209    #[test]
210    fn is_null_basic_true_false() {
211        assert!(DbValue::Null.is_null());
212        assert!(!DbValue::Integer(0).is_null());
213    }
214
215    #[test]
216    fn is_null_only_null_variant_is_null() {
217        for v in all_variants() {
218            let expect = matches!(v, DbValue::Null);
219            assert_eq!(v.is_null(), expect, "is_null mismatch for {v:?}");
220        }
221        // Extreme payloads must not change the discriminant-only answer.
222        assert!(!DbValue::Integer(i64::MIN).is_null());
223        assert!(!DbValue::Real(f64::NAN).is_null());
224        assert!(!DbValue::Blob(U8Vec::from_vec(vec![])).is_null());
225    }
226
227    #[test]
228    fn is_null_is_const_evaluable() {
229        // `const fn`: forcing const evaluation guards against a future body that
230        // is no longer const-safe.
231        const NULL_IS_NULL: bool = DbValue::Null.is_null();
232        const INT_IS_NULL: bool = DbValue::Integer(9).is_null();
233        assert!(NULL_IS_NULL);
234        assert!(!INT_IS_NULL);
235    }
236
237    // ---- DbValue::as_integer (getter) --------------------------------------
238
239    #[test]
240    fn as_integer_extremes_round_trip() {
241        for &i in &[0i64, 1, -1, i64::MIN, i64::MIN + 1, i64::MAX - 1, i64::MAX] {
242            assert_eq!(DbValue::Integer(i).as_integer(), Some(i), "round-trip {i}");
243        }
244    }
245
246    #[test]
247    fn as_integer_wrong_variant_is_none() {
248        for v in all_variants() {
249            if matches!(v, DbValue::Integer(_)) {
250                continue;
251            }
252            assert_eq!(v.as_integer(), None, "expected None for {v:?}");
253        }
254    }
255
256    #[test]
257    fn as_integer_is_const_evaluable() {
258        const MIN_INT: Option<i64> = DbValue::Integer(i64::MIN).as_integer();
259        assert_eq!(MIN_INT, Some(i64::MIN));
260    }
261
262    // ---- DbValue::as_real (getter) -----------------------------------------
263
264    #[test]
265    fn as_real_extremes_round_trip_bit_exact() {
266        // Compare via bit pattern so ±0.0 and subnormals are distinguished.
267        for &r in &[
268            0.0f64,
269            -0.0,
270            1.0,
271            -1.0,
272            f64::MIN,
273            f64::MAX,
274            f64::MIN_POSITIVE,
275            f64::EPSILON,
276            f64::INFINITY,
277            f64::NEG_INFINITY,
278        ] {
279            assert_eq!(
280                DbValue::Real(r).as_real().map(f64::to_bits),
281                Some(r.to_bits()),
282                "bit-exact round-trip for {r:?}",
283            );
284        }
285    }
286
287    #[test]
288    fn as_real_nan_is_preserved_not_lost() {
289        // NaN != NaN, so assert on the predicate rather than equality.
290        let got = DbValue::Real(f64::NAN).as_real();
291        assert!(got.is_some());
292        assert!(got.unwrap().is_nan());
293    }
294
295    #[test]
296    fn as_real_negative_zero_keeps_sign() {
297        let got = DbValue::Real(-0.0).as_real().unwrap();
298        assert_eq!(got, 0.0);
299        assert!(got.is_sign_negative(), "-0.0 must stay negative-signed");
300    }
301
302    #[test]
303    fn as_real_wrong_variant_is_none() {
304        for v in all_variants() {
305            if matches!(v, DbValue::Real(_)) {
306                continue;
307            }
308            assert_eq!(v.as_real(), None, "expected None for {v:?}");
309        }
310    }
311
312    // ---- DbValue::as_text (getter) -----------------------------------------
313
314    #[test]
315    fn as_text_empty_unicode_and_huge() {
316        // Empty string.
317        assert_eq!(
318            DbValue::Text(AzString::from_const_str("")).as_text().map(AzString::as_str),
319            Some(""),
320        );
321        // Multi-byte Unicode + NUL byte survives round-trip.
322        let tricky = "áé💥🔥\u{0}\u{FEFF}中文";
323        let v = DbValue::Text(AzString::from(tricky.to_string()));
324        assert_eq!(v.as_text().map(AzString::as_str), Some(tricky));
325        // Large payload: no truncation, length preserved.
326        let huge = "x".repeat(200_000);
327        let v = DbValue::Text(AzString::from(huge.clone()));
328        assert_eq!(v.as_text().map(|s| s.as_str().len()), Some(huge.len()));
329    }
330
331    #[test]
332    fn as_text_wrong_variant_is_none() {
333        for v in all_variants() {
334            if matches!(v, DbValue::Text(_)) {
335                continue;
336            }
337            assert!(v.as_text().is_none(), "expected None for {v:?}");
338        }
339    }
340
341    // ---- DbRows::num_columns (getter) --------------------------------------
342
343    #[test]
344    fn num_columns_empty_and_many() {
345        let empty = DbRows {
346            columns: StringVec::from_vec(vec![]),
347            values: DbValueVec::from_vec(vec![]),
348        };
349        assert_eq!(empty.num_columns(), 0);
350
351        let names: Vec<AzString> =
352            (0..1000).map(|_| AzString::from_const_str("c")).collect();
353        let wide = DbRows {
354            columns: StringVec::from_vec(names),
355            values: DbValueVec::from_vec(vec![]),
356        };
357        assert_eq!(wide.num_columns(), 1000);
358    }
359
360    // ---- DbRows::num_rows (getter) -----------------------------------------
361
362    #[test]
363    fn num_rows_zero_columns_never_divides_by_zero() {
364        // Malformed: 0 columns but non-empty values. Documented: `0` rows,
365        // and crucially no divide-by-zero panic.
366        let rows = DbRows {
367            columns: StringVec::from_vec(vec![]),
368            values: DbValueVec::from_vec(vec![DbValue::Null, DbValue::Integer(1)]),
369        };
370        assert_eq!(rows.num_rows(), 0);
371        // `get` must also stay safe with 0 columns.
372        assert!(rows.get(0, 0).is_none());
373    }
374
375    #[test]
376    fn num_rows_exact_and_ragged_truncates() {
377        // Exact multiple: 2 cols, 4 values → 2 rows.
378        assert_eq!(grid_2x2().num_rows(), 2);
379
380        // Ragged: 2 cols, 3 values → floor(3/2) = 1 row (last partial row dropped).
381        let ragged = DbRows {
382            columns: cols(&["a", "b"]),
383            values: DbValueVec::from_vec(vec![
384                DbValue::Integer(1),
385                DbValue::Integer(2),
386                DbValue::Integer(3),
387            ]),
388        };
389        assert_eq!(ragged.num_rows(), 1);
390        // Flat index 3 is past the end → None (deterministic, no panic).
391        assert!(ragged.get(1, 1).is_none());
392
393        // Single column: N values → N rows.
394        let single = DbRows {
395            columns: cols(&["only"]),
396            values: DbValueVec::from_vec(vec![
397                DbValue::Integer(0),
398                DbValue::Integer(1),
399                DbValue::Integer(2),
400            ]),
401        };
402        assert_eq!(single.num_rows(), 3);
403    }
404
405    // ---- DbRows::get (numeric / bounds) ------------------------------------
406
407    #[test]
408    fn get_zero_and_all_in_range_cells() {
409        let g = grid_2x2();
410        assert_eq!(g.get(0, 0).and_then(DbValue::as_integer), Some(10));
411        assert_eq!(g.get(0, 1).and_then(DbValue::as_integer), Some(11));
412        assert_eq!(g.get(1, 0).and_then(DbValue::as_integer), Some(20));
413        assert_eq!(g.get(1, 1).and_then(DbValue::as_integer), Some(21));
414    }
415
416    #[test]
417    fn get_out_of_range_column_is_none() {
418        let g = grid_2x2();
419        assert!(g.get(0, 2).is_none()); // col == num_columns
420        // col == usize::MAX hits the `col >= cols` guard before any arithmetic.
421        assert!(g.get(0, usize::MAX).is_none());
422        // Both extreme: the column guard short-circuits before `row * cols`.
423        assert!(g.get(usize::MAX, usize::MAX).is_none());
424    }
425
426    #[test]
427    fn get_out_of_range_row_is_none() {
428        let g = grid_2x2();
429        assert!(g.get(2, 0).is_none());
430        assert!(g.get(1_000_000, 1).is_none());
431    }
432
433    #[test]
434    fn get_extreme_row_single_column_no_overflow() {
435        // With cols == 1, `row * cols + col` == usize::MAX (no overflow) and
436        // resolves to an out-of-range slice index → None.
437        let single = DbRows {
438            columns: cols(&["only"]),
439            values: DbValueVec::from_vec(vec![DbValue::Integer(0)]),
440        };
441        assert!(single.get(usize::MAX, 0).is_none());
442    }
443
444    #[test]
445    fn get_on_empty_grid_is_none() {
446        let empty = DbRows {
447            columns: StringVec::from_vec(vec![]),
448            values: DbValueVec::from_vec(vec![]),
449        };
450        assert!(empty.get(0, 0).is_none());
451        assert!(empty.get(usize::MAX, 0).is_none());
452    }
453
454    #[test]
455    fn get_extreme_row_multi_column_never_yields_bogus_cell() {
456        // 2-column grid: `row * cols` = usize::MAX * 2 overflows usize. Under the
457        // default overflow-checked test profile this panics; in a wrapping
458        // (release) build it must still resolve to None, never a real cell.
459        // Guard with catch_unwind so the observation is non-fatal either way.
460        // NOTE: this documents a latent overflow in `DbRows::get` — see report.
461        let g = grid_2x2();
462        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
463            g.get(usize::MAX, 0).cloned()
464        }));
465        match outcome {
466            Ok(v) => assert!(v.is_none(), "overflowed index must not map to a real cell, got {v:?}"),
467            Err(_) => { /* overflow-check panic: latent bug, reported separately */ }
468        }
469    }
470
471    #[test]
472    fn get_largest_non_overflowing_row_is_none_not_a_panic() {
473        // With `cols == 2`, the largest row whose flat index cannot overflow is
474        // `usize::MAX / 2`: `row * 2 + 1 == usize::MAX` exactly. One row higher
475        // overflows (covered above); *this* one must resolve cleanly to `None` —
476        // no panic, and no wrap-around into a live cell.
477        let row = usize::MAX / 2;
478        assert_eq!(
479            row.checked_mul(2).and_then(|x| x.checked_add(1)),
480            Some(usize::MAX),
481            "precondition: this row is the exact non-overflowing boundary",
482        );
483        let g = grid_2x2();
484        assert!(g.get(row, 0).is_none());
485        assert!(g.get(row, 1).is_none());
486    }
487
488    // ---- DbValue: cross-variant equality invariants ------------------------
489
490    #[test]
491    fn eq_nan_never_equals_itself_but_survives_the_clone() {
492        // Derived PartialEq delegates to f64, so IEEE-754 NaN semantics must hold.
493        let nan = DbValue::Real(f64::NAN);
494        let nan_clone = nan.clone();
495        assert_ne!(nan, nan_clone, "NaN must not compare equal to itself");
496
497        let v = DbValueVec::from_vec(vec![DbValue::Real(f64::NAN)]);
498        let v_clone = v.clone();
499        assert_ne!(v, v_clone, "a vec holding NaN is not equal to its own clone");
500        // ...but the payload is still a NaN, not a corrupted bit pattern.
501        assert!(v_clone.as_slice()[0].as_real().unwrap().is_nan());
502    }
503
504    #[test]
505    fn eq_signed_zero_and_distinct_storage_classes() {
506        // IEEE: +0.0 == -0.0, even though the bits differ.
507        assert_eq!(DbValue::Real(0.0), DbValue::Real(-0.0));
508        assert_ne!(DbValue::Real(0.0).as_real().map(f64::to_bits), DbValue::Real(-0.0).as_real().map(f64::to_bits));
509
510        // Different storage classes never compare equal, however similar the payload.
511        assert_ne!(DbValue::Integer(1), DbValue::Real(1.0));
512        assert_ne!(DbValue::Null, DbValue::Integer(0));
513        assert_ne!(DbValue::Text(AzString::from_const_str("1")), DbValue::Integer(1));
514        assert_ne!(
515            DbValue::Text(AzString::from_const_str("ab")),
516            DbValue::Blob(U8Vec::from_vec(vec![b'a', b'b'])),
517            "Text and Blob with identical bytes are still distinct storage classes",
518        );
519        // Same variant, same payload → equal (sanity anchor for the assertions above).
520        assert_eq!(DbValue::Integer(i64::MIN), DbValue::Integer(i64::MIN));
521    }
522
523    // ---- DbValue::Blob: opaque to every typed getter -----------------------
524
525    #[test]
526    fn blob_is_opaque_to_every_getter_and_round_trips_bit_exact() {
527        // Non-UTF-8, embedded NULs, every byte value — 70 KB of it.
528        let bytes: Vec<u8> = (0..=255u8).cycle().take(70_000).collect();
529        let b = DbValue::Blob(U8Vec::from_vec(bytes.clone()));
530
531        // There is no `as_blob`, so every typed getter must decline — and a Blob
532        // is emphatically not NULL.
533        assert!(!b.is_null());
534        assert_eq!(b.as_integer(), None);
535        assert_eq!(b.as_real(), None);
536        assert!(b.as_text().is_none());
537
538        // Payload survives a clone byte-for-byte (no UTF-8 validation, no truncation).
539        match b.clone() {
540            DbValue::Blob(v) => assert_eq!(v.as_slice(), bytes.as_slice()),
541            other => panic!("clone changed the variant: {other:?}"),
542        }
543
544        // An empty Blob is still a Blob, not a Null.
545        let empty = DbValue::Blob(U8Vec::from_vec(vec![]));
546        assert!(!empty.is_null());
547        assert_eq!(empty.as_integer(), None);
548        assert_ne!(empty, DbValue::Null);
549    }
550
551    // ---- round-trip: Vec<DbValue> → DbValueVec → Vec<DbValue> --------------
552
553    /// Extreme-but-comparable payloads (no NaN — it would break `assert_eq!`).
554    fn extreme_values() -> Vec<DbValue> {
555        vec![
556            DbValue::Null,
557            DbValue::Integer(i64::MIN),
558            DbValue::Integer(i64::MAX),
559            DbValue::Real(f64::NEG_INFINITY),
560            DbValue::Real(f64::MIN_POSITIVE),
561            DbValue::Real(-0.0),
562            DbValue::Text(AzString::from(String::new())),
563            DbValue::Text(AzString::from("nul\u{0}\u{FEFF}émoji💥中文".to_string())),
564            DbValue::Blob(U8Vec::from_vec(vec![])),
565            DbValue::Blob(U8Vec::from_vec((0..=255u8).collect())),
566        ]
567    }
568
569    #[test]
570    fn dbvaluevec_round_trip_preserves_order_and_payloads() {
571        let original = extreme_values();
572        let encoded = DbValueVec::from_vec(original.clone());
573
574        assert_eq!(encoded.len(), original.len());
575        assert!(!encoded.is_empty());
576        assert_eq!(encoded.as_slice(), original.as_slice(), "decode(encode(v)) != v");
577
578        // Element-wise too, so a reordering/aliasing bug names the offending index.
579        for (i, (got, want)) in encoded.as_slice().iter().zip(original.iter()).enumerate() {
580            assert_eq!(got, want, "cell {i} changed across the vec round-trip");
581        }
582
583        // ...and the full ownership round-trip back out to a Rust Vec.
584        let decoded = DbValueVec::from_vec(original.clone()).into_library_owned_vec();
585        assert_eq!(decoded, original);
586    }
587
588    #[test]
589    fn dbvaluevec_empty_is_safe_to_read() {
590        // A null-ptr / zero-len vec must yield an empty slice, never a deref of 0x0.
591        for v in [DbValueVec::new(), DbValueVec::default(), DbValueVec::from_vec(vec![])] {
592            assert_eq!(v.len(), 0);
593            assert!(v.is_empty());
594            assert_eq!(v.as_slice(), &[] as &[DbValue]);
595            assert!(v.get(0).is_none());
596            assert!(v.get(usize::MAX).is_none());
597            assert!(v.c_get(0).is_none());
598            assert_eq!(v.iter().count(), 0);
599        }
600    }
601
602    #[test]
603    fn dbvaluevec_c_get_agrees_with_get_and_declines_out_of_range() {
604        let vals = extreme_values();
605        let v = DbValueVec::from_vec(vals.clone());
606        for (i, want) in vals.iter().enumerate() {
607            assert!(v.c_get(i).is_some(), "c_get({i}) should be Some");
608            assert_eq!(v.c_get(i).into_option().as_ref(), Some(want), "c_get({i})");
609            assert_eq!(v.get(i), Some(want), "get({i})");
610        }
611        // Past the end, and at the usize limit: None, no panic.
612        assert!(v.c_get(vals.len()).is_none());
613        assert!(v.c_get(usize::MAX).is_none());
614        assert!(v.get(usize::MAX).is_none());
615    }
616
617    #[test]
618    fn option_dbvalue_round_trips_through_option() {
619        let inner = DbValue::Text(AzString::from("💥".to_string()));
620
621        let some: OptionDbValue = Some(inner.clone()).into();
622        assert!(some.is_some());
623        assert!(!some.is_none());
624        assert_eq!(some.as_ref(), Some(&inner));
625        assert_eq!(Option::<DbValue>::from(some.clone()), Some(inner.clone()));
626        assert_eq!(some.into_option(), Some(inner));
627
628        let none: OptionDbValue = Option::<DbValue>::None.into();
629        assert!(none.is_none());
630        assert_eq!(none.into_option(), None);
631        assert_eq!(OptionDbValue::default().into_option(), None);
632
633        // A `Some(Null)` is NOT a `None` — the two nullities must not collapse.
634        let some_null: OptionDbValue = Some(DbValue::Null).into();
635        assert!(some_null.is_some());
636        assert_ne!(some_null, OptionDbValue::None);
637    }
638
639    // ---- DbRows: clone / equality ------------------------------------------
640
641    #[test]
642    fn dbrows_clone_is_deep_and_outlives_the_original() {
643        let rows = DbRows {
644            columns: cols(&["id", "name"]),
645            values: DbValueVec::from_vec(vec![
646                DbValue::Integer(-1),
647                DbValue::Text(AzString::from("héllo 💥".to_string())),
648            ]),
649        };
650        let copy = rows.clone();
651        assert_eq!(copy, rows);
652
653        // A shallow (pointer-aliasing) clone would leave `copy` dangling here —
654        // and a double-free would trip on the second drop.
655        drop(rows);
656        assert_eq!(copy.num_columns(), 2);
657        assert_eq!(copy.num_rows(), 1);
658        assert_eq!(copy.get(0, 0).and_then(DbValue::as_integer), Some(-1));
659        assert_eq!(
660            copy.get(0, 1).and_then(|v| v.as_text()).map(AzString::as_str),
661            Some("héllo 💥"),
662        );
663
664        // Cloning a clone keeps working (no destructor-state corruption).
665        let copy2 = copy.clone();
666        drop(copy);
667        assert_eq!(
668            copy2.get(0, 1).and_then(|v| v.as_text()).map(AzString::as_str),
669            Some("héllo 💥"),
670        );
671    }
672
673    #[test]
674    fn dbrows_equality_is_structural() {
675        assert_eq!(grid_2x2(), grid_2x2());
676
677        // Same cells, different column names → different result set.
678        let renamed = DbRows { columns: cols(&["a", "z"]), ..grid_2x2() };
679        assert_ne!(renamed, grid_2x2());
680
681        // Same cells in a different row order → different result set.
682        let reordered = DbRows {
683            columns: cols(&["a", "b"]),
684            values: DbValueVec::from_vec(vec![
685                DbValue::Integer(20),
686                DbValue::Integer(21),
687                DbValue::Integer(10),
688                DbValue::Integer(11),
689            ]),
690        };
691        assert_ne!(reordered, grid_2x2());
692
693        // Same flat cells, but 1 column instead of 2 → a different shape entirely.
694        let reshaped = DbRows { columns: cols(&["a"]), ..grid_2x2() };
695        assert_ne!(reshaped, grid_2x2());
696        assert_eq!(reshaped.num_rows(), 4);
697    }
698
699    #[test]
700    fn dbrows_from_default_collections_is_an_empty_grid() {
701        let rows = DbRows { columns: StringVec::default(), values: DbValueVec::default() };
702        assert_eq!(rows.num_columns(), 0);
703        assert_eq!(rows.num_rows(), 0);
704        assert!(rows.get(0, 0).is_none());
705        assert!(rows.get(usize::MAX, usize::MAX).is_none());
706        assert_eq!(
707            rows,
708            DbRows {
709                columns: StringVec::from_vec(vec![]),
710                values: DbValueVec::from_vec(vec![]),
711            },
712        );
713    }
714
715    // ---- DbRows: shape invariants over every small grid --------------------
716
717    fn grid(cols_n: usize, len: usize) -> (DbRows, Vec<DbValue>) {
718        let names: Vec<AzString> = (0..cols_n).map(|_| AzString::from_const_str("c")).collect();
719        let cells: Vec<DbValue> = (0..len).map(|i| DbValue::Integer(i as i64)).collect();
720        let rows = DbRows {
721            columns: StringVec::from_vec(names),
722            values: DbValueVec::from_vec(cells.clone()),
723        };
724        (rows, cells)
725    }
726
727    #[test]
728    fn get_agrees_with_the_row_major_flat_index_for_every_shape() {
729        for cols_n in 1..=5usize {
730            for len in 0..=17usize {
731                let (rows, cells) = grid(cols_n, len);
732                assert_eq!(rows.num_columns(), cols_n);
733                assert_eq!(rows.num_rows(), len / cols_n, "cols={cols_n} len={len}");
734
735                // Every in-range cell is exactly `values[row * cols + col]`.
736                for r in 0..rows.num_rows() {
737                    for c in 0..cols_n {
738                        assert_eq!(
739                            rows.get(r, c),
740                            Some(&cells[r * cols_n + c]),
741                            "({r},{c}) cols={cols_n} len={len}",
742                        );
743                    }
744                }
745
746                // The ragged tail: `get` reads the physically-present cells of a
747                // partial row even though `num_rows` doesn't count it, and returns
748                // None past the end. Either way — deterministic, never a panic.
749                let tail = rows.num_rows();
750                for c in 0..cols_n {
751                    assert_eq!(
752                        rows.get(tail, c),
753                        cells.get(tail * cols_n + c),
754                        "ragged tail ({tail},{c}) cols={cols_n} len={len}",
755                    );
756                }
757
758                // An out-of-range column is None regardless of the row.
759                assert!(rows.get(0, cols_n).is_none());
760                assert!(rows.get(rows.num_rows().saturating_sub(1), cols_n).is_none());
761            }
762        }
763    }
764
765    #[test]
766    fn num_rows_times_num_columns_never_exceeds_the_cell_count() {
767        for cols_n in 0..=6usize {
768            for len in 0..=20usize {
769                let (rows, _) = grid(cols_n, len);
770                let (r, c) = (rows.num_rows(), rows.num_columns());
771
772                let covered = r.checked_mul(c).expect("row×col count must not overflow");
773                assert!(covered <= len, "claims {covered} cells but only {len} exist");
774                if c > 0 {
775                    // At most one partial row may be dropped — never more.
776                    assert!(len - covered < c, "dropped a whole row: cols={c} len={len}");
777                }
778            }
779        }
780    }
781
782    #[test]
783    fn more_columns_than_values_yields_zero_rows_and_no_bogus_cells() {
784        let rows = DbRows {
785            columns: cols(&["a", "b", "c"]),
786            values: DbValueVec::from_vec(vec![DbValue::Integer(1), DbValue::Null]),
787        };
788        assert_eq!(rows.num_columns(), 3);
789        assert_eq!(rows.num_rows(), 0, "floor(2/3) == 0: no complete row exists");
790
791        // No complete row, but the two cells that physically exist still read back.
792        assert_eq!(rows.get(0, 0).and_then(DbValue::as_integer), Some(1));
793        assert!(matches!(rows.get(0, 1), Some(DbValue::Null)));
794        // ...and nothing past the end is invented.
795        assert!(rows.get(0, 2).is_none());
796        assert!(rows.get(1, 0).is_none());
797    }
798
799    #[test]
800    fn num_columns_counts_names_verbatim_including_duplicates_and_unicode() {
801        let rows = DbRows {
802            columns: StringVec::from_vec(vec![
803                AzString::from_const_str("dup"),
804                AzString::from_const_str("dup"), // duplicates are not deduped
805                AzString::from_const_str(""),    // an empty name still counts
806                AzString::from("列💥\u{0}".to_string()),
807            ]),
808            values: DbValueVec::from_vec(vec![]),
809        };
810        assert_eq!(rows.num_columns(), 4);
811        assert_eq!(rows.num_rows(), 0);
812        assert!(rows.get(0, 3).is_none()); // no values at all
813    }
814}