1use azul_css::{AzString, StringVec, U8Vec};
15
16#[repr(C, u8)]
20#[derive(Debug, Clone, PartialEq)]
21pub enum DbValue {
22 Null,
24 Integer(i64),
26 Real(f64),
28 Text(AzString),
30 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#[repr(C)]
78#[derive(Debug, Clone, PartialEq)]
79pub struct DbRows {
80 pub columns: StringVec,
82 pub values: DbValueVec,
84}
85
86impl DbRows {
87 #[must_use] pub fn num_columns(&self) -> usize {
89 self.columns.as_ref().len()
90 }
91 #[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 #[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 assert_eq!(DbValue::Null.as_integer(), None);
125 assert!(!DbValue::Integer(0).is_null());
126 }
127
128 #[test]
129 fn dbrows_indexing() {
130 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 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 use super::*;
176
177 fn cols(names: &[&'static str]) -> StringVec {
180 StringVec::from_vec(names.iter().copied().map(AzString::from_const_str).collect())
181 }
182
183 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 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 #[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 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 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 #[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 #[test]
265 fn as_real_extremes_round_trip_bit_exact() {
266 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 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 #[test]
315 fn as_text_empty_unicode_and_huge() {
316 assert_eq!(
318 DbValue::Text(AzString::from_const_str("")).as_text().map(AzString::as_str),
319 Some(""),
320 );
321 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 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 #[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 #[test]
363 fn num_rows_zero_columns_never_divides_by_zero() {
364 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 assert!(rows.get(0, 0).is_none());
373 }
374
375 #[test]
376 fn num_rows_exact_and_ragged_truncates() {
377 assert_eq!(grid_2x2().num_rows(), 2);
379
380 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 assert!(ragged.get(1, 1).is_none());
392
393 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 #[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()); assert!(g.get(0, usize::MAX).is_none());
422 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 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 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(_) => { }
468 }
469 }
470
471 #[test]
472 fn get_largest_non_overflowing_row_is_none_not_a_panic() {
473 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 #[test]
491 fn eq_nan_never_equals_itself_but_survives_the_clone() {
492 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 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 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 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 assert_eq!(DbValue::Integer(i64::MIN), DbValue::Integer(i64::MIN));
521 }
522
523 #[test]
526 fn blob_is_opaque_to_every_getter_and_round_trips_bit_exact() {
527 let bytes: Vec<u8> = (0..=255u8).cycle().take(70_000).collect();
529 let b = DbValue::Blob(U8Vec::from_vec(bytes.clone()));
530
531 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 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 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 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 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 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 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 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 let some_null: OptionDbValue = Some(DbValue::Null).into();
635 assert!(some_null.is_some());
636 assert_ne!(some_null, OptionDbValue::None);
637 }
638
639 #[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 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 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 let renamed = DbRows { columns: cols(&["a", "z"]), ..grid_2x2() };
679 assert_ne!(renamed, grid_2x2());
680
681 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 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 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 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 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 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 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 assert_eq!(rows.get(0, 0).and_then(DbValue::as_integer), Some(1));
793 assert!(matches!(rows.get(0, 1), Some(DbValue::Null)));
794 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"), AzString::from_const_str(""), 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()); }
814}