Aggregates for Ramal.aggregate/4: a pure computation over a list of records, and the delta path that uses it.
The aggregates Ash hands to the data layer never see a branch's overlay. Ramal.aggregate/4 therefore answers in
one of two ways:
- delta (the default). Let K be the primary keys in the branch's overlay for the resource. The database
aggregates every matching row except K (
delta/5, which isAsh.count/2,Ash.sum/3and the like with a key filter added). The branch then reads only the K rows through the patched path, which gives exactly the branch's version of them, filtered by the whole query.combine/5joins the two. The cost grows with the size of the overlay, not with the size of the table. It does not apply when the query has a limit, an offset or adistinct, because the aggregate then depends on the order of rows the delta does not have. - read (
compute/4). Read every matching row through the patched path and aggregate in Elixir. Exact for any query, and as expensive as the read.
The kinds follow Ash's: :count (rows), :exists, :sum, :min, :max and :avg. As in SQL, nil values are
skipped, and :sum, :min, :max and :avg of no values are nil. :sum of decimals is a Decimal, :avg is a
float (Ash types it as :float, lib/ash/query/aggregate.ex). :min and :max order values the way a runtime sort
does (the runtime sort in Ash.Actions.Sort), so dates, decimals and strings compare by their type.
Confirmed against Ash 3.37 for the delta path: the aggregate action (lib/ash/actions/aggregate.ex) builds its query with
Ash.Query.for_read/4, so the action's policies and preparations apply, but it does not run the before_action and
after_action hooks that Ramal.Preparation installs. The database part is therefore never patched, which is what
the delta needs. The rows it aggregates are not fingerprinted either, so they do not join the merge staleness check
(ADR 0013).
Example
iex> records = [%{head: 30}, %{head: nil}, %{head: 12}]
iex> Ramal.Aggregate.compute(:sum, :head, records, Ramal.Test.Farm.Herd)
42
iex> Ramal.Aggregate.compute(:count, nil, records, Ramal.Test.Farm.Herd)
3
iex> Ramal.Aggregate.combine(:sum, :head, Ramal.Test.Farm.Herd, 100, [%{head: 30}, %{head: nil}])
130
Summary
Functions
Whether the delta path is valid for query with key_count overlaid rows. A limit, an offset or a distinct make the
aggregate depend on which rows come first, which the delta does not know.
Joins the database's answer for the rows outside the overlay with the branch's rows inside it. base is the
database's value for kind: an integer for :count, a boolean for :exists, the value or nil for :sum, :min
and :max, and {sum, non_nil_count} for :avg. rows are the branch's version of the overlaid rows that match the
query.
Computes kind over records, which are rows of resource. field is the attribute to aggregate; :count and
:exists ignore it. Raises ArgumentError for an unknown kind or a field that is not an attribute of resource.
The delta path of Ramal.aggregate/4: kind over query for a branch whose overlay holds keys (the primary keys
of the overlaid rows of the resource). Returns {:ok, value} or the error Ash returned. opts are the read options
(:actor, :authorize?, :tenant, :domain).
Raises ArgumentError unless kind is known and, for the kinds that read a value, field is an attribute of
resource. Ramal.aggregate/4 calls it before reading so that both of its paths refuse the same input.
Types
Functions
@spec applies?(Ash.Query.t(), non_neg_integer()) :: boolean()
Whether the delta path is valid for query with key_count overlaid rows. A limit, an offset or a distinct make the
aggregate depend on which rows come first, which the delta does not know.
Example
iex> Ramal.Aggregate.applies?(Ash.Query.new(Ramal.Test.Farm.Herd), 3)
true
iex> Ramal.Aggregate.applies?(Ash.Query.limit(Ramal.Test.Farm.Herd, 2), 3)
false
Joins the database's answer for the rows outside the overlay with the branch's rows inside it. base is the
database's value for kind: an integer for :count, a boolean for :exists, the value or nil for :sum, :min
and :max, and {sum, non_nil_count} for :avg. rows are the branch's version of the overlaid rows that match the
query.
Example
iex> Ramal.Aggregate.combine(:count, nil, Ramal.Test.Farm.Herd, 10, [%{}, %{}])
12
iex> Ramal.Aggregate.combine(:max, :head, Ramal.Test.Farm.Herd, 30, [%{head: 35}])
35
iex> Ramal.Aggregate.combine(:avg, :head, Ramal.Test.Farm.Herd, {60, 2}, [%{head: 30}])
30.0
iex> Ramal.Aggregate.combine(:sum, :head, Ramal.Test.Farm.Herd, nil, [])
nil
Computes kind over records, which are rows of resource. field is the attribute to aggregate; :count and
:exists ignore it. Raises ArgumentError for an unknown kind or a field that is not an attribute of resource.
Example
iex> Ramal.Aggregate.compute(:max, :head, [%{head: 30}, %{head: 12}], Ramal.Test.Farm.Herd)
30
iex> Ramal.Aggregate.compute(:avg, :head, [%{head: 30}, %{head: 12}], Ramal.Test.Farm.Herd)
21.0
iex> Ramal.Aggregate.compute(:exists, nil, [], Ramal.Test.Farm.Herd)
false
@spec delta(Ash.Query.t(), kind(), atom() | nil, keyword(), [term()]) :: {:ok, term()} | {:error, term()}
The delta path of Ramal.aggregate/4: kind over query for a branch whose overlay holds keys (the primary keys
of the overlaid rows of the resource). Returns {:ok, value} or the error Ash returned. opts are the read options
(:actor, :authorize?, :tenant, :domain).
Call it only inside Ramal.run/2, and only when applies?/2 is true.
@spec kinds() :: [kind()]
The kinds compute/4 knows.
Example
iex> Ramal.Aggregate.kinds()
[:count, :exists, :sum, :min, :max, :avg]
Raises ArgumentError unless kind is known and, for the kinds that read a value, field is an attribute of
resource. Ramal.aggregate/4 calls it before reading so that both of its paths refuse the same input.
Example
iex> Ramal.Aggregate.validate!(:sum, :head, Ramal.Test.Farm.Herd)
:ok
iex> Ramal.Aggregate.validate!(:count, nil, Ramal.Test.Farm.Herd)
:ok