fix(core): a model put over a source drops the formula cells its own cells computed (improvise-rnak)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (dbsp-sources)
2026-10-10 19:32:39 -07:00
committed by Edward Langley
parent bcc773bb2a
commit 7f42a724f9
4 changed files with 45 additions and 2 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ cells it never owns.
1. **A model over a source is read-only, in every build.** `set_cell`, `clear_cell`, `remove_category` and
`remove_item` `assert!` that no source is set (a `debug_assert` first shipped and was lost in release:
the write went to the owned store, which nothing reads). A caller who holds a source writes to the
source (ADR 003: the trait has no writes). `Model::over` drops the model's own cells, so no stale copy
source (ADR 003: the trait has no writes). `Model::over` drops the model's own cells and the formula cells computed from them, so no stale copy
is left for `get_cell`, persistence or the harness to read. `data` is `pub`, so a caller can still write
it directly; it is not read, and nothing prevents that. Structural edits (a category or item removed)
are refused too, because the source's cells would still carry them.
+8
View File
@@ -2442,3 +2442,11 @@ source holds, and the evaluator still reads the source's value.
Review of #357: a records-mode `GridLayout` over a source. `group_records` reading the owned store
left all 563 tests green; with this test it fails (the grid has no rows).
### putting_a_model_with_formula_cells_over_a_source_drops_what_those_cells_computed
Review of #357 (Codex): `Model::over` emptied the store and kept `formula_cache`, which was computed from
those cells; reads consult the cache first, so old formula values survived until a recompute. The model
has a computed Profit, the source holds a different revenue, and the cell must be gone after `over`.
Rival: the `formula_cache.clear()` removed fails this test alone (before the fix it failed here first).
`measure_agg` and the categories are the definition and stay; nothing else is derived from the cells.
@@ -286,3 +286,37 @@ fn the_records_a_drill_freezes_are_read_from_the_source() {
}
assert!(rows > 10, "only {rows} records were compared");
}
#[test]
fn putting_a_model_with_formula_cells_over_a_source_drops_what_those_cells_computed() {
let mut owned = fixture();
owned.recompute_formulas(&["Region".to_string()]);
let profit = CellKey::new(vec![
("Product".into(), "A".into()),
("_Measure".into(), "Profit".into()),
]);
let stale = owned.evaluate_aggregated(&profit, Aggregation::OverHidden);
assert!(
matches!(stale, Some(CellValue::Number(_))),
"the cache holds Profit"
);
let revenue = key("East", "A", "Revenue");
let mut rows: Vec<_> = owned
.data
.iter_cells()
.map(|(k, v)| (k, v.clone()))
.collect();
rows.iter_mut()
.filter(|(k, _)| *k == revenue)
.for_each(|(_, v)| *v = CellValue::Number(1000.0));
let over = owned.clone().over(Arc::new(TableSource { rows }));
assert_eq!(
over.formula_cell(&profit),
None,
"computed from cells that are gone"
);
assert_ne!(
over.evaluate_aggregated(&profit, Aggregation::OverHidden),
stale
);
}
+2 -1
View File
@@ -431,9 +431,10 @@ impl Model {
}
/// This model, reading its raw cells from `source` and not `data`: read-only. Its own
/// cells are dropped, so no stale second copy is left to read. Why: `context/code/source.md`.
/// cells are dropped, and the formula cells computed from them, so no stale copy is left to read. Why: `context/code/source.md`.
pub fn over(mut self, source: SharedSource) -> Self {
self.data = DataStore::new();
self.formula_cache.clear();
self.external = Some(source);
self
}