ReactiveDag.Node.Rows (reactive_dag v0.17.0-rc.4)

Copy Markdown View Source

Reads a cell's own rows, keyed the way the DAG keys them.

A node's rows live in its resource, but the DAG addresses them by cell key — a "|"-joined identity for a composite-PK node, a single column's value otherwise. Anything that wants to ask a question about a cell rather than about a table (ReactiveDag.Insights, ReactiveDag.Verdict, a union reading its inputs) needs the rows under those keys, not under the resource's own primary key.

This is the read side of what ReactiveDag.Node.Payload writes, and it deliberately mirrors that module's key derivation: identity_fields when the node is identity-keyed, payload_key (defaulting to :key) otherwise.

Why it is not the coordination tuple

These reads used to go to reactive_dag_tuple, which carried a status per (cell_id, key). That made the tuple a second home for derived results — one with a fixed two-column schema, updated by a writer the host had to configure, and queryable only through this library. Reading the resource instead means a status is an ordinary column with ordinary Ash semantics: policies apply, loads work, and a host can add a second column without asking us.

Missing columns are not errors

A node need not have a :status column — most don't; a rollup has sums. Rows from such a node come back with status: nil, and callers that count statuses simply find nothing to count. A node with no attributes at all keeps its rows somewhere else entirely and reads as empty. Asking is always safe.

Summary

Types

one of a cell's rows, addressed by cell key

Where a node's rows live and how they are keyed — a cell's meta, or the same three fields lifted out of it (which is what a union carries for each of its inputs, since it reads rows it does not own).

Functions

Every row the cell currently holds, as %{key:, status:, record:}.

The keys whose status is in statuses, at most limit of them (sorted, so a sample is stable between calls rather than reshuffling on every render).

%{status => count} over the cell's rows — the histogram Insights shows and Verdict folds into one answer.

Types

row()

@type row() :: %{key: String.t(), status: String.t() | nil, record: struct()}

one of a cell's rows, addressed by cell key

source()

@type source() :: %{
  optional(:resource) => module() | nil,
  optional(:payload_key) => atom() | nil,
  optional(:identity_fields) => [atom()] | nil
}

Where a node's rows live and how they are keyed — a cell's meta, or the same three fields lifted out of it (which is what a union carries for each of its inputs, since it reads rows it does not own).

Functions

all(source)

@spec all(ReactiveDag.Cell.t() | source()) :: [row()]

Every row the cell currently holds, as %{key:, status:, record:}.

Returns [] for a node that keeps no rows here — no resource at all, or a resource with no attributes (the shape a compute/custom-upsert: node has, where the real writes land somewhere this library never sees). That is different from "holds nothing", so a caller that must tell the two apart should check meta[:resource] itself.

Raises whatever the underlying Ash.read!/1 raises. Callers on a display path (Insights) already run these behind their own rescue; a caller on a compute path wants the failure.

keys_by_status(cell, statuses, opts \\ [])

@spec keys_by_status(ReactiveDag.Cell.t() | source(), [String.t() | nil], keyword()) ::
  [String.t()]

The keys whose status is in statuses, at most limit of them (sorted, so a sample is stable between calls rather than reshuffling on every render).

status_histogram(cell)

@spec status_histogram(ReactiveDag.Cell.t() | source()) :: %{
  required(String.t() | nil) => non_neg_integer()
}

%{status => count} over the cell's rows — the histogram Insights shows and Verdict folds into one answer.

Rows with no status are counted under nil, so the counts always sum to the cell's key count and a node without a :status column reports %{nil => n} rather than lying with %{}.