# `Ramal.Aggregate`
[🔗](https://github.com/agustinrojas1/ramal/blob/v0.2.0/lib/ramal/aggregate.ex#L1)

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 is `Ash.count/2`, `Ash.sum/3` and 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/5` joins 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 a
    `distinct`, 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

# `kind`

```elixir
@type kind() :: :count | :exists | :sum | :min | :max | :avg
```

An aggregate kind.

# `applies?`

```elixir
@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

# `combine`

```elixir
@spec combine(kind(), atom() | nil, module(), term(), [map()]) :: term()
```

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

# `compute`

```elixir
@spec compute(kind(), atom() | nil, [map()], module()) :: term()
```

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

# `delta`

```elixir
@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.

# `kinds`

```elixir
@spec kinds() :: [kind()]
```

The kinds `compute/4` knows.

## Example

    iex> Ramal.Aggregate.kinds()
    [:count, :exists, :sum, :min, :max, :avg]

# `validate!`

```elixir
@spec validate!(kind(), atom() | nil, module()) :: :ok
```

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
