Ash.Query (ash v3.5.24)

View Source

A data structure for reading data from a resource.

Queries are run by calling Ash.read/2.

Examples:

require Ash.Query

MyApp.Post
|> Ash.Query.filter(likes > 10)
|> Ash.Query.sort([:title])
|> Ash.read!()

MyApp.Author
|> Ash.Query.aggregate(:published_post_count, :posts, query: [filter: [published: true]])
|> Ash.Query.sort(published_post_count: :desc)
|> Ash.Query.limit(10)
|> Ash.read!()

MyApp.Author
|> Ash.Query.load([:post_count, :comment_count])
|> Ash.Query.load(posts: [:comments])
|> Ash.read!()

To see more examples of what you can do with Ash.Query and read actions in general, see the writing queries how-to guide.

Capabilities & Limitations

Ash Framework provides a comprehensive suite of querying tools designed to address common application development needs. While powerful and flexible, these tools are focused on domain-driven design rather than serving as a general-purpose ORM.

Ash's query tools support:

  • Filtering records based on complex conditions
  • Sorting results using single or multiple criteria
  • Setting result limits and offsets
  • Pagination, with offset/limit and keysets
  • Selecting distinct records to eliminate duplicates
  • Computing dynamic properties at query time
  • Aggregating data from related resources

While Ash's query tools often eliminate the need for direct database queries, Ash is not itself designed to be a comprehensive ORM or database query builder.

For specialized querying needs that fall outside Ash's standard capabilities, the framework provides escape hatches. These mechanisms allow developers to implement custom query logic when necessary.

Important Considerations

  1. Ash is primarily a domain modeling framework, not a database abstraction layer
  2. While comprehensive, the tooling is intentionally constrained to resource-oriented access
  3. Escape hatches exist for cases that require custom query logic

For complex queries that fall outside these tools, consider whether they represent domain concepts that could be modeled differently, or if they truly require custom implementation through escape hatches.

Escape Hatches

Many of the tools in Ash.Query are surprisingly deep and capable, covering everything you need to build your domain logic. With that said, these tools are not designed to encompass every kind of query that you could possibly want to write over your data. Ash is not an ORM or a database query tool, despite the fact that its query building tools often make those kinds of tools unnecessary in all but the rarest of cases. Not every kind of query that you could ever wish to write can be expressed with Ash.Query. Elixir has a best-in-class library for working directly with databases, called Ecto, and if you end up building a certain type of feature like analytics or reporting dashboards, you may find yourself working directly with Ecto. Data layers like AshPostgres are built on top of Ecto. In fact, every Ash.Resource is an Ecto.Schema!

Choose escape hatches wisely

You should choose to use Ash builtin functionality wherever possible. Barring that, you should choose the least powerful escape hatch that can solve your problem. The options below are presented in the order that you should prefer them, but you should only use any of them if no builtin tooling will suffice.

Fragments

Fragments only barely count as an escape hatch. You will often find yourself wanting to use a function or operator specific to your data layer, and fragments are purpose built to this end. You can use data-layer-specific expressions in your expressions for filters, calculations, etc. For example:

Resource
|> Ash.Query.filter(expr(fragment("lower(?)", name) == "fred"))
|> Ash.Query.filter(expr(fragment("? @> ?", tags, ["important"])))

Manual Read Actions

See the manual read actions guide.

Ash.Resource.Dsl.actions.read.modify_query

When running read actions, you can modify the underlying data layer query directly, which can solve for cases when you cannot express your query using the standard Ash query interface.

actions do
  read :complex_search do
    argument
    modify_query {SearchMod, :modify, []}
  end
end
defmodule SearchMod do
  def modify(ash_query, data_layer_query) do
    # Here you can modify the underlying data layer query directly
    # For example, with AshPostgres you get access to the Ecto query
    {:ok, Ecto.Query.where(data_layer_query, [p], fragment("? @@ plainto_tsquery(?)", p.search_vector, ^ash_query.arguments.search_text))}
  end
end

Using Ecto directly

For data layers like AshPostgres, you can interact directly with Ecto. You can do this by using the Ash.Resource as its corresponding Ecto.Schema, like so:

import Ecto.Query

query =
  from p in MyApp.Post,
    where: p.likes > 100,
    select: p

 MyApp.Repo.all(query)

Or you can build an Ash.Query, and get the corresponding ecto query:

MyApp.Post
|> Ash.Query.for_read(:read)
|> Ash.data_layer_query()
|> case do
  {:ok, %{query: ecto_query}} ->
    ecto_query
    |> Ecto.Query.where([p], p.likes > 100)
    |> MyApp.Repo.all()

  {:error, error} ->
    {:error, error}
end

Summary

Types

Function type for around_action hooks that modify query execution flow.

Callback function type that takes a query and returns an around_result.

Result type for around_transaction hooks, containing either successful records or an error.

Function type for around_transaction hooks that wrap query execution in a transaction.

t()

A query struct for reading data from a resource.

Functions

Returns a list of attributes, aggregates, relationships, and calculations that are being loaded

Add an error to the errors list and mark the query as invalid.

Adds an after_action hook to the query.

Adds an aggregation to the query.

Applies a query to a list of records in memory.

Adds an around_transaction hook to the query.

Adds a before_action hook to the query.

Builds a query from a keyword list.

Removes a result set previously with set_result/2

Produces a query that is the combination of multiple queries.

Return the underlying data layer query for an ash query

Apply a sort only if no sort has been specified yet.

Remove an argument from the query

Ensures that the specified attributes are nil in the query results.

Get results distinct on the provided fields.

Set a sort to determine how distinct records are selected.

Ensures that the given attributes are selected.

Determines if the filter statement of a query is equivalent to the provided expression.

Same as equivalent_to/2 but always returns a boolean. :maybe returns false.

Fetches the value of an argument provided to the query.

Attach a filter statement to the query.

Attach a filter statement to the query labelled as user input.

Creates a query for a given read action and prepares it.

Gets the value of an argument provided to the query.

Limits the number of results returned from the query.

Loads relationships, calculations, or aggregates on the resource.

Adds a resource calculation to the query as a custom calculation with the provided name.

Adds a load statement to the result of an attribute or calculation.

Returns true if the field/relationship or path to field/relationship is being loaded.

Lock the query results.

Merges two query's load statements, for the purpose of handling calculation requirements.

Creates a new query for the given resource.

Skips the first n records in the query results.

Sets the pagination options of the query.

Sets a specific context key to a specific value.

Ensure that only the specified attributes are present in the results.

Checks if a specific field is currently selected in the query.

