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