AshDyan.Engine (AshDyan v0.5.0)

Copy Markdown View Source

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:

  1. selects only the columns it needs (the metric column, the time field, the group_by fields),
  2. applies the caller's filters and the configured limit (a hard cap that prevents full-cardinality group-bys from blowing up the DB),
  3. runs the query through the resource's read action — so Ash.Policy authorization applies unchanged,
  4. aggregates the returned rows in memory into the stable labels/series output shape.

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.

Types

hook_module()

@type hook_module() :: module()

Functions

build_query(request, opts \\ [])

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

format(request, records)

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

run_query(query, request, opts)

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