Adds an argument to the query.

Merge a map of arguments to the arguments list

Merge a map of values into the query context

Set the query's domain, and any loaded query's domain

Set the result of the action. This will prevent running the underlying datalayer behavior

Sets the tenant for the query.

Sort the results based on attributes, aggregates or calculations.

Attach a sort statement to the query labelled as user input.

Determines if the provided expression would return data that is a suprset of the data returned by the filter on the query.

Same as subset_of/2 but always returns a boolean. :maybe returns false.

Determines if the provided expression would return data that is a subset of the data returned by the filter on the query.

Same as superset_of/2 but always returns a boolean. :maybe returns false.

Set a timeout for the query.

Removes a field from the list of fields to load

Removes specified keys from the query, resetting them to their default values.

Types

around_action_fun()

@type around_action_fun() :: (t(), around_callback() -> around_result())

Function type for around_action hooks that modify query execution flow.

around_callback()

@type around_callback() :: (t() -> around_result())

Callback function type that takes a query and returns an around_result.

around_result()

@type around_result() :: {:ok, [Ash.Resource.record()]} | {:error, Ash.Error.t()}

Result type for around_transaction hooks, containing either successful records or an error.

around_transaction_fun()

@type around_transaction_fun() :: (t() ->
                               {:ok, Ash.Resource.record()} | {:error, any()})

Function type for around_transaction hooks that wrap query execution in a transaction.

t()

@type t() :: %Ash.Query{
  __validated_for_action__: atom() | nil,
  action: Ash.Resource.Actions.Read.t() | nil,
  action_failed?: boolean(),
  after_action: [
    (t(), [Ash.Resource.record()] ->
       {:ok, [Ash.Resource.record()]}
       | {:ok, [Ash.Resource.record()], [Ash.Notifier.Notification.t()]}
       | {:error, any()})
  ],
  aggregates: %{optional(atom()) => Ash.Filter.t()},
  arguments: %{optional(atom()) => any()},
  around_transaction: term(),
  authorize_results: [
    (t(), [Ash.Resource.record()] ->
       {:ok, [Ash.Resource.record()]} | {:error, any()})
  ],
  before_action: [(t() -> t())],
  calculations: %{optional(atom()) => :wat},
  combination_of: [Ash.Query.Combination.t()],
  context: map(),
  distinct: [atom()],
  distinct_sort: term(),
  domain: module() | nil,
  errors: [Ash.Error.t()],
  filter: Ash.Filter.t() | nil,
  invalid_keys: term(),
  limit: nil | non_neg_integer(),
  load: keyword(keyword()),
  load_through: term(),
  lock: term(),
  offset: non_neg_integer(),
  page: keyword() | nil | false,
  params: %{optional(atom() | binary()) => any()},
  phase: :preparing | :before_action | :after_action | :executing,
  resource: module(),
  select: nil | [atom()],
  sort: [atom() | {atom(), :asc | :desc}],
  sort_input_indices: term(),
  tenant: term(),
  timeout: pos_integer() | nil,
  to_tenant: term(),
  valid?: boolean()
}

A query struct for reading data from a resource.

Contains all the configuration needed to read data including filters, sorting, pagination, field selection, and relationship loading. Built incrementally through functions like filter/2, sort/2, load/2, etc.

Functions

accessing(query, types \\ [:attributes, :relationships, :calculations, :aggregates], only_public? \\ true)

Returns a list of attributes, aggregates, relationships, and calculations that are being loaded

Provide a list of field types to narrow down the returned results.

add_error(query, path \\ [], error)

@spec add_error(t(), path :: Ash.Error.path_input(), Ash.Error.error_input()) :: t()

Add an error to the errors list and mark the query as invalid.

See Ash.Error.to_ash_error/3 for more on supported values for error

Inconsistencies

The path argument is the second argument here, but the third argument in Ash.ActionInput.add_error/2 and Ash.Changeset.add_error/2. This will be fixed in 4.0.

after_action(query, func)

@spec after_action(
  t(),
  (t(), [Ash.Resource.record()] ->
     {:ok, [Ash.Resource.record()]}
     | {:ok, [Ash.Resource.record()], [Ash.Notifier.Notification.t()]}
     | {:error, term()})
) :: t()

Adds an after_action hook to the query.

After action hooks are called with the query and the list of records returned from the action. They can modify the records, perform side effects, or return errors to halt processing. The hook can return notifications alongside the records.

Examples

# Transform records after loading
iex> query = MyApp.Post
...> |> Ash.Query.after_action(fn query, records ->
...>   enriched_records = Enum.map(records, &add_computed_field/1)
...>   {:ok, enriched_records}
...> end)

# Log successful reads
iex> query = MyApp.Post
...> |> Ash.Query.after_action(fn query, records ->
...>   IO.puts("Successfully loaded #{length(records)} posts")
...>   {:ok, records}
...> end)

# Add notifications after the action
iex> query = MyApp.Post
...> |> Ash.Query.after_action(fn query, records ->
...>   notifications = create_read_notifications(records)
...>   {:ok, records, notifications}
...> end)

# Validate results and potentially error
iex> query = MyApp.Post
...> |> Ash.Query.after_action(fn query, records ->
...>   if Enum.any?(records, &restricted?/1) do
...>     {:error, "Access denied to restricted posts"}
...>   else
...>     {:ok, records}
...>   end
...> end)

See also

aggregate(query, name, kind, relationship)

@spec aggregate(t() | Ash.Resource.t(), atom() | String.t(), atom(), atom()) :: t()

Adds an aggregation to the query.

Aggregations are made available on the aggregates field of the records returned. They allow you to compute values from related data without loading entire relationships, making them very efficient for statistical operations.

Examples

# Count related records
iex> Ash.Query.aggregate(MyApp.Author, :post_count, :count, :posts)
%Ash.Query{aggregates: %{post_count: %Ash.Query.Aggregate{...}}, ...}

# Sum values from related records
iex> Ash.Query.aggregate(MyApp.Author, :total_likes, :sum, :posts, field: :like_count)
%Ash.Query{aggregates: %{total_likes: %Ash.Query.Aggregate{...}}, ...}

# Average with filtered aggregation
iex> published_query = Ash.Query.filter(MyApp.Post, published: true)
iex> Ash.Query.aggregate(MyApp.Author, :avg_published_likes, :avg, :posts,
...>   field: :like_count, query: published_query)
%Ash.Query{aggregates: %{avg_published_likes: %Ash.Query.Aggregate{...}}, ...}

