Source: context/code/source.md

On this page

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

methodwhat 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
count(select, excluding)how many cells a select names, leaving out those holding a hidden pair: occupancy
distinct_without(cats)the cell keys with cats removed, each once, first seen first: the stems formulas are evaluated at
  1. Aggregation is the interface. ADR 003 clause 1: GridLayout asks 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 a Summary and never a list of values. keys and cells exist 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 a SELECT DISTINCT or a plain select.
  2. Writes are not in the trait. DataStore::set and remove are 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.
  3. The trait has no Result. A source that can fail (a connection) decides at its own boundary what a failed read is, and improvise-io is where that decision belongs. AGENTS.md keeps Option and Result at the edges; Option is the one the trait uses, for "no such cell".
  4. Select is spanned_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:

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.

  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 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.
  2. 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.
  3. What still reads the owned store: get_cell (a production caller: improvise-command/src/ui/effect.rs, the old value of a rename), Model::data (pub), persistence, the import wizard and the effects in improvise-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 change get_cell's &CellValue return for 129 call sites. Eager::occupancy, Shapes::of and Stems::build in improvise-incremental are routed by the PRs that need them.
  4. Debug shows 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.

#ChangeSource

CellSource plus drain_changes(&self) -> Vec<CellChange>: a source that reports what changed in it. The contract that matters is that a source's cells are those as of the last drain, so a snapshot read now plus the changes drained later counts no change twice or not at all. Why, and how Incremental uses it: context/code/improvise-incremental.md, step 7. DataStore is not a ChangeSource: its log is &mut and is drained by the owner in the step that made the edit. A SharedFeed is the Arc a backend and a model share.

#Pushdown needs two more queries

count and distinct_without joined the trait for Pushdown (improvise-rnak). Both are what a query engine answers without sending rows (COUNT(*) ... WHERE, SELECT DISTINCT), and each replaces a read that would have transferred every key: Eager::occupancy lists all keys, and recompute_formulas walked every cell to find its stems. recompute_formulas now asks distinct_without(target and None categories) once per formula, and the order it returns is the order the cells were first seen in, so the stems are inserted in the order they always were. That order is not a contract a source can be held to: the passes of the fixed point visit stems in it and a chain of stems can settle or not by it. That is a known evaluator defect, improvise-bw4p.30 (context/code/improvise-incremental.md, step 7 item 8), and until it is fixed a source with no order of its own should list stems in a stable one.

Select::admits is the predicate of a select over one key, for a source that must test keys itself (DataStore::count). TableSource does not use it: its own holds is the independent second reading.

backend and a model share. A feed has one drainer: a drain empties the pending list, so two backends over one feed split the changes between them (measured in review: the same write gave one backend a Revenue of 128 and the other 38, and the second then disagreed with its own exact reads). a_feed_has_one_drainer pins the behaviour. Kb-style invariant: invariant(change_source, one_drainer).

#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.