| name | ash-calculations-aggregates |
| description | Calculations, aggregates, and exists expressions for derived data |
Calculations
Calculations allow you to define derived values based on a resource's attributes or related data. Define calculations in the calculations block of a resource:
calculations do
# Simple expression calculation
calculate :full_name, :string, expr(first_name <> " " <> last_name)
# Expression with conditions
calculate :status_label, :string, expr(
cond do
status == :active -> "Active"
status == :pending -> "Pending Review"
true -> "Inactive"
end
)
# Using module calculations for more complex logic
calculate :risk_score, :integer, {MyApp.Calculations.RiskScore, min: 0, max: 100}
end
Expression Calculations
Expression calculations use Ash expressions and can be pushed down to the data layer when possible:
calculations do
# Simple string concatenation
calculate :full_name, :string, expr(first_name <> " " <> last_name)
# Math operations
calculate :total_with_tax, :decimal, expr(amount * (1 + tax_rate))
# Date manipulation
calculate :days_since_created, :integer, expr(
date_diff(^now(), inserted_at, :day)
)
end
Expressions
In order to use expressions outside of resources, changes, preparations etc. you will need to use Ash.Expr.
It provides both expr/1 and template helpers like actor/1 and arg/1.
For example:
import Ash.Expr
Author
|> Ash.Query.aggregate(:count_of_my_favorited_posts, :count, [:posts], query: [
filter: expr(favorited_by(user_id: ^actor(:id)))
])
See the expressions guide for more information on what is available in expresisons and how to use them.
Module Calculations
For complex calculations, create a module that implements Ash.Resource.Calculation:
defmodule MyApp.Calculations.FullName do
use Ash.Resource.Calculation
# Validate and transform options
@impl true
def init(opts) do
{:ok, Map.put_new(opts, :separator, " ")}
end
# Specify what data needs to be loaded
@impl true
def load(_query, _opts, _context) do
[:first_name, :last_name]
end
# Implement the calculation logic
@impl true
def calculate(records, opts, _context) do
Enum.map(records, fn record ->
[record.first_name, record.last_name]
|> Enum.reject(&is_nil/1)
|> Enum.join(opts.separator)
end)
end
end
# Usage in a resource
calculations do
calculate :full_name, :string, {MyApp.Calculations.FullName, separator: ", "}
end
Calculations with Arguments
You can define calculations that accept arguments:
calculations do
calculate :full_name, :string, expr(first_name <> ^arg(:separator) <> last_name) do
argument :separator, :string do
allow_nil? false
default " "
constraints [allow_empty?: true, trim?: false]
end
end
end
Using Calculations
# Using code interface options (preferred)
users = MyDomain.list_users!(load: [full_name: [separator: ", "]])
# Filtering and sorting
users = MyDomain.list_users!(
query: [
filter: [full_name: [separator: " ", value: "John Doe"]],
sort: [full_name: {[separator: " "], :asc}]
]
)
# Manual query building (for complex cases)
User |> Ash.Query.load(full_name: [separator: ", "]) |> Ash.read!()
# Loading on existing records
Ash.load!(users, :full_name)
Code Interface for Calculations
Define calculation functions on your domain for standalone use:
# In your domain
resource User do
define_calculation :full_name, args: [:first_name, :last_name, {:optional, :separator}]
end
# Then call it directly
MyDomain.full_name("John", "Doe", ", ") # Returns "John, Doe"
Aggregates
Aggregates allow you to retrieve summary information over groups of related data, like counts, sums, or averages. Define aggregates in the aggregates block of a resource.
Aggregates can work over relationships or directly over unrelated resources:
aggregates do
# Related aggregates - use relationship path
count :published_post_count, :posts do
filter expr(published == true)
end
sum :total_sales, :orders, :amount
exists :is_admin, :roles do
filter expr(name == "admin")
end
# Unrelated aggregates - use resource module directly
count :matching_profiles_count, Profile do
filter expr(name == parent(name))
end
sum :total_report_score, Report, :score do
filter expr(author_name == parent(name))
end
exists :has_reports, Report do
filter expr(author_name == parent(name))
end
end
For unrelated aggregates, use parent/1 to reference fields from the source resource.
Aggregate Types
- count: Counts related items meeting criteria
- sum: Sums a field across related items
- exists: Returns boolean indicating if matching related items exist (also supports unrelated resources)
- first: Gets the first related value matching criteria
- list: Lists the related values for a specific field
- max: Gets the maximum value of a field
- min: Gets the minimum value of a field
- avg: Gets the average value of a field
Using Aggregates
# Using code interface options (preferred)
users = MyDomain.list_users!(
load: [:published_post_count, :total_sales],
query: [
filter: [published_post_count: [greater_than: 5]],
sort: [published_post_count: :desc]
]
)
# Manual query building (for complex cases)
User |> Ash.Query.filter(published_post_count > 5) |> Ash.read!()
# Loading on existing records
Ash.load!(users, :published_post_count)
Join Filters
For complex aggregates involving multiple relationships, use join filters:
aggregates do
sum :redeemed_deal_amount, [:redeems, :deal], :amount do
# Filter on the aggregate as a whole
filter expr(redeems.redeemed == true)
# Apply filters to specific relationship steps
join_filter :redeems, expr(redeemed == true)
join_filter [:redeems, :deal], expr(active == parent(require_active))
end
end
Inline Aggregates
Use aggregates inline within expressions:
# Related inline aggregates
calculate :grade_percentage, :decimal, expr(
count(answers, query: [filter: expr(correct == true)]) * 100 /
count(answers)
)
# Unrelated inline aggregates
calculate :profile_count, :integer, expr(
count(Profile, filter: expr(name == parent(name)))
)
calculate :stats, :map, expr(%{
profiles: count(Profile, filter: expr(active == true)),
reports: count(Report, filter: expr(author_name == parent(name))),
has_active_profile: exists(Profile, active == true and name == parent(name))
})
Exists Expressions
Use exists/2 to check for the existence of records, either through relationships or unrelated resources:
Related Exists
# Check if user has any admin roles
Ash.Query.filter(User, exists(roles, name == "admin"))
# Check if post has comments with high scores
Ash.Query.filter(Post, exists(comments, score > 50))
Unrelated Exists
# Check if any profile exists with the same name
Ash.Query.filter(User, exists(Profile, name == parent(name)))
# Check if user has any reports
Ash.Query.filter(User, exists(Report, author_name == parent(name)))
# Complex existence checks
Ash.Query.filter(User,
active == true and
exists(Profile, active == true and name == parent(name))
)
Unrelated exists expressions automatically apply authorization using the target resource's primary read action. Use parent/1 to reference fields from the source resource.