# Count with default value
iex> Ash.Query.aggregate(MyApp.Author, :post_count, :count, :posts, default: 0)
%Ash.Query{aggregates: %{post_count: %Ash.Query.Aggregate{...}}, ...}

Options

  • query - The query over the destination resource to use as a base for aggregation
  • field - The field to use for the aggregate. Not necessary for all aggregate types
  • default - The default value to use if the aggregate returns nil
  • filterable? - Whether or not this aggregate may be referenced in filters
  • type - The type of the aggregate
  • constraints - Type constraints for the aggregate's type
  • implementation - An implementation used when the aggregate kind is custom
  • read_action - The read action to use on the destination resource
  • authorize? - Whether or not to authorize access to this aggregate
  • join_filters - A map of relationship paths to filter expressions

See also

aggregate(query, name, kind, relationship, opts)

@spec aggregate(
  t() | Ash.Resource.t(),
  atom() | String.t(),
  atom(),
  atom(),
  Keyword.t()
) :: t()

apply_to(query, records, opts \\ [])

@spec apply_to(t(), records :: [Ash.Resource.record()], opts :: Keyword.t()) ::
  {:ok, [Ash.Resource.record()]}

Applies a query to a list of records in memory.

This function takes a query and applies its filters, sorting, pagination, and loading operations to an existing list of records in memory rather than querying the data layer. Useful for post-processing records or applying query logic to data from multiple sources.

Examples

# Apply filtering to records in memory
iex> records = [%MyApp.Post{title: "A", published: true}, %MyApp.Post{title: "B", published: false}]
iex> query = MyApp.Post |> Ash.Query.filter(published: true)
iex> Ash.Query.apply_to(query, records)
{:ok, [%MyApp.Post{title: "A", published: true}]}

# Apply sorting and limiting
iex> records = [%MyApp.Post{title: "C", likes: 5}, %MyApp.Post{title: "A", likes: 10}]
iex> query = MyApp.Post |> Ash.Query.sort(likes: :desc) |> Ash.Query.limit(1)
iex> Ash.Query.apply_to(query, records)
{:ok, [%MyApp.Post{title: "A", likes: 10}]}

# Apply with loading relationships
iex> records = [%MyApp.Post{id: 1}, %MyApp.Post{id: 2}]
iex> query = MyApp.Post |> Ash.Query.load(:author)
iex> Ash.Query.apply_to(query, records, domain: MyApp.Blog)
{:ok, [%MyApp.Post{id: 1, author: %MyApp.User{...}}, ...]}

Options

  • domain - The domain to use for loading relationships
  • actor - The actor for authorization during loading
  • tenant - The tenant for multitenant operations
  • parent - Parent context for nested operations

See also

  • Ash.read/2 for querying the data layer directly
  • load/3 for configuring relationship loading
  • filter/2 for adding filter conditions

around_transaction(query, func)

@spec around_transaction(t(), around_transaction_fun()) :: t()

Adds an around_transaction hook to the query.

Your function will get the query, and a callback that must be called with a query (that may be modified). The callback will return {:ok, results} or {:error, error}. You can modify these values, but the return value must be one of those types.

The around_transaction calls happen first, and then (after they each resolve their callbacks) the before_action hooks are called, followed by the after_action hooks being run. Then, the code that appeared after the callbacks were called is then run.

Examples

# Add logging around the transaction
iex> query = MyApp.Post |> Ash.Query.around_transaction(fn query, callback ->
...>   IO.puts("Starting transaction for #{inspect(query.resource)}")
...>   result = callback.(query)
...>   IO.puts("Transaction completed: #{inspect(result)}")
...>   result
...> end)

# Add error handling and retry logic
iex> query = MyApp.Post |> Ash.Query.around_transaction(fn query, callback ->
...>   case callback.(query) do
...>     {:ok, results} = success -> success
...>     {:error, %{retryable?: true}} ->
...>       callback.(query)  # Retry once
...>     error -> error
...>   end
...> end)

Warning

Using this without understanding how it works can cause big problems. You must call the callback function that is provided to your hook, and the return value must contain the same structure that was given to you, i.e {:ok, result_of_action}.

See also

before_action(query, func, opts \\ [])

@spec before_action(
  query :: t(),
  fun :: (t() -> t() | {t(), [Ash.Notifier.Notification.t()]}),
  opts :: Keyword.t()
) :: t()

Adds a before_action hook to the query.

Before action hooks are called after preparations but before the actual data layer query is executed. They receive the prepared query and can modify it or perform side effects before the action runs.

Examples

# Add validation before the query runs
iex> query = MyApp.Post
...> |> Ash.Query.before_action(fn query ->
...>   if Enum.empty?(query.sort) do
...>     Ash.Query.sort(query, :created_at)
...>   else
...>     query
...>   end
...> end)

# Add logging before the action
iex> query = MyApp.Post
...> |> Ash.Query.before_action(fn query ->
...>   IO.puts("Executing query for #{length(query.filter || [])} filters")
...>   query
...> end)

# Prepend a hook to run first
iex> query = MyApp.Post
...> |> Ash.Query.before_action(&setup_query/1)
...> |> Ash.Query.before_action(&early_validation/1, prepend?: true)

Options

  • prepend? - when true, places the hook before all other hooks instead of after

See also

build(resource, domain \\ nil, keyword)

@spec build(Ash.Resource.t() | t(), Ash.Domain.t() | nil, Keyword.t()) :: t()

Builds a query from a keyword list.

This is used by certain query constructs like aggregates. It can also be used to manipulate a data structure before passing it to an ash query. It allows for building an entire query struct using only a keyword list.

For example:

Ash.Query.build(MyResource, filter: [name: "fred"], sort: [name: :asc], load: [:foo, :bar], offset: 10)

If you want to use the expression style filters, you can use expr/1.

For example:

import Ash.Expr, only: [expr: 1]

Ash.Query.build(Myresource, filter: expr(name == "marge"))

Options

  • :filter (term/0) - A filter keyword, map or expression

  • :filter_input (term/0) - A filter keyword or map, provided as input from an external source

  • :sort (term/0) - A sort list or keyword

  • :sort_input (term/0) - A sort list or keyword, provided as input from an external source

  • :default_sort (term/0) - A sort list or keyword to apply only if no other sort is specified, So if you apply any sort, this will be ignored.

  • :distinct_sort (term/0) - A distinct_sort list or keyword

  • :limit (integer/0) - A limit to apply

  • :offset (integer/0) - An offset to apply

  • :load (term/0) - A load statement to add to the query

  • :strict_load (term/0) - A load statement to add to the query with the strict? option set to true

  • :select (term/0) - A select statement to add to the query

  • :ensure_selected (term/0) - An ensure_selected statement to add to the query

  • :aggregate (term/0) - A custom aggregate to add to the query. Can be {name, type, relationship} or {name, type, relationship, build_opts}

  • :calculate (term/0) - A custom calculation to add to the query. Can be {name, module_and_opts} or {name, module_and_opts, context}

  • :distinct (list of atom/0) - A distinct clause to add to the query

  • :context (map/0) - A map to merge into the query context

