ReactiveDag.Node.CalcGrain (reactive_dag v0.17.0-rc.63)

Copy Markdown View Source

A grain entry that is a CALCULATION, evaluated on a change rather than read from the datastore.

Most grains are columns, and a change carries them directly: project the diff onto a map and the values are there. A calculation is different — it is an Ash expr the datastore normally evaluates, so a fold grouping by one used to fall back to reading live rows, which cannot answer for a deleted row and names only where a moved row landed.

It does not have to. A calculation over attributes of the same row is a function of values the diff already holds, and Ash can evaluate it in the BEAM: build a record from the diff and Ash.load/3 the calculation onto it. No query, no round trip, and a destroyed row's prior side evaluates exactly as well as a live one's.

Why Ash.load/3 rather than evaluating the expr directly

The expr is reachable — Ash.Resource.Info.calculation/2 hands back {Ash.Resource.Calculation.Expression, [expr: …]} — but its refs are unresolved (%Ash.Query.Ref{resource: nil}), so Ash.Expr.eval/2 refuses them with "Invalid reference kind". Resolving refs is query-planning work, and reimplementing it here would be a second, worse copy of Ash's own evaluator.

Ash.load/3 on an in-memory struct does the whole job — including calculations that depend on other calculations — and stays correct as Ash's expression language grows. Measured: a if kind == "osc_actual" … calculation evaluates correctly on a struct that was never in the database.

What is NOT supported

A calculation that traverses a RELATIONSHIP, or a custom calculation module doing its own reads. Both need data the change does not carry, so they keep the live-read fallback rather than being half-evaluated against nils — a wrong unit is worse than a wide one, because a fold that claims the wrong unit reconciles a live one away.

This is decided per cell at assembly (Node.group_key_plan/2 marks the entry {:calc, name}), and per change here: an evaluation that errors or comes back nil yields no unit, and the caller falls back.

Summary

Functions

Project a change onto a grain that may contain calculations.

Functions

values(plan, row, resource)

@spec values([tuple()], map(), module()) :: [term()] | nil

Project a change onto a grain that may contain calculations.

plan is the assembled group_key_plan{:attr, name, string?} and {:calc, name} entries in group order. row is the diff projected to a map (Diff.before/1 or Diff.after_/1).

Returns the grain's values in plan order, or nil when any entry cannot be resolved — the caller then reads live rather than claiming a partial unit.