Translates a validated AshDyan.Request into an Ash.Query, runs it through
the resource's normal read action (so Ash policies apply), and aggregates the
result in memory into a chart-ready shape.
Design note
Ash's Ash.Query (3.x) does not expose a generic group_by builder, and the
shape of grouped aggregates is data-layer dependent. To keep AshDyan
data-layer agnostic, safe, and predictable, the engine:
- selects only the columns it needs (the metric column, the time field, the group_by fields),
- applies the caller's filters and the configured
limit(a hard cap that prevents full-cardinality group-bys from blowing up the DB), - runs the query through the resource's read action — so
Ash.Policyauthorization applies unchanged, - aggregates the returned rows in memory into the stable
labels/seriesoutput shape.
Consequence of step 2 + 4: for :aggregate, :time_bucket, :percentile,
and :histogram, the metric is computed over the limited row set, not the
full dataset. limit acts as a scan budget for these types. See the
"Limit semantics" section of AshDyan.run/2.
This keeps the security boundary (the dyan DSL whitelist + enforced limits)
intact while avoiding data-layer-specific query shapes. Percentiles, in
particular, are computed in memory so they work on any data layer; the
capability check still surfaces data-layer limits explicitly via
AshDyan.supports?/2.
Pipeline Hooks
Extensions can hook into the pipeline via config :ash_dyan, :hooks:
config :ash_dyan, :hooks, %{
before_query: [MyApp.Hooks.BeforeQuery],
after_query: [MyApp.Hooks.AfterQuery],
before_format: [MyApp.Hooks.BeforeFormat],
after_format: [MyApp.Hooks.AfterFormat]
}Each hook module must implement AshDyan.Engine.Hook behaviour.
Summary
Functions
Build an Ash.Query that selects exactly the columns needed for the request.
Format raw records into an AshDyan.Result using the analysis module's format callback.
Run a built query through the resource's read action.
Stream records for a query without materializing the full result set.
Types
@type hook_module() :: module()
Functions
@spec build_query(AshDyan.Request.t(), [AshDyan.run_opt()]) :: {:ok, Ash.Query.t()} | {:error, term()}
Build an Ash.Query that selects exactly the columns needed for the request.
@spec format(AshDyan.Request.t(), [Ash.Resource.Record.t()]) :: {:ok, AshDyan.Result.t()} | {:error, term()}
Format raw records into an AshDyan.Result using the analysis module's format callback.
@spec run_query(Ash.Query.t(), AshDyan.Request.t(), [AshDyan.run_opt()]) :: {:ok, [Ash.Resource.Record.t()]} | {:error, term()}
Run a built query through the resource's read action.
@spec stream_query(Ash.Query.t(), AshDyan.Request.t(), [AshDyan.run_opt()]) :: {:ok, Enumerable.t()} | {:error, term()}
Stream records for a query without materializing the full result set.
Uses Ash.stream!/2 (keyset/offset pagination where the data layer supports
it, falling back to a full read) so callers can aggregate over large datasets
batch-by-batch instead of loading every row at once. The before_query hooks
still apply; after_query hooks cannot (they expect a complete list), so
callers using this path should apply per-record logic themselves.
The returned enumerable must be consumed exactly once; the query's limit
still applies.