calculate(query, name, type, module_and_opts, arguments \\ %{}, constraints \\ [], extra_context \\ %{}, new_calculation_opts \\ [])

@spec calculate(
  t() | Ash.Resource.t(),
  atom(),
  Ash.Type.t(),
  module() | {module(), Keyword.t()},
  map(),
  Keyword.t(),
  map(),
  Keyword.t()
) :: t()

Adds a calculation to the query.

Calculations are made available on the calculations field of the records returned. They allow you to compute dynamic values based on record data, other fields, or external information at query time.

The module_and_opts argument accepts either a module or a {module, opts}. For more information on what that module should look like, see Ash.Resource.Calculation.

Examples

# Add a simple calculation
iex> Ash.Query.calculate(MyApp.User, :display_name, :string,
...>   {MyApp.Calculations.DisplayName, []})
%Ash.Query{calculations: %{display_name: %{...}}, ...}

# Add calculation with arguments
iex> Ash.Query.calculate(MyApp.Post, :word_count, :integer,
...>   {MyApp.Calculations.WordCount, []}, %{field: :content})
%Ash.Query{calculations: %{word_count: %{...}}, ...}

# Add calculation with constraints and context
iex> Ash.Query.calculate(MyApp.Product, :discounted_price, :decimal,
...>   {MyApp.Calculations.Discount, []}, %{rate: 0.1},
...>   [precision: 2, scale: 2], %{currency: "USD"})
%Ash.Query{calculations: %{discounted_price: %{...}}, ...}

See also

  • Ash.Resource.Calculation for implementing custom calculations
  • aggregate/5 for computing values from related records
  • load/3 for loading predefined calculations from the resource
  • select/3 for controlling which fields are returned alongside calculations

clear_result(query)

@spec clear_result(t()) :: t()

Removes a result set previously with set_result/2

combination_of(query, combinations)

@spec combination_of(t(), Ash.Query.Combination.t() | [Ash.Query.Combination.t()]) ::
  t()

Produces a query that is the combination of multiple queries.

All aspects of the parent query are applied to the combination in total.

See Ash.Query.Combination for more on creating combination queries.

Example

# Top ten users not on a losing streak and top ten users who are not on a winning streak
User
|> Ash.Query.filter(active == true)
|> Ash.Query.combination_of([
  # must always begin with a base combination
  Ash.Query.Combination.base(
    sort: [score: :desc],
    filter: expr(not(on_a_losing_streak)),
    limit: 10
  ),
  Ash.Query.Combination.union(
    sort: [score: :asc],
    filter: expr(not(on_a_winning_streak)),
    limit: 10
  )
])
|> Ash.read!()

Select and calculations

There is no select available for combinations, instead the select of the outer query is used for each combination. However, you can use the calculations field in Ash.Query.Combination to add expression calculations. Those calculations can "overwrite" a selected attribute, or can introduce a new field. Note that, for SQL data layers, all combinations will be required to have the same number of fields in their SELECT statement, which means that if one combination adds a calculation, all of the others must also add that calculation.

In this example, we compute separate match scores

query = "fred"

User
|> Ash.Query.filter(active == true)
|> Ash.Query.combination_of([
  # must always begin with a base combination
  Ash.Query.Combination.base(
    filter: expr(trigram_similarity(user_name, ^query) >= 0.5),
    calculate: %{
      match_score: trigram_similarity(user_name, ^query)
    },
    sort: [
      calc(trigram_similarity(user_name, ^query), :desc)
    ],
    limit: 10
  ),
  Ash.Query.Combination.union(
    filter: expr(trigram_similarity(email, ^query) >= 0.5),
    calculate: %{
      match_score: trigram_similarity(email, ^query)
    },
    sort: [
      calc(trigram_similarity(email, ^query), :desc)
    ],
    limit: 10
  )
])
|> Ash.read!()

data_layer_query(ash_query, opts \\ [])

Return the underlying data layer query for an ash query

default_sort(query, sorts, opts \\ [])

@spec default_sort(t() | Ash.Resource.t(), Ash.Sort.t(), opts :: Keyword.t()) :: t()

Apply a sort only if no sort has been specified yet.

This is useful for providing default sorts that can be overridden.

Examples

# This will sort by name if no sort has been specified
Ash.Query.default_sort(query, :name)

# This will sort by name descending if no sort has been specified
Ash.Query.default_sort(query, name: :desc)

delete_argument(query, argument_or_arguments)

Remove an argument from the query

deselect(query, fields)

@spec deselect(t() | Ash.Resource.t(), [atom()]) :: t()

Ensures that the specified attributes are nil in the query results.

This function removes specified fields from the selection, causing them to be excluded from the query results. If no fields are currently selected (meaning all fields would be returned by default), this will first select all default fields and then remove the specified ones.

Examples

# Remove specific fields from results
iex> MyApp.Post |> Ash.Query.deselect([:content])
%Ash.Query{select: [:id, :title, :created_at, ...], ...}

# Remove multiple fields
iex> MyApp.Post |> Ash.Query.deselect([:content, :metadata])
%Ash.Query{select: [:id, :title, :created_at, ...], ...}

# Deselect from existing selection
iex> MyApp.Post
...> |> Ash.Query.select([:title, :content, :author_id])
...> |> Ash.Query.deselect([:content])
%Ash.Query{select: [:id, :title, :author_id], ...}

# Deselect empty list (no-op)
iex> MyApp.Post |> Ash.Query.deselect([])
%Ash.Query{...}

See also

  • select/3 for explicitly controlling field selection
  • ensure_selected/2 for adding fields without removing others
  • Primary key fields cannot be deselected and will always be included

distinct(query, distincts)

@spec distinct(t() | Ash.Resource.t(), Ash.Sort.t()) :: t()

Get results distinct on the provided fields.

Takes a list of fields to distinct on. Each call is additive, so to remove the distinct use unset/2.

Examples:

Ash.Query.distinct(query, [:first_name, :last_name])

Ash.Query.distinct(query, :email)

distinct_sort(query, sorts, opts \\ [])

