fix(core): a model over a source refuses every write in every build and drops its own cells (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 18:57:13 -07:00
committed by Edward Langley
parent 7c5f77708c
commit bebaeb2d61
5 changed files with 112 additions and 8 deletions
@@ -219,6 +219,48 @@ fn a_model_over_a_source_refuses_a_write() {
over.set_cell(key("East", "A", "Revenue"), CellValue::Number(1.0));
}
#[test]
#[should_panic(expected = "read-only")]
fn a_model_over_a_source_refuses_a_clear() {
let mut over = lent(&fixture());
over.clear_cell(&key("East", "A", "Revenue"));
}
#[test]
#[should_panic(expected = "read-only")]
fn a_model_over_a_source_refuses_to_remove_a_category() {
let mut over = lent(&fixture());
over.remove_category("Product");
}
#[test]
#[should_panic(expected = "read-only")]
fn a_model_over_a_source_refuses_to_remove_an_item() {
let mut over = lent(&fixture());
over.remove_item("Product", "A");
}
#[test]
fn putting_a_populated_model_over_a_source_drops_its_own_cells() {
let owned = fixture();
assert!(owned.data.iter_cells().count() > 0);
let rows = owned
.data
.iter_cells()
.map(|(k, v)| (k, v.clone()))
.collect();
let over = owned.clone().over(Arc::new(TableSource { rows }));
assert_eq!(over.data.iter_cells().count(), 0, "no stale second copy");
let cell = key("East", "A", "Revenue");
assert_eq!(
over.get_cell(&cell),
None,
"the owned store is empty, the source is not"
);
let read = over.evaluate_aggregated(&cell, Aggregation::Exact);
assert_eq!(read, owned.evaluate_aggregated(&cell, Aggregation::Exact));
}
#[test]
fn the_records_a_drill_freezes_are_read_from_the_source() {
use crate::view::layout::drill_snapshot;
+12 -4
View File
@@ -430,13 +430,19 @@ impl Model {
}
}
/// This model, reading its raw cells from `source` and not `data`: read-only.
/// Why: `context/code/source.md`.
/// 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`.
pub fn over(mut self, source: SharedSource) -> Self {
self.data = DataStore::new();
self.external = Some(source);
self
}
/// Refuses, in every build, a write the source would never see.
fn writable(&self) {
assert!(self.external.is_none(), "{READ_ONLY}");
}
/// Add a pivot category, enforcing `MAX_CATEGORIES` over regular ones.
/// The caller registers it on every view — normally `Workbook`.
pub fn add_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
@@ -480,6 +486,7 @@ impl Model {
/// Remove a category, its cells and the formulas targeting it. The caller
/// removes it from the views — normally `Workbook`.
pub fn remove_category(&mut self, name: &str) {
self.writable();
if !self.categories.contains_key(name) || CategoryKind::of_name(name).is_virtual() {
return;
}
@@ -502,6 +509,7 @@ impl Model {
/// An item, the formulas targeting it and the cells naming it. The sweep
/// is `remove_category`'s one level down — `context/code/core.md` §9.
pub fn remove_item(&mut self, cat_name: &str, item_name: &str) {
self.writable();
if let Some(cat) = self.categories.get_mut(cat_name) {
cat.remove_item(item_name);
}
@@ -552,12 +560,12 @@ impl Model {
}
pub fn set_cell(&mut self, key: CellKey, value: CellValue) {
debug_assert!(self.external.is_none(), "{READ_ONLY}");
self.writable();
self.data.set(key, value);
}
pub fn clear_cell(&mut self, key: &CellKey) {
debug_assert!(self.external.is_none(), "{READ_ONLY}");
self.writable();
self.data.remove(key);
}
+26
View File
@@ -1348,6 +1348,32 @@ mod tests {
assert_eq!(layout.col_label(0), "X");
}
/// The records a records-mode grid draws come from the source the model is over.
#[test]
fn records_mode_reads_the_records_from_a_source() {
use crate::model::source::TableSource;
let mut wb = records_workbook();
let v = wb.active_view_mut();
v.set_axis("_Index", Axis::Row);
v.set_axis("_Dim", Axis::Column);
let rows = wb.model.data.iter_cells().map(|(k, v)| (k, v.clone()));
let over = wb.model.clone().over(std::sync::Arc::new(TableSource {
rows: rows.collect(),
}));
let (owned, lent) = (
GridLayout::new(&wb.model, wb.active_view()),
GridLayout::new(&over, wb.active_view()),
);
assert!(lent.is_records_mode());
assert_eq!(lent.row_count(), 1, "the source holds the records");
for col in 0..owned.col_count() {
assert_eq!(lent.records_display(0, col), owned.records_display(0, col));
}
let revenue = (0..lent.col_count()).find(|&c| lent.col_label(c) == "Revenue");
let revenue = revenue.unwrap();
assert_eq!(lent.records_display(0, revenue).as_deref(), Some("100"));
}
#[test]
fn records_mode_activated_when_index_and_dim_on_axes() {
let mut wb = records_workbook();