use azul_css::{AzString, StringVec, U8Vec};
#[repr(C, u8)]
#[derive(Debug, Clone, PartialEq)]
pub enum DbValue {
Null,
Integer(i64),
Real(f64),
Text(AzString),
Blob(U8Vec),
}
impl DbValue {
#[must_use] pub const fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[must_use] pub const fn as_integer(&self) -> Option<i64> {
if let Self::Integer(i) = self {
Some(*i)
} else {
None
}
}
#[must_use] pub const fn as_real(&self) -> Option<f64> {
if let Self::Real(r) = self {
Some(*r)
} else {
None
}
}
#[must_use] pub const fn as_text(&self) -> Option<&AzString> {
if let Self::Text(t) = self {
Some(t)
} else {
None
}
}
}
impl_vec!(
DbValue,
DbValueVec,
DbValueVecDestructor,
DbValueVecDestructorType,
DbValueVecSlice,
OptionDbValue
);
impl_vec_debug!(DbValue, DbValueVec);
impl_vec_clone!(DbValue, DbValueVec, DbValueVecDestructor);
impl_vec_partialeq!(DbValue, DbValueVec);
impl_option!(DbValue, OptionDbValue, copy = false, [Debug, Clone, PartialEq]);
#[repr(C)]
#[derive(Debug, Clone, PartialEq)]
pub struct DbRows {
pub columns: StringVec,
pub values: DbValueVec,
}
impl DbRows {
#[must_use] pub fn num_columns(&self) -> usize {
self.columns.as_ref().len()
}
#[must_use] pub fn num_rows(&self) -> usize {
let cols = self.num_columns();
if cols == 0 {
0
} else {
self.values.as_ref().len() / cols
}
}
#[must_use] pub fn get(&self, row: usize, col: usize) -> Option<&DbValue> {
let cols = self.num_columns();
if col >= cols {
return None;
}
self.values.as_ref().get(row * cols + col)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dbvalue_accessors() {
assert!(DbValue::Null.is_null());
assert_eq!(DbValue::Integer(7).as_integer(), Some(7));
assert_eq!(DbValue::Real(1.5).as_real(), Some(1.5));
assert_eq!(
DbValue::Text(AzString::from_const_str("hi")).as_text().map(AzString::as_str),
Some("hi")
);
assert_eq!(DbValue::Null.as_integer(), None);
assert!(!DbValue::Integer(0).is_null());
}
#[test]
fn dbrows_indexing() {
let columns = StringVec::from_vec(vec![
AzString::from_const_str("id"),
AzString::from_const_str("name"),
]);
let values = DbValueVec::from_vec(vec![
DbValue::Integer(1),
DbValue::Text(AzString::from_const_str("alice")),
DbValue::Integer(2),
DbValue::Text(AzString::from_const_str("bob")),
]);
let rows = DbRows { columns, values };
assert_eq!(rows.num_columns(), 2);
assert_eq!(rows.num_rows(), 2);
assert_eq!(rows.get(0, 0).and_then(DbValue::as_integer), Some(1));
assert_eq!(
rows.get(1, 1).and_then(|v| v.as_text()).map(AzString::as_str),
Some("bob")
);
assert!(rows.get(0, 2).is_none());
assert!(rows.get(2, 0).is_none());
}
#[test]
fn dbrows_empty() {
let rows = DbRows {
columns: StringVec::from_vec(vec![]),
values: DbValueVec::from_vec(vec![]),
};
assert_eq!(rows.num_columns(), 0);
assert_eq!(rows.num_rows(), 0);
assert!(rows.get(0, 0).is_none());
}
}
#[cfg(test)]
mod autotest_generated {
use super::*;
fn cols(names: &[&'static str]) -> StringVec {
StringVec::from_vec(names.iter().copied().map(AzString::from_const_str).collect())
}
fn grid_2x2() -> DbRows {
DbRows {
columns: cols(&["a", "b"]),
values: DbValueVec::from_vec(vec![
DbValue::Integer(10),
DbValue::Integer(11),
DbValue::Integer(20),
DbValue::Integer(21),
]),
}
}
fn all_variants() -> Vec<DbValue> {
vec![
DbValue::Null,
DbValue::Integer(0),
DbValue::Real(0.0),
DbValue::Text(AzString::from_const_str("t")),
DbValue::Blob(U8Vec::from_vec(vec![1, 2, 3])),
]
}
#[test]
fn is_null_basic_true_false() {
assert!(DbValue::Null.is_null());
assert!(!DbValue::Integer(0).is_null());
}
#[test]
fn is_null_only_null_variant_is_null() {
for v in all_variants() {
let expect = matches!(v, DbValue::Null);
assert_eq!(v.is_null(), expect, "is_null mismatch for {v:?}");
}
assert!(!DbValue::Integer(i64::MIN).is_null());
assert!(!DbValue::Real(f64::NAN).is_null());
assert!(!DbValue::Blob(U8Vec::from_vec(vec![])).is_null());
}
#[test]
fn is_null_is_const_evaluable() {
const NULL_IS_NULL: bool = DbValue::Null.is_null();
const INT_IS_NULL: bool = DbValue::Integer(9).is_null();
assert!(NULL_IS_NULL);
assert!(!INT_IS_NULL);
}
#[test]
fn as_integer_extremes_round_trip() {
for &i in &[0i64, 1, -1, i64::MIN, i64::MIN + 1, i64::MAX - 1, i64::MAX] {
assert_eq!(DbValue::Integer(i).as_integer(), Some(i), "round-trip {i}");
}
}
#[test]
fn as_integer_wrong_variant_is_none() {
for v in all_variants() {
if matches!(v, DbValue::Integer(_)) {
continue;
}
assert_eq!(v.as_integer(), None, "expected None for {v:?}");
}
}
#[test]
fn as_integer_is_const_evaluable() {
const MIN_INT: Option<i64> = DbValue::Integer(i64::MIN).as_integer();
assert_eq!(MIN_INT, Some(i64::MIN));
}
#[test]
fn as_real_extremes_round_trip_bit_exact() {
for &r in &[
0.0f64,
-0.0,
1.0,
-1.0,
f64::MIN,
f64::MAX,
f64::MIN_POSITIVE,
f64::EPSILON,
f64::INFINITY,
f64::NEG_INFINITY,
] {
assert_eq!(
DbValue::Real(r).as_real().map(f64::to_bits),
Some(r.to_bits()),
"bit-exact round-trip for {r:?}",
);
}
}
#[test]
fn as_real_nan_is_preserved_not_lost() {
let got = DbValue::Real(f64::NAN).as_real();
assert!(got.is_some());
assert!(got.unwrap().is_nan());
}
#[test]
fn as_real_negative_zero_keeps_sign() {
let got = DbValue::Real(-0.0).as_real().unwrap();
assert_eq!(got, 0.0);
assert!(got.is_sign_negative(), "-0.0 must stay negative-signed");
}
#[test]
fn as_real_wrong_variant_is_none() {
for v in all_variants() {
if matches!(v, DbValue::Real(_)) {
continue;
}
assert_eq!(v.as_real(), None, "expected None for {v:?}");
}
}
#[test]
fn as_text_empty_unicode_and_huge() {
assert_eq!(
DbValue::Text(AzString::from_const_str("")).as_text().map(AzString::as_str),
Some(""),
);
let tricky = "áé💥🔥\u{0}\u{FEFF}中文";
let v = DbValue::Text(AzString::from(tricky.to_string()));
assert_eq!(v.as_text().map(AzString::as_str), Some(tricky));
let huge = "x".repeat(200_000);
let v = DbValue::Text(AzString::from(huge.clone()));
assert_eq!(v.as_text().map(|s| s.as_str().len()), Some(huge.len()));
}
#[test]
fn as_text_wrong_variant_is_none() {
for v in all_variants() {
if matches!(v, DbValue::Text(_)) {
continue;
}
assert!(v.as_text().is_none(), "expected None for {v:?}");
}
}
#[test]
fn num_columns_empty_and_many() {
let empty = DbRows {
columns: StringVec::from_vec(vec![]),
values: DbValueVec::from_vec(vec![]),
};
assert_eq!(empty.num_columns(), 0);
let names: Vec<AzString> =
(0..1000).map(|_| AzString::from_const_str("c")).collect();
let wide = DbRows {
columns: StringVec::from_vec(names),
values: DbValueVec::from_vec(vec![]),
};
assert_eq!(wide.num_columns(), 1000);
}
#[test]
fn num_rows_zero_columns_never_divides_by_zero() {
let rows = DbRows {
columns: StringVec::from_vec(vec![]),
values: DbValueVec::from_vec(vec![DbValue::Null, DbValue::Integer(1)]),
};
assert_eq!(rows.num_rows(), 0);
assert!(rows.get(0, 0).is_none());
}
#[test]
fn num_rows_exact_and_ragged_truncates() {
assert_eq!(grid_2x2().num_rows(), 2);
let ragged = DbRows {
columns: cols(&["a", "b"]),
values: DbValueVec::from_vec(vec![
DbValue::Integer(1),
DbValue::Integer(2),
DbValue::Integer(3),
]),
};
assert_eq!(ragged.num_rows(), 1);
assert!(ragged.get(1, 1).is_none());
let single = DbRows {
columns: cols(&["only"]),
values: DbValueVec::from_vec(vec![
DbValue::Integer(0),
DbValue::Integer(1),
DbValue::Integer(2),
]),
};
assert_eq!(single.num_rows(), 3);
}
#[test]
fn get_zero_and_all_in_range_cells() {
let g = grid_2x2();
assert_eq!(g.get(0, 0).and_then(DbValue::as_integer), Some(10));
assert_eq!(g.get(0, 1).and_then(DbValue::as_integer), Some(11));
assert_eq!(g.get(1, 0).and_then(DbValue::as_integer), Some(20));
assert_eq!(g.get(1, 1).and_then(DbValue::as_integer), Some(21));
}
#[test]
fn get_out_of_range_column_is_none() {
let g = grid_2x2();
assert!(g.get(0, 2).is_none()); assert!(g.get(0, usize::MAX).is_none());
assert!(g.get(usize::MAX, usize::MAX).is_none());
}
#[test]
fn get_out_of_range_row_is_none() {
let g = grid_2x2();
assert!(g.get(2, 0).is_none());
assert!(g.get(1_000_000, 1).is_none());
}
#[test]
fn get_extreme_row_single_column_no_overflow() {
let single = DbRows {
columns: cols(&["only"]),
values: DbValueVec::from_vec(vec![DbValue::Integer(0)]),
};
assert!(single.get(usize::MAX, 0).is_none());
}
#[test]
fn get_on_empty_grid_is_none() {
let empty = DbRows {
columns: StringVec::from_vec(vec![]),
values: DbValueVec::from_vec(vec![]),
};
assert!(empty.get(0, 0).is_none());
assert!(empty.get(usize::MAX, 0).is_none());
}
#[test]
fn get_extreme_row_multi_column_never_yields_bogus_cell() {
let g = grid_2x2();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
g.get(usize::MAX, 0).cloned()
}));
match outcome {
Ok(v) => assert!(v.is_none(), "overflowed index must not map to a real cell, got {v:?}"),
Err(_) => { }
}
}
#[test]
fn get_largest_non_overflowing_row_is_none_not_a_panic() {
let row = usize::MAX / 2;
assert_eq!(
row.checked_mul(2).and_then(|x| x.checked_add(1)),
Some(usize::MAX),
"precondition: this row is the exact non-overflowing boundary",
);
let g = grid_2x2();
assert!(g.get(row, 0).is_none());
assert!(g.get(row, 1).is_none());
}
#[test]
fn eq_nan_never_equals_itself_but_survives_the_clone() {
let nan = DbValue::Real(f64::NAN);
let nan_clone = nan.clone();
assert_ne!(nan, nan_clone, "NaN must not compare equal to itself");
let v = DbValueVec::from_vec(vec![DbValue::Real(f64::NAN)]);
let v_clone = v.clone();
assert_ne!(v, v_clone, "a vec holding NaN is not equal to its own clone");
assert!(v_clone.as_slice()[0].as_real().unwrap().is_nan());
}
#[test]
fn eq_signed_zero_and_distinct_storage_classes() {
assert_eq!(DbValue::Real(0.0), DbValue::Real(-0.0));
assert_ne!(DbValue::Real(0.0).as_real().map(f64::to_bits), DbValue::Real(-0.0).as_real().map(f64::to_bits));
assert_ne!(DbValue::Integer(1), DbValue::Real(1.0));
assert_ne!(DbValue::Null, DbValue::Integer(0));
assert_ne!(DbValue::Text(AzString::from_const_str("1")), DbValue::Integer(1));
assert_ne!(
DbValue::Text(AzString::from_const_str("ab")),
DbValue::Blob(U8Vec::from_vec(vec![b'a', b'b'])),
"Text and Blob with identical bytes are still distinct storage classes",
);
assert_eq!(DbValue::Integer(i64::MIN), DbValue::Integer(i64::MIN));
}
#[test]
fn blob_is_opaque_to_every_getter_and_round_trips_bit_exact() {
let bytes: Vec<u8> = (0..=255u8).cycle().take(70_000).collect();
let b = DbValue::Blob(U8Vec::from_vec(bytes.clone()));
assert!(!b.is_null());
assert_eq!(b.as_integer(), None);
assert_eq!(b.as_real(), None);
assert!(b.as_text().is_none());
match b.clone() {
DbValue::Blob(v) => assert_eq!(v.as_slice(), bytes.as_slice()),
other => panic!("clone changed the variant: {other:?}"),
}
let empty = DbValue::Blob(U8Vec::from_vec(vec![]));
assert!(!empty.is_null());
assert_eq!(empty.as_integer(), None);
assert_ne!(empty, DbValue::Null);
}
fn extreme_values() -> Vec<DbValue> {
vec![
DbValue::Null,
DbValue::Integer(i64::MIN),
DbValue::Integer(i64::MAX),
DbValue::Real(f64::NEG_INFINITY),
DbValue::Real(f64::MIN_POSITIVE),
DbValue::Real(-0.0),
DbValue::Text(AzString::from(String::new())),
DbValue::Text(AzString::from("nul\u{0}\u{FEFF}émoji💥中文".to_string())),
DbValue::Blob(U8Vec::from_vec(vec![])),
DbValue::Blob(U8Vec::from_vec((0..=255u8).collect())),
]
}
#[test]
fn dbvaluevec_round_trip_preserves_order_and_payloads() {
let original = extreme_values();
let encoded = DbValueVec::from_vec(original.clone());
assert_eq!(encoded.len(), original.len());
assert!(!encoded.is_empty());
assert_eq!(encoded.as_slice(), original.as_slice(), "decode(encode(v)) != v");
for (i, (got, want)) in encoded.as_slice().iter().zip(original.iter()).enumerate() {
assert_eq!(got, want, "cell {i} changed across the vec round-trip");
}
let decoded = DbValueVec::from_vec(original.clone()).into_library_owned_vec();
assert_eq!(decoded, original);
}
#[test]
fn dbvaluevec_empty_is_safe_to_read() {
for v in [DbValueVec::new(), DbValueVec::default(), DbValueVec::from_vec(vec![])] {
assert_eq!(v.len(), 0);
assert!(v.is_empty());
assert_eq!(v.as_slice(), &[] as &[DbValue]);
assert!(v.get(0).is_none());
assert!(v.get(usize::MAX).is_none());
assert!(v.c_get(0).is_none());
assert_eq!(v.iter().count(), 0);
}
}
#[test]
fn dbvaluevec_c_get_agrees_with_get_and_declines_out_of_range() {
let vals = extreme_values();
let v = DbValueVec::from_vec(vals.clone());
for (i, want) in vals.iter().enumerate() {
assert!(v.c_get(i).is_some(), "c_get({i}) should be Some");
assert_eq!(v.c_get(i).into_option().as_ref(), Some(want), "c_get({i})");
assert_eq!(v.get(i), Some(want), "get({i})");
}
assert!(v.c_get(vals.len()).is_none());
assert!(v.c_get(usize::MAX).is_none());
assert!(v.get(usize::MAX).is_none());
}
#[test]
fn option_dbvalue_round_trips_through_option() {
let inner = DbValue::Text(AzString::from("💥".to_string()));
let some: OptionDbValue = Some(inner.clone()).into();
assert!(some.is_some());
assert!(!some.is_none());
assert_eq!(some.as_ref(), Some(&inner));
assert_eq!(Option::<DbValue>::from(some.clone()), Some(inner.clone()));
assert_eq!(some.into_option(), Some(inner));
let none: OptionDbValue = Option::<DbValue>::None.into();
assert!(none.is_none());
assert_eq!(none.into_option(), None);
assert_eq!(OptionDbValue::default().into_option(), None);
let some_null: OptionDbValue = Some(DbValue::Null).into();
assert!(some_null.is_some());
assert_ne!(some_null, OptionDbValue::None);
}
#[test]
fn dbrows_clone_is_deep_and_outlives_the_original() {
let rows = DbRows {
columns: cols(&["id", "name"]),
values: DbValueVec::from_vec(vec![
DbValue::Integer(-1),
DbValue::Text(AzString::from("héllo 💥".to_string())),
]),
};
let copy = rows.clone();
assert_eq!(copy, rows);
drop(rows);
assert_eq!(copy.num_columns(), 2);
assert_eq!(copy.num_rows(), 1);
assert_eq!(copy.get(0, 0).and_then(DbValue::as_integer), Some(-1));
assert_eq!(
copy.get(0, 1).and_then(|v| v.as_text()).map(AzString::as_str),
Some("héllo 💥"),
);
let copy2 = copy.clone();
drop(copy);
assert_eq!(
copy2.get(0, 1).and_then(|v| v.as_text()).map(AzString::as_str),
Some("héllo 💥"),
);
}
#[test]
fn dbrows_equality_is_structural() {
assert_eq!(grid_2x2(), grid_2x2());
let renamed = DbRows { columns: cols(&["a", "z"]), ..grid_2x2() };
assert_ne!(renamed, grid_2x2());
let reordered = DbRows {
columns: cols(&["a", "b"]),
values: DbValueVec::from_vec(vec![
DbValue::Integer(20),
DbValue::Integer(21),
DbValue::Integer(10),
DbValue::Integer(11),
]),
};
assert_ne!(reordered, grid_2x2());
let reshaped = DbRows { columns: cols(&["a"]), ..grid_2x2() };
assert_ne!(reshaped, grid_2x2());
assert_eq!(reshaped.num_rows(), 4);
}
#[test]
fn dbrows_from_default_collections_is_an_empty_grid() {
let rows = DbRows { columns: StringVec::default(), values: DbValueVec::default() };
assert_eq!(rows.num_columns(), 0);
assert_eq!(rows.num_rows(), 0);
assert!(rows.get(0, 0).is_none());
assert!(rows.get(usize::MAX, usize::MAX).is_none());
assert_eq!(
rows,
DbRows {
columns: StringVec::from_vec(vec![]),
values: DbValueVec::from_vec(vec![]),
},
);
}
fn grid(cols_n: usize, len: usize) -> (DbRows, Vec<DbValue>) {
let names: Vec<AzString> = (0..cols_n).map(|_| AzString::from_const_str("c")).collect();
let cells: Vec<DbValue> = (0..len).map(|i| DbValue::Integer(i as i64)).collect();
let rows = DbRows {
columns: StringVec::from_vec(names),
values: DbValueVec::from_vec(cells.clone()),
};
(rows, cells)
}
#[test]
fn get_agrees_with_the_row_major_flat_index_for_every_shape() {
for cols_n in 1..=5usize {
for len in 0..=17usize {
let (rows, cells) = grid(cols_n, len);
assert_eq!(rows.num_columns(), cols_n);
assert_eq!(rows.num_rows(), len / cols_n, "cols={cols_n} len={len}");
for r in 0..rows.num_rows() {
for c in 0..cols_n {
assert_eq!(
rows.get(r, c),
Some(&cells[r * cols_n + c]),
"({r},{c}) cols={cols_n} len={len}",
);
}
}
let tail = rows.num_rows();
for c in 0..cols_n {
assert_eq!(
rows.get(tail, c),
cells.get(tail * cols_n + c),
"ragged tail ({tail},{c}) cols={cols_n} len={len}",
);
}
assert!(rows.get(0, cols_n).is_none());
assert!(rows.get(rows.num_rows().saturating_sub(1), cols_n).is_none());
}
}
}
#[test]
fn num_rows_times_num_columns_never_exceeds_the_cell_count() {
for cols_n in 0..=6usize {
for len in 0..=20usize {
let (rows, _) = grid(cols_n, len);
let (r, c) = (rows.num_rows(), rows.num_columns());
let covered = r.checked_mul(c).expect("row×col count must not overflow");
assert!(covered <= len, "claims {covered} cells but only {len} exist");
if c > 0 {
assert!(len - covered < c, "dropped a whole row: cols={c} len={len}");
}
}
}
}
#[test]
fn more_columns_than_values_yields_zero_rows_and_no_bogus_cells() {
let rows = DbRows {
columns: cols(&["a", "b", "c"]),
values: DbValueVec::from_vec(vec![DbValue::Integer(1), DbValue::Null]),
};
assert_eq!(rows.num_columns(), 3);
assert_eq!(rows.num_rows(), 0, "floor(2/3) == 0: no complete row exists");
assert_eq!(rows.get(0, 0).and_then(DbValue::as_integer), Some(1));
assert!(matches!(rows.get(0, 1), Some(DbValue::Null)));
assert!(rows.get(0, 2).is_none());
assert!(rows.get(1, 0).is_none());
}
#[test]
fn num_columns_counts_names_verbatim_including_duplicates_and_unicode() {
let rows = DbRows {
columns: StringVec::from_vec(vec![
AzString::from_const_str("dup"),
AzString::from_const_str("dup"), AzString::from_const_str(""), AzString::from("列💥\u{0}".to_string()),
]),
values: DbValueVec::from_vec(vec![]),
};
assert_eq!(rows.num_columns(), 4);
assert_eq!(rows.num_rows(), 0);
assert!(rows.get(0, 3).is_none()); }
}