Set a sort to determine how distinct records are selected.

If none is set, any sort applied to the query will be used.

This is useful if you want to control how the distinct records are selected without affecting (necessarily, it may affect it if there is no sort applied) the overall sort of the query

ensure_selected(query, fields)

@spec ensure_selected(t() | Ash.Resource.t(), [atom()] | atom()) :: t()

Ensures that the given attributes are selected.

The first call to select/2 will limit the fields to only the provided fields. Use ensure_selected/2 to say "select this field (or these fields) without deselecting anything else". This function is additive - it will not remove any fields that are already selected.

Examples

# Ensure specific fields are selected (additive)
iex> MyApp.Post |> Ash.Query.ensure_selected([:title])
%Ash.Query{select: [:id, :title, :content, :created_at], ...}

# Add to existing selection
iex> MyApp.Post
...> |> Ash.Query.select([:title])
...> |> Ash.Query.ensure_selected([:content, :author_id])
%Ash.Query{select: [:id, :title, :content, :author_id], ...}

# Ensure fields for relationship loading
iex> MyApp.Post
...> |> Ash.Query.ensure_selected([:author_id])
...> |> Ash.Query.load(:author)
%Ash.Query{select: [..., :author_id], load: [author: []], ...}

See also

  • select/3 for explicitly controlling field selection
  • deselect/2 for removing specific fields from selection
  • load/3 for loading relationships that may require specific fields

equivalent_to(query, expr)

(macro)

Determines if the filter statement of a query is equivalent to the provided expression.

This uses the satisfiability solver that is used when solving for policy authorizations. In complex scenarios, or when using custom database expressions, (like fragments in ash_postgres), this function may return :maybe. Use supserset_of? to always return a boolean.

equivalent_to?(query, expr)

(macro)

Same as equivalent_to/2 but always returns a boolean. :maybe returns false.

fetch_argument(query, argument)

@spec fetch_argument(t(), atom()) :: {:ok, term()} | :error

Fetches the value of an argument provided to the query.

Returns {:ok, value} if the argument exists, or :error if not found. This is the safer alternative to get_argument/2 when you need to distinguish between a nil value and a missing argument.

Examples

# Fetch an argument that exists
iex> query = Ash.Query.for_read(MyApp.Post, :published, %{since: ~D[2023-01-01]})
iex> Ash.Query.fetch_argument(query, :since)
{:ok, ~D[2023-01-01]}

# Fetch an argument that doesn't exist
iex> query = Ash.Query.for_read(MyApp.Post, :published, %{})
iex> Ash.Query.fetch_argument(query, :since)
:error

# Distinguish between nil and missing arguments
iex> query = Ash.Query.for_read(MyApp.Post, :search, %{query: nil})
iex> Ash.Query.fetch_argument(query, :query)
{:ok, nil}

See also

filter(query, filter)

(macro)

Attach a filter statement to the query.

The filter is applied as an "and" to any filters currently on the query. Filters allow you to specify conditions that records must meet to be included in the query results. Multiple filters on the same query are combined with "and" logic.

Examples

# Filter with simple equality
MyApp.Post
|> Ash.Query.filter(published: true)

# Filter with comparison operators
MyApp.Post
|> Ash.Query.filter(view_count > 100)

# Filter with complex expressions using do block
MyApp.Post
|> Ash.Query.filter do
  published == true and view_count > 100
end

See also

filter_input(query, filter)

Attach a filter statement to the query labelled as user input.

Filters added as user input (or filters constructed with Ash.Filter.parse_input) will honor any field policies on resources by replacing any references to the field with nil in cases where the actor should not be able to see the given field.

This function does not expect the expression style filter (because an external source could never reasonably provide that). Instead, use the keyword/map style syntax. For example:

expr(name == "fred")

could be any of

  • map syntax: %{"name" => %{"eq" => "fred"}}
  • keyword syntax: [name: [eq: "fred"]]

See Ash.Filter for more.

for_read(query, action_name, args \\ %{}, opts \\ [])

@spec for_read(t() | Ash.Resource.t(), atom(), map() | Keyword.t(), Keyword.t()) ::
  t()

Creates a query for a given read action and prepares it.

This function configures the query to use a specific read action with the provided arguments and options. The query will be validated and prepared according to the action's configuration, including applying preparations and action filters.

Multitenancy is not validated until an action is called. This allows you to avoid specifying a tenant until just before calling the domain action.

Examples

# Create a query for a simple read action
iex> Ash.Query.for_read(MyApp.Post, :read)
%Ash.Query{action: %{name: :read}, ...}

# Create a query with arguments for a parameterized action
iex> Ash.Query.for_read(MyApp.Post, :published, %{since: ~D[2023-01-01]})
%Ash.Query{action: %{name: :published}, arguments: %{since: ~D[2023-01-01]}, ...}

# Create a query with options
iex> Ash.Query.for_read(MyApp.Post, :read, %{}, actor: current_user, authorize?: true)
%Ash.Query{action: %{name: :read}, ...}

Options

  • :actor (term/0) - set the actor, which can be used in any Ash.Resource.Changes configured on the action. (in the context argument)

  • :scope (term/0) - A value that implements the Ash.Scope.ToOpts protocol, for passing around actor/tenant/context in a single value. See Ash.Scope.ToOpts for more.

  • :authorize? (boolean/0) - set authorize?, which can be used in any Ash.Resource.Changes configured on the action. (in the context argument)

  • :tracer (one or a list of module that adopts Ash.Tracer) - A tracer to use. Will be carried over to the action. For more information see Ash.Tracer.

  • :tenant (value that implements the Ash.ToTenant protocol) - set the tenant on the query

  • :load (term/0) - A load statement to apply to the query

  • :skip_unknown_inputs - A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use :* to indicate all unknown keys.

  • :context (map/0) - A map of context to set on the query. This will be merged with any context set on the query itself.

See also

get_argument(query, argument)

@spec get_argument(t(), atom()) :: term()

Gets the value of an argument provided to the query.

Returns the argument value if found, or nil if not found. Arguments can be provided when creating queries with for_read/4 and are used by action logic such as preparations and filters.

Examples

# Get an argument that exists
iex> query = Ash.Query.for_read(MyApp.Post, :published, %{since: ~D[2023-01-01]})
iex> Ash.Query.get_argument(query, :since)
~D[2023-01-01]

# Get an argument that doesn't exist
iex> query = Ash.Query.for_read(MyApp.Post, :published, %{})
iex> Ash.Query.get_argument(query, :since)
nil

