Calendar bucketing as an ASH CALCULATION — the Ash-native answer to the classic "date-marked records → time-bucketed aggregate". The bucket is declared on the resource that OWNS the date (where derived values live in Ash), and a rollup node just groups by the calculation:
# on the data's resource
calculations do
calculate :month, :string, {ReactiveDag.Calendar, bucket: :month, of: :observed_on}
end
# on the rollup node
reduce over: :readings,
group_by: [:month],
into: [sum: [value: :total], count: :n]Buckets: :day ("2026-08-11"), :week (ISO — "2026-W33"), :month
("2026-08"), :quarter ("2026-Q3"), :year ("2026"). Labels sort
lexicographically in chronological order, and the derived cell key IS the
label. Accepts Date, DateTime, NaiveDateTime; nil stays nil.
This module computes in the BEAM after the read, so it works on every data layer (Ets included). A Postgres host wanting datastore pushdown declares an expr calculation instead — the rollup neither knows nor cares:
calculate :month, :string, expr(fragment("to_char(?, 'YYYY-MM')", observed_on))
Summary
Functions
The kind bucket a child KEY belongs to, by PURE string work: the key's
leading |-segment is parsed as a date or a finer bucket label, and
relabeled — bucket_of_key(:month, "2026-08-11|r4") → "2026-08";
bucket_of_key(:month, "2026-08-11") → "2026-08". :error when the
leading segment isn't date-shaped (the key rule then degrades to :all).
The supported bucket kinds.
Callback implementation for Ash.Resource.Calculation.describe/1.
Callback implementation for Ash.Resource.Calculation.has_expression?/0.
The bucket label for a date-ish value (nil-safe).
Parse a bucket LABEL back to {kind, first_date} — the pure inverse of
label/2 ("2026-08" → {:month, ~D[2026-08-01]}). :error for anything
that isn't a bucket label. This is what makes bucket keys self-describing:
a key rule can relabel a child's key without consulting any data.
The half-open date range a bucket label covers: {first, next_first} —
range(:month, "2026-08") → {~D[2026-08-01], ~D[2026-09-01]}. :error
when the label isn't a kind label. What a host (or the library's automatic
bucket scoping) filters the date attribute by.
Callback implementation for Ash.Resource.Calculation.strict_loads?/0.
Functions
The kind bucket a child KEY belongs to, by PURE string work: the key's
leading |-segment is parsed as a date or a finer bucket label, and
relabeled — bucket_of_key(:month, "2026-08-11|r4") → "2026-08";
bucket_of_key(:month, "2026-08-11") → "2026-08". :error when the
leading segment isn't date-shaped (the key rule then degrades to :all).
Deletion-safe: a vanished entry's key still parses to the bucket it left.
Nesting is "the bucket containing the child bucket's START date" — exact
for day→month/quarter/year and month→quarter/year; a :week child only
nests deterministically into :year (weeks straddle months).
@spec buckets() :: [atom()]
The supported bucket kinds.
Callback implementation for Ash.Resource.Calculation.describe/1.
Callback implementation for Ash.Resource.Calculation.has_expression?/0.
The bucket label for a date-ish value (nil-safe).
Parse a bucket LABEL back to {kind, first_date} — the pure inverse of
label/2 ("2026-08" → {:month, ~D[2026-08-01]}). :error for anything
that isn't a bucket label. This is what makes bucket keys self-describing:
a key rule can relabel a child's key without consulting any data.
The half-open date range a bucket label covers: {first, next_first} —
range(:month, "2026-08") → {~D[2026-08-01], ~D[2026-09-01]}. :error
when the label isn't a kind label. What a host (or the library's automatic
bucket scoping) filters the date attribute by.
Callback implementation for Ash.Resource.Calculation.strict_loads?/0.