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
14
    #[must_use] pub const fn is_null(&self) -> bool {
36
14
        matches!(self, Self::Null)
37
14
    }
38
22
    #[must_use] pub const fn as_integer(&self) -> Option<i64> {
39
22
        if let Self::Integer(i) = self {
40
15
            Some(*i)
41
        } else {
42
7
            None
43
        }
44
22
    }
45
21
    #[must_use] pub const fn as_real(&self) -> Option<f64> {
46
21
        if let Self::Real(r) = self {
47
16
            Some(*r)
48
        } else {
49
5
            None
50
        }
51
21
    }
52
12
    #[must_use] pub const fn as_text(&self) -> Option<&AzString> {
53
12
        if let Self::Text(t) = self {
54
7
            Some(t)
55
        } else {
56
5
            None
57
        }
58
12
    }
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
1925
    #[must_use] pub fn num_columns(&self) -> usize {
89
1925
        self.columns.as_ref().len()
90
1925
    }
91
    /// Number of result rows (`0` when there are no columns).
92
518
    #[must_use] pub fn num_rows(&self) -> usize {
93
518
        let cols = self.num_columns();
94
518
        if cols == 0 {
95
24
            0
96
        } else {
97
494
            self.values.as_ref().len() / cols
98
        }
99
518
    }
100
    /// The cell at `(row, col)`, or `None` if out of range.
101
1162
    #[must_use] pub fn get(&self, row: usize, col: usize) -> Option<&DbValue> {
102
1162
        let cols = self.num_columns();
103
1162
        if col >= cols {
104
190
            return None;
105
972
        }
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
972
        let idx = row.checked_mul(cols)?.checked_add(col)?;
110
971
        self.values.as_ref().get(idx)
111
1162
    }
112
}
113

            
114
#[cfg(test)]
115
mod tests {
116
    use super::*;
117

            
118
    #[test]
119
1
    fn dbvalue_accessors() {
120
1
        assert!(DbValue::Null.is_null());
121
1
        assert_eq!(DbValue::Integer(7).as_integer(), Some(7));
122
1
        assert_eq!(DbValue::Real(1.5).as_real(), Some(1.5));
123
1
        assert_eq!(
124
1
            DbValue::Text(AzString::from_const_str("hi")).as_text().map(AzString::as_str),
125
            Some("hi")
126
        );
127
        // Wrong-variant accessors return None.
128
1
        assert_eq!(DbValue::Null.as_integer(), None);
129
1
        assert!(!DbValue::Integer(0).is_null());
130
1
    }
131

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

            
147
1
        assert_eq!(rows.num_columns(), 2);
148
1
        assert_eq!(rows.num_rows(), 2);
149
1
        assert_eq!(rows.get(0, 0).and_then(DbValue::as_integer), Some(1));
150
1
        assert_eq!(
151
1
            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
1
        assert!(rows.get(0, 2).is_none());
156
1
        assert!(rows.get(2, 0).is_none());
157
1
    }
158

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

            
171
#[cfg(test)]
172
mod 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
}