# Arguments can be accessed by string or atom key
iex> query = Ash.Query.for_read(MyApp.Post, :search, %{"query" => "elixir"})
iex> Ash.Query.get_argument(query, :query)
"elixir"

See also

limit(query, limit)

@spec limit(t() | Ash.Resource.t(), nil | integer()) :: t()

Limits the number of results returned from the query.

This function sets the maximum number of records that will be returned when the query is executed. Useful for pagination and preventing large result sets from consuming too much memory.

Examples

# Limit to 10 results
iex> MyApp.Post |> Ash.Query.limit(10)
%Ash.Query{limit: 10, ...}

# Remove existing limit
iex> query |> Ash.Query.limit(nil)
%Ash.Query{limit: nil, ...}

# Use with other query functions
iex> MyApp.Post
...> |> Ash.Query.filter(published: true)
...> |> Ash.Query.sort(:created_at)
...> |> Ash.Query.limit(5)
%Ash.Query{limit: 5, ...}

See also

  • offset/2 for skipping records (pagination)
  • page/2 for keyset pagination
  • sort/3 for ordering results before limiting

load(query, load_statement, opts \\ [])

Loads relationships, calculations, or aggregates on the resource.

By default, loading attributes has no effect, as all attributes are returned. See the section below on "Strict Loading" for more.

Examples

# Load simple relationships
iex> Ash.Query.load(MyApp.Post, :author)
%Ash.Query{load: [author: []], ...}

# Load nested relationships
iex> Ash.Query.load(MyApp.Post, [comments: [:author, :ratings]])
%Ash.Query{load: [comments: [author: [], ratings: []]], ...}

# Load relationships with custom queries
iex> author_query = Ash.Query.filter(MyApp.User, active: true)
iex> Ash.Query.load(MyApp.Post, [comments: [author: author_query]])
%Ash.Query{load: [comments: [author: %Ash.Query{...}]], ...}

# Load calculations with arguments
iex> Ash.Query.load(MyApp.User, full_name: %{format: :last_first})
%Ash.Query{calculations: %{full_name: %Ash.Query.Calculation{...}}, ...}

Strict Loading

By passing strict?: true, only specified attributes will be loaded when passing a list of fields to fetch on a relationship, which allows for more optimized data-fetching.

# Only load specific fields on relationships
iex> Ash.Query.load(MyApp.Category, [:name, posts: [:title, :published_at]], strict?: true)
%Ash.Query{load: [posts: [:title, :published_at]], ...}

When using strict?: true and loading nested relationships, you must specify all the attributes you want to load alongside the nested relationships:

# Must include needed attributes when loading nested relationships strictly
iex> Ash.Query.load(MyApp.Post, [:title, :published_at, category: [:name]], strict?: true)
%Ash.Query{...}

See also

load_calculation_as(query, calc_name, as_name, opts_or_args \\ %{}, opts \\ [])

Adds a resource calculation to the query as a custom calculation with the provided name.

Example:

Ash.Query.load_calculation_as(query, :calculation, :some_name, args: %{}, load_through: [:foo])

load_through(query, type, name, load)

Adds a load statement to the result of an attribute or calculation.

Uses Ash.Type.load/5 to request that the type load nested data.

loading?(query, item)

Returns true if the field/relationship or path to field/relationship is being loaded.

It accepts an atom or a list of atoms, which is treated for as a "path", i.e:

Resource |> Ash.Query.load(friends: [enemies: [:score]]) |> Ash.Query.loading?([:friends, :enemies, :score])
iex> true

Resource |> Ash.Query.load(friends: [enemies: [:score]]) |> Ash.Query.loading?([:friends, :score])
iex> false

Resource |> Ash.Query.load(friends: [enemies: [:score]]) |> Ash.Query.loading?(:friends)
iex> true

lock(query, lock_type)

@spec lock(t() | Ash.Resource.t(), Ash.DataLayer.lock_type()) :: t()

Lock the query results.

This must be run while in a transaction, and is not supported by all data layers.

merge_query_load(left, right, context)

Merges two query's load statements, for the purpose of handling calculation requirements.

This should only be used if you are writing a custom type that is loadable. See the callback documentation for Ash.Type.merge_load/4 for more.

new(resource, opts \\ [])

@spec new(Ash.Resource.t() | t(), opts :: Keyword.t()) :: t()

Creates a new query for the given resource.

This is the starting point for building queries. The query will automatically include the resource's base filter and default context.

Examples

# Create a new query for a resource
iex> Ash.Query.new(MyApp.Post)
%Ash.Query{resource: MyApp.Post, ...}

# Create a query with options
iex> Ash.Query.new(MyApp.Post, domain: MyApp.Blog)
%Ash.Query{resource: MyApp.Post, domain: MyApp.Blog, ...}

# Pass an existing query (returns the query unchanged)
iex> query = Ash.Query.new(MyApp.Post)
iex> Ash.Query.new(query)
%Ash.Query{resource: MyApp.Post, ...}

See also

offset(query, offset)

@spec offset(t() | Ash.Resource.t(), nil | integer()) :: t()

Skips the first n records in the query results.

This function is often used for offset-based pagination, allowing you to skip a specified number of records from the beginning of the result set. Often used together with limit/2 to implement pagination.

Examples

# Skip the first 20 records
iex> MyApp.Post |> Ash.Query.offset(20)
%Ash.Query{offset: 20, ...}

# Remove existing offset
iex> query |> Ash.Query.offset(nil)
%Ash.Query{offset: 0, ...}

# Pagination example: page 3 with 10 items per page
iex> MyApp.Post
...> |> Ash.Query.sort(:created_at)
...> |> Ash.Query.offset(20)  # Skip first 20 (pages 1-2)
...> |> Ash.Query.limit(10)   # Take next 10 (page 3)
%Ash.Query{offset: 20, limit: 10, ...}

See also

  • limit/2 for limiting the number of results
  • page/2 for keyset pagination (more efficient for large datasets)
  • sort/3 for ordering results before offsetting

page(query, page_opts)

@spec page(t() | Ash.Resource.t(), Keyword.t() | nil | false) :: t()

Sets the pagination options of the query.

This function configures how results should be paginated when the query is executed. Ash supports both offset-based pagination (limit/offset) and keyset-based pagination (cursor-based), with keyset being more efficient for large datasets.

Examples

# Offset-based pagination (page 2, 10 items per page)
iex> MyApp.Post
...> |> Ash.Query.page(limit: 10, offset: 10)
%Ash.Query{page: [limit: 10, offset: 10], ...}

# Keyset pagination with before/after cursors
iex> MyApp.Post
...> |> Ash.Query.sort(:created_at)
...> |> Ash.Query.page(limit: 20, after: "cursor_string")
%Ash.Query{page: [limit: 20, after: "cursor_string"], ...}

# Disable pagination (return all results)
iex> MyApp.Post |> Ash.Query.page(nil)
%Ash.Query{page: nil, ...}

# Pagination with counting
iex> MyApp.Post |> Ash.Query.page(limit: 10, count: true)
%Ash.Query{page: [limit: 10, count: true], ...}

Pagination Types

Limit/offset pagination

  • :offset (non_neg_integer/0) - The number of records to skip from the beginning of the query

  • :limit (pos_integer/0) - The number of records to include in the page

  • :filter (term/0) - A filter to apply for pagination purposes, that should not be considered in the full count.
    This is used by the liveview paginator to only fetch the records that were already on the page when refreshing data, to avoid pages jittering.

  • :count (boolean/0) - Whether or not to return the page with a full count of all records

Keyset pagination

  • :before (String.t/0) - Get records that appear before the provided keyset (mutually exclusive with after)

  • :after (String.t/0) - Get records that appear after the provided keyset (mutually exclusive with before)

  • :limit (pos_integer/0) - How many records to include in the page

  • :filter (term/0) - See the filter option for offset pagination, this behaves the same.

  • :count (boolean/0) - Whether or not to return the page with a full count of all records

See also

  • limit/2 and offset/2 for simple pagination without page metadata
  • sort/3 for ordering results (required for keyset pagination)
  • Ash.read/2 for executing paginated queries

put_context(query, key, value)

@spec put_context(t() | Ash.Resource.t(), atom(), term()) :: t()

Sets a specific context key to a specific value.

Context is used to pass additional information through the query pipeline that can be accessed by preparations, calculations, and other query logic. This function adds or updates a single key in the query's context map.

Examples

# Add actor information to context
iex> query = MyApp.Post |> Ash.Query.put_context(:actor, current_user)
%Ash.Query{context: %{actor: %User{...}}, ...}

# Add custom metadata for preparations
iex> query = MyApp.Post |> Ash.Query.put_context(:source, "api")
%Ash.Query{context: %{source: "api"}, ...}

# Chain multiple context additions
iex> MyApp.Post
...> |> Ash.Query.put_context(:tenant, "org_123")
...> |> Ash.Query.put_context(:locale, "en_US")
%Ash.Query{context: %{tenant: "org_123", locale: "en_US"}, ...}

See also

  • set_context/2 for setting the entire context map
  • for_read/4 for passing context when creating queries
  • Preparations and calculations can access context for custom logic

select(query, fields, opts \\ [])

@spec select(t() | Ash.Resource.t(), [atom()] | atom(), Keyword.t()) :: t()

Ensure that only the specified attributes are present in the results.

The first call to select/2 will replace the default behavior of selecting all attributes. Subsequent calls to select/2 will combine the provided fields unless the replace? option is provided with a value of true.

If a field has been deselected, selecting it again will override that (because a single list of fields is tracked for selection)

Primary key attributes are always selected and cannot be deselected.

When attempting to load a relationship (or manage it with Ash.Changeset.manage_relationship/3), if the source field is not selected on the query/provided data an error will be produced. If loading a relationship with a query, an error is produced if the query does not select the destination field of the relationship.

Use ensure_selected/2 if you wish to make sure a field has been selected, without deselecting any other fields.

Examples

# Select specific attributes
iex> MyApp.Post |> Ash.Query.select([:title, :content])
%Ash.Query{select: [:id, :title, :content], ...}

# Select additional attributes (combines with existing selection)
iex> MyApp.Post
...> |> Ash.Query.select([:title])
...> |> Ash.Query.select([:content])
%Ash.Query{select: [:id, :title, :content], ...}

# Replace existing selection
iex> MyApp.Post
...> |> Ash.Query.select([:title])
...> |> Ash.Query.select([:content], replace?: true)
%Ash.Query{select: [:id, :content], ...}

See also

  • ensure_selected/2 for adding fields without deselecting others
  • deselect/2 for removing specific fields from selection
  • load/3 for loading relationships and calculations

selecting?(query, field)

@spec selecting?(t(), atom()) :: boolean()

Checks if a specific field is currently selected in the query.

Returns true if the field will be included in the query results, either because it's explicitly selected, it's selected by default, or it's a primary key field (which are always selected).

Examples

# Check selection when no explicit select is set (uses defaults)
iex> query = MyApp.Post |> Ash.Query.new()
iex> Ash.Query.selecting?(query, :title)
true

# Check selection with explicit select
iex> query = MyApp.Post |> Ash.Query.select([:title, :content])
iex> Ash.Query.selecting?(query, :title)
true
iex> Ash.Query.selecting?(query, :metadata)
false

# Primary key fields are always selected
iex> query = MyApp.Post |> Ash.Query.select([:title])
iex> Ash.Query.selecting?(query, :id)  # assuming :id is primary key
true

See also

set_argument(query, argument, value)

Adds an argument to the query.

Arguments are used by action logic such as preparations, filters, and other query modifications. They become available in filter templates and can be referenced in action configurations. Setting an argument after a query has been validated for an action will result in an error.

Examples

# Set an argument for use in action filters
iex> query = Ash.Query.new(MyApp.Post)
iex> Ash.Query.set_argument(query, :author_id, 123)
%Ash.Query{arguments: %{author_id: 123}, ...}

# Set multiple arguments by chaining
iex> MyApp.Post
...> |> Ash.Query.set_argument(:category, "tech")
...> |> Ash.Query.set_argument(:published, true)
%Ash.Query{arguments: %{category: "tech", published: true}, ...}

# Arguments are used in action preparations and filters
iex> query = MyApp.Post
...> |> Ash.Query.for_read(:by_author, %{author_id: 123})
...> |> Ash.Query.set_argument(:include_drafts, false)
%Ash.Query{arguments: %{author_id: 123, include_drafts: false}, ...}

See also

set_arguments(query, map)

Merge a map of arguments to the arguments list

set_context(query, map)

@spec set_context(t() | Ash.Resource.t(), map() | nil) :: t()

Merge a map of values into the query context

set_domain(query, domain)

Set the query's domain, and any loaded query's domain

set_result(query, result)

@spec set_result(t(), term()) :: t()

Set the result of the action. This will prevent running the underlying datalayer behavior

set_tenant(query, tenant)

@spec set_tenant(t() | Ash.Resource.t(), Ash.ToTenant.t()) :: t()

Sets the tenant for the query.

In multitenant applications, this function configures which tenant's data the query should operate on. The tenant value is used to filter data and ensure proper data isolation between tenants.

Examples

# Set tenant using a string identifier
iex> MyApp.Post |> Ash.Query.set_tenant("org_123")
%Ash.Query{tenant: "org_123", ...}

# Set tenant using a struct that implements Ash.ToTenant
iex> org = %MyApp.Organization{id: 456}
iex> MyApp.Post |> Ash.Query.set_tenant(org)
%Ash.Query{tenant: %MyApp.Organization{id: 456}, ...}

# Use with other query functions
iex> MyApp.Post
...> |> Ash.Query.set_tenant("org_123")
...> |> Ash.Query.filter(published: true)
%Ash.Query{tenant: "org_123", ...}

See also

sort(query, sorts, opts \\ [])

@spec sort(t() | Ash.Resource.t(), Ash.Sort.t(), opts :: Keyword.t()) :: t()

Sort the results based on attributes, aggregates or calculations.

Format

Your sort can be an atom, list of atoms, a keyword list, or a string. When an order is not specified, :asc is the default. See Sort Orders below for more on the available orders.

# sort by name ascending
Ash.Query.sort(query, :name)

# sort by name descending
Ash.Query.sort(query, name: :desc)

# sort by name descending with nils at the end
Ash.Query.sort(query, name: :desc_nils_last)

# sort by name descending, and title ascending
Ash.Query.sort(query, name: :desc, title: :asc)

# sort by name ascending
Ash.Query.sort(query, "name")

# sort by name descending, and title ascending
Ash.Query.sort(query, "-name,title")

# sort by name descending with nils at the end
Ash.Query.sort(query, "--name")

You can refer to related fields using the shorthand of "rel1.rel2.field". For example:

# sort by the username of the comment's author.
Ash.Query.sort(query, "comment.author.username")

# Use as an atom for keyword lists
Ash.Query.sort(query, "comment.author.username": :desc)

Expression Sorts

You can use the Ash.Expr.calc/2 macro to sort on expressions:

import Ash.Expr

# Sort on an expression
Ash.Query.sort(query, calc(count(friends), :desc))

# Specify a type (required in some cases when we can't determine a type)
Ash.Query.sort(query, [{calc(fragment("some_sql(?)", field, type: :string), :desc}])

Sort Strings

A comma separated list of fields to sort on, each with an optional prefix.

The prefixes are:

  • "+" - Same as no prefix. Sorts :asc.
  • "++" - Sorts :asc_nils_first
  • "-" - Sorts :desc
  • "--" - Sorts :desc_nils_last

For example

"foo,-bar,++baz,--buz"

A list of sort strings

Same prefix rules as above, but provided as a list.

For example:

["foo", "-bar", "++baz", "--buz"]

Calculations

Calculation inputs can be provided by providing a map. To provide both inputs and an order, use a tuple with the first element being the inputs, and the second element being the order.

Ash.Query.sort(query, full_name: %{separator: " "})

Ash.Query.sort(query, full_name: {%{separator: " "}, :asc})

Sort Orders

The available orders are:

  • :asc - Sort values ascending, with lowest first and highest last, and nil values at the end
  • :desc - Sort values descending, with highest first and lowest last, and nil values at the beginning
  • :asc_nils_first - Sort values ascending, with lowest first and highest last, and nil values at the beginning
  • :desc_nils_last - Sort values descending, with highest first and lowest last, and nil values at the end

Examples

Ash.Query.sort(query, [:foo, :bar])

Ash.Query.sort(query, [:foo, bar: :desc])

Ash.Query.sort(query, [foo: :desc, bar: :asc])

See the guide on calculations for more.

Options

  • prepend? - set to true to put your sort at the front of the list of a sort is already specified

sort_input(query, sorts, opts \\ [])

Attach a sort statement to the query labelled as user input.

Sorts added as user input (or filters constructed with Ash.Filter.parse_input) will honor any field policies on resources by replacing any references to the field with nil in cases where the actor should not be able to see the given field.

See Ash.Query.sort/3 for more information on accepted formats.

subset_of(query, expr)

(macro)

Determines if the provided expression would return data that is a suprset of the data returned by the filter on the query.

This uses the satisfiability solver that is used when solving for policy authorizations. In complex scenarios, or when using custom database expressions, (like fragments in ash_postgres), this function may return :maybe. Use subset_of? to always return a boolean.

subset_of?(query, expr)

(macro)

Same as subset_of/2 but always returns a boolean. :maybe returns false.

superset_of(query, expr)

(macro)

Determines if the provided expression would return data that is a subset of the data returned by the filter on the query.

This uses the satisfiability solver that is used when solving for policy authorizations. In complex scenarios, or when using custom database expressions, (like fragments in ash_postgres), this function may return :maybe. Use supserset_of? to always return a boolean.

superset_of?(query, expr)

(macro)

Same as superset_of/2 but always returns a boolean. :maybe returns false.

timeout(query, timeout)

@spec timeout(t(), pos_integer() | :infinity | nil) :: t()

Set a timeout for the query.

For more information, see the timeouts guide

unload(query, fields)

@spec unload(t(), [atom()]) :: t()

Removes a field from the list of fields to load

unset(query, keys)

@spec unset(Ash.Resource.t() | t(), atom() | [atom()]) :: t()

Removes specified keys from the query, resetting them to their default values.

This function allows you to "unset" or reset parts of a query back to their initial state. Useful when you want to remove filters, sorts, loads, or other query modifications while keeping the rest of the query intact.

Examples

# Remove multiple query aspects at once
iex> query = MyApp.Post
...> |> Ash.Query.filter(published: true)
...> |> Ash.Query.sort(:created_at)
...> |> Ash.Query.limit(10)
iex> Ash.Query.unset(query, [:filter, :sort, :limit])
%Ash.Query{filter: nil, sort: [], limit: nil, ...}

# Remove just the sort from a query
iex> query = MyApp.Post |> Ash.Query.sort([:title, :created_at])
iex> Ash.Query.unset(query, :sort)
%Ash.Query{sort: [], ...}

# Remove load statements
iex> query = MyApp.Post |> Ash.Query.load([:author, :comments])
iex> Ash.Query.unset(query, :load)
%Ash.Query{load: [], ...}

# Reset pagination settings
iex> query = MyApp.Post |> Ash.Query.limit(20) |> Ash.Query.offset(10)
iex> Ash.Query.unset(query, [:limit, :offset])
%Ash.Query{limit: nil, offset: 0, ...}

See also