7.2 KiB
CellSource: where cell values are read from
crates/improvise-core/src/model/source.rs. ADR 003 asked for a source trait in core with the
in-RAM DataStore as one implementation (improvise-qddo). ADR 009 step 7 (improvise-rnak) asks for
the two live sources that need it. This file is the reasoning; the evidence for each test is in
context/test-evidence/core.md. The ADRs are not amended: what follows are the readings taken where
they leave a choice.
What is in the trait, and why
| method | what the evaluator asks |
|---|---|
value_at(key) |
the value stored at exactly key (an exact read, the filter-mismatch fallback) |
summarize(select) |
the summary of the cells a Select names: the aggregate, not the rows |
keys(partial) |
the keys of the cells containing partial: stems, and the records behind an aggregate |
cells(partial) |
the same with values: records mode and a drill |
- Aggregation is the interface. ADR 003 clause 1:
GridLayoutasks for a value, not for the rows behind it, and the aggregates are SQL's. So the one read the evaluator makes in bulk,summarize, returns aSummaryand never a list of values.keysandcellsexist because some reads are of records, not of a fold (the stems a formula is evaluated at; records mode), and a SQL source answers them with aSELECT DISTINCTor a plain select. - Writes are not in the trait.
DataStore::setandremoveare inherent methods. A source that cannot be written to has no method to fail in, which is ADR 003's "unrepresentable rather than enforced". The model's own writes (set_cell,clear_cell) go to the owned store. - The trait has no
Result. A source that can fail (a connection) decides at its own boundary what a failed read is, andimprovise-iois where that decision belongs. AGENTS.md keepsOptionandResultat the edges;Optionis the one the trait uses, for "no such cell". Selectisspanned_values' arguments. A fold names a partial key, the covered members of each group or omitted category it spans, and whether only an exact-width cell counts. The covered members are decided in RAM from the categories and the view's hidden items, so a source is handed explicit item lists and never a predicate. A hidden item therefore reaches a source only as the absence of an item from a list.
Summary and Numbers
A Summary is what a source reports about the values of a read: a Numbers (count, an exact sum,
the least and greatest non-NaN, in total_cmp order so -0.0 is below +0.0) and whether the rest
(text, errors) agree on one value. It reduces exactly as aggregate_cell_values did: numbers by the
function, anything else by unanimity, else Error("mixed"), none at all absent. Three properties
carry the design:
- It merges. A read of a collapsed group, a total, or several contexts of a formula aggregate is
the merge of the summaries of its parts, equal to the summary of the union. That is what lets an
engine answer a pooled read without sending rows, and what lets
eval_aggpoolNumbersinstead of concatenatingVec<f64>. The sum isExactSum, so a merge is the same bits in any order. - It is order-free. Unanimity needs the set of non-number values, not their order, so a source that has no insertion order loses nothing by it.
- The reference is kept.
tests::referenceis the old reduction, copied into the test module;a_summary_reduces_as_the_values_didcompares every sequence of an eight-value pool, up to three long, withSummaryover all five functions.characterization_survives_the_move: the existing evaluator tests (423 in the crate) are unchanged and pass.
What a real engine must supply is stated here because the trait cannot enforce it: a Numbers whose
sum is exact. SQLite's SUM is a float sum, so an engine that cannot return an exact accumulator
either returns the numbers (and the adapter sums them) or accepts a last-bit difference, which
context/code/improvise-incremental.md ("Step 6") calls a user-visible change.
Model::source() and Model::over(source)
Model::source() returns the &dyn CellSource that the evaluator, the stems of recompute_formulas,
the member records of an aggregate over an expression and the records of GridLayout read through.
Model::over(source) points it at a SharedSource (an Arc<dyn CellSource + Send + Sync>) instead of
the owned DataStore. That is the whole evaluator hook (improvise-rnak, bw4p.14's prerequisite):
there is still one evaluator, Model's, and it computes every formula value, over a source whose
cells it never owns.
- A model over a source is read-only, in every build.
set_cell,clear_cell,remove_categoryandremove_itemassert!that no source is set (adebug_assertfirst 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::overdrops the model's own cells and the formula cells computed from them, so no stale copy is left forget_cell, persistence or the harness to read.dataispub, 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. - A shared source changes behind its readers by interior mutability of its own. The model holds
an
Arc, so a database connection or a test double behind a lock is the same object the backend feeds from. - What still reads the owned store:
get_cell(a production caller:improvise-command/src/ui/effect.rs, theoldvalue of a rename),Model::data(pub), persistence, the import wizard and the effects inimprovise-command. They are the facade of a document that owns its cells; a model over a source has none of those to offer, and routing them would changeget_cell's&CellValuereturn for 129 call sites.Eager::occupancy,Shapes::ofandStems::buildinimprovise-incrementalare routed by the PRs that need them. Debugshows only whether a source is set. A source is not necessarily printable, and two models over different sources are not told apart by it.
TableSource
A scanned table of rows, behind cfg(any(test, feature = "test-support")). It is the second
implementation, written as a query engine with no index would answer, sharing only Summary with
DataStore. the_store_and_a_scanned_table_answer_every_select_alike asks both every select over a
fixture (every subset of every stored key as a base, with no span, one span of each of six item
lists, and two), at both widths and for all five functions: over 5,000 triples.
Building a Summary from an engine's answer
Summary::from_parts(Numbers::from_parts(count, exact_sum, least, greatest)?, Agreement)?. Agreement
is what the non-number values hold: None, One(v) or Mixed. Both constructors return a Result
and refuse a state no sequence of values could produce (extremes with no numbers, a NaN extreme,
least above greatest, one extreme without the other, a sum with no numbers, no extremes beside a sum
that is not NaN, a number among the "others"), because repairing
it would hide an engine's bug.