Skip to content

Data: unified calculated fields for Store and Cube View (#4620) - #4636

Closed
lbwexler wants to merge 14 commits into
developfrom
claude/issue-4620-kyt4eq
Closed

lbwexler wants to merge 14 commits into
developfrom
claude/issue-4620-kyt4eq

Conversation

@lbwexler

@lbwexler lbwexler commented Aug 31, 2026 •

Copy link
Copy Markdown
Member

Implements #4620 - calculatedFn fields at both data layers - plus the follow-on hardening that grew out of in-PR review, including a breaking change to connected-store configuration.

Key changes

  • FieldSpec.calculatedFn: (data, store) => any - Store fields computed on the client at read time via lazy prototype getters on record data. Never stored, parsed, or included in equality/digest comparisons; always current, even when inputs live outside the row (e.g. a Store.summaryRecords denominator). Read-only: bound grid columns are never editable and modifyRecords() throws.
  • CubeFieldSpec.calculatedFn: (row, ctx) => any - the Cube-layer form, computed on View rows with the AggregationContext. Carries no aggregator, so pct-of-total-style Views retain the incremental data-only update path. ctx.filteredRecords is readable from these fns and always current (sourced lazily from _records or the patched leaf map, per update type).
  • Declarative freshness - grids repaint calculated columns after each transaction; a FieldFilter on a calculated field upgrades incremental refilters to full; sorts on calculated columns are restored through the paced deferred-sort flush, including on empty (summary-only) syncs.
  • 💥 Connected stores are now always projectionOnly - new Store.connectView(view) / disconnectView() encapsulate connected-store adoption (row digest, projection mode, calculated field name marking), called by the View on connect/setStores/query change/destroy. An explicit projectionOnly: false or processRawData on a connected store now throws - parse mode offered connected stores nothing that worked (parseVal is a no-op on typed view rows; updateData never applied processRawData). See the CHANGELOG entry and new docs/upgrade-notes/v88-upgrade-notes.md.
  • Generator symmetry - Store record data generation (shared defaults/template plus the generated dense and projection classes carrying calculated getters) is extracted to data/impl/RecordDataGenerator.ts, mirroring the Cube's RowDataGenerator - parent ViewRowData rows are themselves now generated-class instances. Both generators consume the shared getter util data/impl/FieldGetterSupport.ts (the Getter-based row data for exposed Cube View leaves #4586 technique, generalized).

Follow-on filed: #4637 - reconcile connected store fields with the View's query fields, deleting the calc-name marking shim inside connectView.

Companion Toolbox branch/PR exercises the feature in the cube tester: PctTotalAggregator rewritten as a calculated field, no editable/projectionOnly config needed, and the projection A/B switch removed.

Hoist P/R Checklist

  • Caught up with develop branch as of last change.
  • Added CHANGELOG entry, or determined not required.
  • Reviewed for breaking changes, added breaking-change label + CHANGELOG if so. (Connected-store projectionOnly enforcement - CHANGELOG breaking section + v88 upgrade notes.)
  • Updated doc comments / prop-types, or determined not required. (JSDoc throughout, plus data/README.md and data/cube/README.md sections.)
  • Reviewed and tested on Mobile, or determined not required. (Shared GridModel/Store machinery - no platform-specific code touched.)
  • Created Toolbox branch / PR, or determined not required.

claude added 3 commits August 31, 2026 03:23
Store fields declared with a calculatedFn compute their values on the client at read time, through lazy prototype getters on record data objects - never stored, parsed, or included in the value-equality and digest comparisons used to detect unchanged records, and always current when read.

- Added shared getter-class util in data/impl/CalculatedFieldSupport.ts, generalizing the exposed-leaf technique from RowDataGenerator for consumption by both the Store and (upcoming) Cube layers.
- projectionOnly stores adopt raw rows via a generated wrapper class (data !== raw) carrying source-field and calculated getters.
- Calculated fields are read-only: modifyRecords() throws on writes, and they are excluded from getModifiedValues() and all parse paths.
- GridModel-driven grids auto-refresh columns displaying calculated fields after each transaction, repainting visible cells whose value moved via inputs outside their own row (e.g. a summary denominator), and never suppress re-sorts on calculated sort columns.
- A FieldFilter testing a calculated field upgrades incremental refilters to full, keeping filter membership current as external inputs move.
- Data-object generation is a re-runnable function of Store state, anticipating future runtime updates to calculated field specs.
Cube-layer form of calculated fields, computed at read time on View rows with the View's AggregationContext. Because such fields carry no aggregator, Views using them for globally-dependent values (e.g. pct-of-total) never trip aggregatorsAreSimple and retain the incremental data-only update path.

- RowDataGenerator installs calculated getters on both generated data classes, reading the View's current AggregationContext live. Aggregate/bucket row data moves from a spread-cloned template to a generated class, keeping parent rows on one fixed shape.
- AggregationContext.filteredRecords is now readable by calculated field functions and stays fresh on data-only ticks - rebuilt lazily from the View's leaf map (patched fresh each tick) and memoized per context.
- applyLeafDataUpdate diffs only non-calculated fields.
- Validations: calculatedFn is mutually exclusive with aggregator/canAggregateFn/isDimension; BucketSpec.dependentFields may not name calculated fields; stores connected to a View with calculated fields must set projectionOnly (upgraded from warning to throw). The View marks its calculated field names on connected stores, wiring them into the Store-layer grid repaint and filter refresh handling.
Dense record data on stores with calculated fields is now built from a generated class - constructor-assigned slots for id and every non-calculated field in one fixed order, calculated getters on the prototype - rather than falling back to the sparse form. All instances share a single shape, and constructor slack tracking keeps them in V8's fast-properties mode past the plain-object add limit behind denseRecordThreshold, so huge parsed stores get packed memory and fast access alongside calculated fields. Stores without calculated fields keep the existing template spread-clone unchanged.
- Columns bound to calculated fields are never editable - gated in Column.isEditableForRecord via Store.calculatedFieldNames, covering View-published fields whose grid-store Field is not itself marked calculated.
- Restore grid sort order on empty-transaction syncs (e.g. summary-only updates) when sorted on a calculated column.
- Clear View-marked calculated field names from connected stores on View.destroy(), so a store outliving its View stops paying repaint/refilter handling.
- Doc guidance: calculated values are read by name (own-property enumeration, spread and JSON.stringify do not see them - use StoreRecord.getValues()), and calculatedFns should return primitives or stable references so grid change detection can skip unchanged cells.
…ield refinements (#4620)

- Stores connected to a Cube View now default to projectionOnly: true when unset - an explicit false or a processRawData function opts out. Added internal Store.setProjectionOnly() flipping mode post-construction, with CHANGELOG breaking entry and new v88 upgrade notes.
- Empty-sync sort restore now routes through GridTransactionManager.noteSortStale(), coalescing with the paced deferred-sort flush instead of an immediate unscheduled full sort.
- AggregationContext now takes a lazy filtered-records source from its View, replacing the recordsAreStale flag - View._leafMap returns to private.
- View partitions calc/non-calc fields once in buildIndices (now ordered before the row data generator), consumed by RowDataGenerator and syncStoreCalculatedFields.
- Bucket dependentFields validation runs once per newly-seen name rather than on every bucketing pass.
- Trimmed changelog to an app-developer perspective; condensed comments throughout.
Pulls forward the full form of the earlier defaulting change: View.parseStores now places every connected store into projection mode itself, throwing on an explicit `projectionOnly: false` or a `processRawData` config. The calculated-field-conditional guard in syncStoreCalculatedFields is gone along with the opt-out - parse mode was offering connected stores nothing that worked (parseVal is a no-op on already-typed view rows, and updateData never applied processRawData, so transforms were stale on every incremental update). Changelog breaking entry, v88 upgrade notes, and docs updated to match.
…option (#4620)

- Store.connectView(view): validates conflicting config (digestSpec, idEncodesTreePath, projectionOnly: false, processRawData), installs the View's row digest, adopts projection mode, and marks the View's calculated field names. Idempotent - the View calls it at connection, on setStores, and on query changes.
- Store.disconnectView(): clears connected-View state on View destroy - and View.setStores now disconnects outgoing stores, a pre-existing gap.
- Folds setProjectionOnly into connectView (sole caller); setExternalCalculatedFieldNames becomes private; View.parseStores and syncStoreCalculatedFields deleted.
The module hosts both calculated-field getters and source-field (`_src`) getters - the old name was specific to the first.
…re projection (#4620)

- Condensed FieldSpec.calculatedFn JSDoc (46 -> 28 lines), same content packed.
- data/README projectionOnly section updated - connected stores are always projections set by the View, not a warned recommendation.
- Small code tidies in Store/StoreRecord.
Recomputed only when the filter or calculated field names change, rather than re-flattening the filter on every transaction.
Record data generation - shared defaults/template plus the generated dense and projection classes carrying calculated field getters - moves to data/impl/RecordDataGenerator, mirroring the Cube's RowDataGenerator. Store.generateDataConfig() now just rebuilds the field map and generator. Net -119 lines in Store.
@lbwexler

Copy link
Copy Markdown
Member Author

Superseded by #4638, which contains this PR's full history plus the #4637 follow-on work, now retargeted to develop as the single reviewable unit.

@lbwexler lbwexler closed this Aug 31, 2026
@lbwexler
lbwexler deleted the claude/issue-4620-kyt4eq branch August 31, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants