Write Queries
View SourceMix.install([{:ash, "~> 3.0"}],
  consolidate_protocols: false
)
Application.put_env(:ash, :validate_domain_resource_inclusion?, false)
Application.put_env(:ash, :validate_domain_config_inclusion?, false)
ExUnit.start()Introduction
Here we will show practical examples of using Ash.Query. To understand more about its capabilities, limitations, and design, see the module docs of Ash.Query.
This guide is here to provide a slew of examples, for more information on any given function or option please search the documentation. Please propose additions for any useful patterns that are not demonstrated here!
Setup
First, lets create some resources and some data to query.
defmodule MyApp.Posts do
  use Ash.Domain
  resources do
    resource MyApp.Posts.Post
    resource MyApp.Posts.Comment
  end
end
defmodule MyApp.Posts.Post do
  use Ash.Resource,
    domain: MyApp.Posts,
    data_layer: Ash.DataLayer.Ets
  actions do
    defaults [:read, :destroy, create: :*]
  end
  
  attributes do
    uuid_primary_key :id
    
    attribute :text, :string do
      allow_nil? false
      public? true
    end
  end
  calculations do
    calculate :text_length, :integer, expr(string_length(text))
  end
  aggregates do
    count :count_of_comments, :comments
  end
  relationships do
    has_many :comments, MyApp.Posts.Comment do
      public? true
    end
  end
end
defmodule MyApp.Posts.Comment do
  use Ash.Resource,
    domain: MyApp.Posts,
    data_layer: Ash.DataLayer.Ets
  actions do
    defaults [:read, :destroy, create: :*]
  end
  
  attributes do
    uuid_primary_key :id
    attribute :text, :string do
      allow_nil? false
      public? true
    end
  end
  relationships do
    belongs_to :post, MyApp.Posts.Post do
      public? true
    end
  end
end{:module, MyApp.Posts.Comment, <<70, 79, 82, 49, 0, 0, 110, ...>>,
 [
   Ash.Expr,
   Ash.Resource.Dsl.Relationships.BelongsTo,
   Ash.Resource.Dsl.Relationships.ManyToMany,
   Ash.Resource.Dsl.Relationships.HasMany,
   Ash.Resource.Dsl.Relationships.HasOne,
   %{...}
 ]}# Get rid of any existing comments/posts
Ash.bulk_destroy!(MyApp.Posts.Comment, :destroy, %{})
Ash.bulk_destroy!(MyApp.Posts.Post, :destroy, %{})
# Create some posts
post1 =
  Ash.create!(MyApp.Posts.Post, %{text: "First post about Ash!"})
post2 =
  Ash.create!(MyApp.Posts.Post, %{text: "Learning to write queries"})
comment1 =
  Ash.create!(MyApp.Posts.Comment, %{text: "Great post!", post_id: post1.id})
comment2 =
  Ash.create!(MyApp.Posts.Comment, %{text: "Very helpful!", post_id: post1.id})
comment3 =
  Ash.create!(MyApp.Posts.Comment, %{text: "Thanks for the explanation", post_id: post2.id})
# Store the created records in module attributes for later use
posts = [post1, post2]
comments = [comment1, comment2, comment3]
IO.puts("\nCreated #{length(posts)} posts and #{length(comments)} comments!")
23:17:15.097 [debug] ETS: Destroying MyApp.Posts.Comment
23:17:15.104 [debug] ETS: Destroying MyApp.Posts.Post
23:17:15.110 [debug] Creating MyApp.Posts.Post:
%{id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b", text: "First post about Ash!"}
23:17:15.110 [debug] Creating MyApp.Posts.Post:
%{id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3", text: "Learning to write queries"}
23:17:15.111 [debug] Creating MyApp.Posts.Comment:
%{
  id: "05863bdd-38eb-4e0e-9ff7-f23f65639ec3",
  text: "Great post!",
  post_id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b"
}
23:17:15.111 [debug] Creating MyApp.Posts.Comment:
%{
  id: "437b6966-2929-4e12-94cc-5807adf60c3e",
  text: "Very helpful!",
  post_id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b"
}
23:17:15.111 [debug] Creating MyApp.Posts.Comment:
%{
  id: "09aaffc4-bca7-4848-8b05-2d1ca11aba33",
  text: "Thanks for the explanation",
  post_id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3"
}
Created 2 posts and 3 comments!:okBasic Queries
Let's start with some basic query examples. To use Ash.Query.filter/2, we'll need to
require Ash.Query.
require Ash.QueryAsh.QueryRead everything
# with a lot of data, you probably shouldn't do this
Ash.read!(MyApp.Posts.Post)[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    text: "Learning to write queries",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    text: "First post about Ash!",
    aggregates: %{},
    calculations: %{},
    ...
  >
]Count all comments
MyApp.Posts.Comment
|> Ash.count!()3Filtering
MyApp.Posts.Post
|> Ash.Query.filter(id == ^post1.id)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    text: "First post about Ash!",
    aggregates: %{},
    calculations: %{},
    ...
  >
]MyApp.Posts.Post
# you can filter on calculations
|> Ash.Query.filter(text_length == 25)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    text: "Learning to write queries",
    aggregates: %{},
    calculations: %{},
    ...
  >
]MyApp.Posts.Post
# you can filter on aggregates
|> Ash.Query.filter(count_of_comments == 2)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: 2,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    text: "First post about Ash!",
    aggregates: %{},
    calculations: %{},
    ...
  >
]MyApp.Posts.Post
# use `filter_input` to filter based on user input
# it only allows accessing public fields
|> Ash.Query.filter_input(%{count_of_comments: %{eq: 2}})
|> Ash.read!()Sorting
MyApp.Posts.Post
|> Ash.Query.sort(:text)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    text: "First post about Ash!",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    text: "Learning to write queries",
    aggregates: %{},
    calculations: %{},
    ...
  >
]# Apply multiple sorts
MyApp.Posts.Post
|> Ash.Query.sort(text: :asc, count_of_comments: :desc)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: 2,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    text: "First post about Ash!",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: 1,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    text: "Learning to write queries",
    aggregates: %{},
    calculations: %{},
    ...
  >
]# use `sort_input` to sort based on user input
# it only allows accessing public fields
MyApp.Posts.Post
|> Ash.Query.sort_input("text,-count_of_comments")
|> Ash.read!()Distinct
MyApp.Posts.Comment
# only one comment per post
|> Ash.Query.distinct(:post_id)
|> Ash.read!()[
  #MyApp.Posts.Comment<
    post: #Ash.NotLoaded<:relationship, field: :post>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "09aaffc4-bca7-4848-8b05-2d1ca11aba33",
    text: "Thanks for the explanation",
    post_id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  #MyApp.Posts.Comment<
    post: #Ash.NotLoaded<:relationship, field: :post>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "05863bdd-38eb-4e0e-9ff7-f23f65639ec3",
    text: "Great post!",
    post_id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    aggregates: %{},
    calculations: %{},
    ...
  >
]MyApp.Posts.Comment
# only one comment per post_id & text combination
|> Ash.Query.distinct([:post_id, :text])
|> Ash.read!()[
  #MyApp.Posts.Comment<
    post: #Ash.NotLoaded<:relationship, field: :post>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "09aaffc4-bca7-4848-8b05-2d1ca11aba33",
    text: "Thanks for the explanation",
    post_id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  #MyApp.Posts.Comment<
    post: #Ash.NotLoaded<:relationship, field: :post>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "05863bdd-38eb-4e0e-9ff7-f23f65639ec3",
    text: "Great post!",
    post_id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  #MyApp.Posts.Comment<
    post: #Ash.NotLoaded<:relationship, field: :post>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "437b6966-2929-4e12-94cc-5807adf60c3e",
    text: "Very helpful!",
    post_id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    aggregates: %{},
    calculations: %{},
    ...
  >
]Load calculations/aggregates
MyApp.Posts.Post
|> Ash.Query.load([:count_of_comments, :text_length])
|> Ash.read!()
|> Enum.map(&Map.take(&1, [:text, :count_of_comments, :text_length]))[
  %{text: "Learning to write queries", text_length: 25, count_of_comments: 1},
  %{text: "First post about Ash!", text_length: 21, count_of_comments: 2}
]Load relationships
MyApp.Posts.Post
|> Ash.Query.load(:comments)
|> Ash.read!()
|> Enum.at(0)#MyApp.Posts.Post<
  text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
  count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
  comments: [
    #MyApp.Posts.Comment<
      post: #Ash.NotLoaded<:relationship, field: :post>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "09aaffc4-bca7-4848-8b05-2d1ca11aba33",
      text: "Thanks for the explanation",
      post_id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
      aggregates: %{},
      calculations: %{},
      ...
    >
  ],
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
  text: "Learning to write queries",
  aggregates: %{},
  calculations: %{},
  ...
>Limit & Offset
MyApp.Posts.Post
|> Ash.Query.limit(1)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
    text: "Learning to write queries",
    aggregates: %{},
    calculations: %{},
    ...
  >
]MyApp.Posts.Post
|> Ash.Query.offset(1)
|> Ash.read!()[
  #MyApp.Posts.Post<
    text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
    count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
    comments: #Ash.NotLoaded<:relationship, field: :comments>,
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
    text: "First post about Ash!",
    aggregates: %{},
    calculations: %{},
    ...
  >
]Pagination
# Offset Pagination
MyApp.Posts.Post
|> Ash.Query.page(limit: 1)
|> Ash.read!()%Ash.Page.Offset{
  results: [
    #MyApp.Posts.Post<
      text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
      count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
      comments: #Ash.NotLoaded<:relationship, field: :comments>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "2f22973b-f1cd-4d2d-b241-d7c53bf097d3",
      text: "Learning to write queries",
      aggregates: %{},
      calculations: %{},
      ...
    >
  ],
  limit: 1,
  offset: 0,
  count: nil,
  more?: true
}# Keyset pagination
first_post = 
  MyApp.Posts.Post
  # You can paginate using `Ash.Query.page/1`
  |> Ash.Query.page(limit: 1)
  |> Ash.read!()
  |> Map.get(:results)
  |> Enum.at(0)
  
MyApp.Posts.Post
# Or using the `page` option
|> Ash.read!(page: [limit: 1, after: first_post.__metadata__.keyset])%Ash.Page.Keyset{
  results: [
    #MyApp.Posts.Post<
      text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
      count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
      comments: #Ash.NotLoaded<:relationship, field: :comments>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
      text: "First post about Ash!",
      aggregates: %{},
      calculations: %{},
      ...
    >
  ],
  count: nil,
  before: nil,
  after: "g2wAAAABbQAAACQyZjIyOTczYi1mMWNkLTRkMmQtYjI0MS1kN2M1M2JmMDk3ZDNq",
  limit: 1,
  more?: false
}MyApp.Posts.Post
|> Ash.Query.page(limit: 1)
|> Ash.read!()
# you can ask for :next, :prev, :first, :last, or a page number
|> Ash.page!(:next)%Ash.Page.Offset{
  results: [
    #MyApp.Posts.Post<
      text_length: #Ash.NotLoaded<:calculation, field: :text_length>,
      count_of_comments: #Ash.NotLoaded<:aggregate, field: :count_of_comments>,
      comments: #Ash.NotLoaded<:relationship, field: :comments>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "eafcb80f-8c90-4c16-8a29-cf0c28964d9b",
      text: "First post about Ash!",
      aggregates: %{},
      calculations: %{},
      ...
    >
  ],
  limit: 1,
  offset: 1,
  count: nil,
  more?: false
}