a020ac669f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
229 lines
7.6 KiB
Rust
229 lines
7.6 KiB
Rust
//! What `Incremental` costs in operations, at two sizes of one deterministic table.
|
|
//! Evidence: `context/test-evidence/improvise-incremental.md`. Counts, never time.
|
|
|
|
use improvise_core::formula::ast::AggFunc;
|
|
use improvise_core::model::cell::{CellKey, CellValue};
|
|
use improvise_core::model::{Aggregation, Model};
|
|
use improvise_incremental::{Derived, Edit, Incremental, Maintained, Meter, Reading};
|
|
|
|
fn cell(r: usize, p: usize, m: usize) -> CellKey {
|
|
CellKey::new(vec![
|
|
("Region".into(), format!("r{r}")),
|
|
("Product".into(), format!("p{p}")),
|
|
("Month".into(), format!("m{m}")),
|
|
("_Measure".into(), "Revenue".into()),
|
|
])
|
|
}
|
|
|
|
/// `n` regions by `n` products by 6 months, one measure, values from a fixed sequence.
|
|
fn table(n: usize, func: AggFunc) -> Model {
|
|
let mut model = Model::new("table");
|
|
for (cat, prefix, count) in [("Region", "r", n), ("Product", "p", n), ("Month", "m", 6)] {
|
|
model.add_category(cat).unwrap();
|
|
for i in 0..count {
|
|
model
|
|
.category_mut(cat)
|
|
.unwrap()
|
|
.add_item(format!("{prefix}{i}"));
|
|
}
|
|
}
|
|
model.category_mut("_Measure").unwrap().add_item("Revenue");
|
|
model.measure_agg.insert("Revenue".into(), func);
|
|
let mut seed = 1u64;
|
|
for r in 0..n {
|
|
for p in 0..n {
|
|
for m in 0..6 {
|
|
seed = seed
|
|
.wrapping_mul(6364136223846793005)
|
|
.wrapping_add(1442695040888963407);
|
|
let v = ((seed >> 33) % 1000) as f64;
|
|
model.set_cell(cell(r, p, m), CellValue::Number(v));
|
|
}
|
|
}
|
|
}
|
|
model
|
|
}
|
|
|
|
fn region_partial() -> CellKey {
|
|
CellKey::new(vec![
|
|
("Region".into(), "r1".into()),
|
|
("_Measure".into(), "Revenue".into()),
|
|
])
|
|
}
|
|
|
|
fn product_partial() -> CellKey {
|
|
CellKey::new(vec![
|
|
("Product".into(), "p2".into()),
|
|
("_Measure".into(), "Revenue".into()),
|
|
])
|
|
}
|
|
|
|
fn since(before: Meter, after: Meter) -> Meter {
|
|
Meter {
|
|
updates: after.updates - before.updates,
|
|
reads: after.reads - before.reads,
|
|
builds: after.builds - before.builds,
|
|
built_cells: after.built_cells - before.built_cells,
|
|
stems: after.stems - before.stems,
|
|
formula_cells: after.formula_cells - before.formula_cells,
|
|
rebuilds: after.rebuilds - before.rebuilds,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_group_read_comes_from_a_maintained_group() {
|
|
let model = table(6, AggFunc::Min);
|
|
let mut backend = Incremental::new(model.clone(), vec![]);
|
|
let want = backend.raw_fold(®ion_partial());
|
|
assert!(matches!(want, Some(CellValue::Number(_))));
|
|
assert_eq!(backend.meter().builds, 1, "the first read builds its shape");
|
|
assert_eq!(backend.meter().reads, 1);
|
|
backend.apply(&Edit::SetCell(cell(1, 0, 0), CellValue::Number(-5.0)));
|
|
assert_eq!(
|
|
backend.raw_fold(®ion_partial()),
|
|
Some(CellValue::Number(-5.0)),
|
|
"the new minimum comes from the delta"
|
|
);
|
|
assert_eq!(
|
|
backend.meter().builds,
|
|
1,
|
|
"and no second pass over the store"
|
|
);
|
|
}
|
|
|
|
/// The operations one overwrite costs once `shapes` category sets are maintained.
|
|
fn overwrite_cost(n: usize, shapes: usize, func: AggFunc) -> (Meter, usize) {
|
|
let mut backend = Incremental::new(table(n, func), vec![]);
|
|
let partials = [region_partial(), product_partial()];
|
|
for partial in partials.iter().take(shapes) {
|
|
backend.raw_fold(partial);
|
|
}
|
|
let before = backend.meter();
|
|
backend.apply(&Edit::SetCell(cell(1, 2, 3), CellValue::Number(2000.0)));
|
|
let cost = since(before, backend.meter());
|
|
assert!(backend.raw_fold(®ion_partial()).is_some());
|
|
(cost, backend.shape_count())
|
|
}
|
|
|
|
#[test]
|
|
fn an_overwrite_costs_two_updates_per_shape_whatever_the_table_size() {
|
|
let funcs = [
|
|
AggFunc::Max,
|
|
AggFunc::Min,
|
|
AggFunc::Count,
|
|
AggFunc::Sum,
|
|
AggFunc::Avg,
|
|
];
|
|
for func in funcs {
|
|
for shapes in [1, 2] {
|
|
let (small, built) = overwrite_cost(6, shapes, func.clone());
|
|
let (large, _) = overwrite_cost(12, shapes, func.clone());
|
|
assert!(built >= shapes);
|
|
let updates = 2 * shapes as u64;
|
|
assert_eq!(
|
|
small.updates, updates,
|
|
"{func:?}: retract and insert, per shape"
|
|
);
|
|
assert_eq!(
|
|
small, large,
|
|
"{func:?}: 216 cells and 864 cells cost the same"
|
|
);
|
|
assert_eq!(large.builds, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_sum_read_after_a_write_comes_from_the_delta_and_equals_a_fresh_sum() {
|
|
let mut backend = Incremental::new(table(6, AggFunc::Sum), vec![]);
|
|
backend.raw_fold(®ion_partial());
|
|
backend.apply(&Edit::SetCell(cell(1, 0, 0), CellValue::Number(0.1)));
|
|
let built = backend.meter().builds;
|
|
let got = backend.raw_fold(®ion_partial());
|
|
assert_eq!(
|
|
backend.meter().builds,
|
|
built,
|
|
"no second pass over the store"
|
|
);
|
|
let fresh = Incremental::new(backend.model().clone(), vec![]);
|
|
assert_eq!(got, fresh.raw_fold(®ion_partial()));
|
|
}
|
|
|
|
#[test]
|
|
fn a_read_visits_one_group_where_eager_visits_every_cell_it_covers() {
|
|
for n in [6, 12] {
|
|
let model = table(n, AggFunc::Min);
|
|
let backend = Incremental::new(model.clone(), vec![]);
|
|
backend.raw_fold(®ion_partial());
|
|
let before = backend.meter();
|
|
backend.raw_fold(®ion_partial());
|
|
assert_eq!(since(before, backend.meter()).reads, 1, "n = {n}");
|
|
let eager_visits = model.data.matching_values(®ion_partial().0).len();
|
|
assert_eq!(
|
|
eager_visits,
|
|
n * 6,
|
|
"eager folds a region's {} cells",
|
|
n * 6
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_collapsed_group_reads_one_group_per_member_it_covers() {
|
|
let mut model = table(6, AggFunc::Count);
|
|
let region = model.category_mut("Region").unwrap();
|
|
region.add_group(improvise_core::model::category::Group::new("Coast"));
|
|
for item in ["r0", "r1", "r2"] {
|
|
region.items.get_mut(item).unwrap().group = Some("Coast".into());
|
|
}
|
|
let backend = Incremental::new(model, vec![]);
|
|
let coast = CellKey::new(vec![
|
|
("Region".into(), "Coast".into()),
|
|
("_Measure".into(), "Revenue".into()),
|
|
]);
|
|
backend.raw_fold(&coast);
|
|
let before = backend.meter();
|
|
assert_eq!(
|
|
backend.raw_fold(&coast),
|
|
Some(CellValue::Number(3.0 * 6.0 * 6.0))
|
|
);
|
|
assert_eq!(since(before, backend.meter()).reads, 3, "r0, r1 and r2");
|
|
}
|
|
|
|
#[test]
|
|
fn occupancy_with_a_hidden_item_reads_the_groups_it_subtracts() {
|
|
let backend = Incremental::new(table(6, AggFunc::Count), vec![]);
|
|
let reading = Reading::new(
|
|
Aggregation::OverHidden,
|
|
[("Product".to_string(), "p3".to_string())],
|
|
);
|
|
let line = CellKey::new(vec![("Region".into(), "r0".into())]);
|
|
assert_eq!(
|
|
backend.occupancy(&line, &reading),
|
|
5 * 6,
|
|
"p3's six cells are left out"
|
|
);
|
|
let before = backend.meter();
|
|
backend.occupancy(&line, &reading);
|
|
assert_eq!(
|
|
since(before, backend.meter()).reads,
|
|
2,
|
|
"the line, less p3 under it"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_group_whose_cells_are_all_cleared_is_no_longer_read() {
|
|
let mut backend = Incremental::new(table(2, AggFunc::Count), vec![]);
|
|
let one = cell(1, 1, 1);
|
|
assert_eq!(backend.raw_fold(&one), Some(CellValue::Number(1.0)));
|
|
backend.apply(&Edit::ClearCell(cell(1, 1, 1)));
|
|
let before = backend.meter();
|
|
assert_eq!(backend.raw_fold(&one), None, "nothing is left under it");
|
|
assert_eq!(
|
|
since(before, backend.meter()).reads,
|
|
0,
|
|
"and no empty group stands in for it"
|
|
);
|
|
}
|