Add filter option to update/destroy actions - #2774
Open
Ryan6794 wants to merge 1 commit into
Open
Conversation
zachdaniel
reviewed
Jul 2, 2026
| @spec filter(expr :: Ash.Expr.t()) :: Ash.Resource.Change.ref() | ||
| def filter(filter) do | ||
| @spec filter_where(expr :: Ash.Expr.t()) :: Ash.Resource.Change.ref() | ||
| def filter_where(filter) do |
Contributor
There was a problem hiding this comment.
We can't change this, as it would be a breaking change. We would either need to call the DSL builder something else, or save this change for 4.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contributor checklist
Leave anything that you believe does not apply unchecked.
Implements #1350 - adds a
filteroption toupdateanddestroyactions, bringing them to parity withreadactions which have supported this since early on.What this enables
The filter is enforced at the action level and cannot be bypassed by the caller. Multiple filters can be declared and are combined with
and. Filters support^actor/1,^arg/1and^context/1templates, identical to read action filters.Changes
lib/ash/resource/actions/update.exandlib/ash/resource/actions/destroy.exBoth action structs gain two new fields:
filters(plural) which holds the raw list of filter entities as declared in the DSL, andfilter(singular) which holds the final merged expression. Thetransform/1callback now callsconcat_filters/1which collapses the list into a single expression joined with:and, or leavesfilterasnilif none were declared. This is the same pattern theReadaction already uses.lib/ash/resource/dsl.exAdds
filters: [@filter]to the entities list for both@updateand@destroyDSL entities. This is what makesfilter expr(...)valid syntax insideupdateanddestroyblocks. The@filterentity already existed and was already used by@read- this change simply registers it for the two new contexts.lib/ash/changeset/changeset.exAdds
|> apply_action_filter(action)at the end ofprepare_changeset_for_action/3. This is the function that wires a changeset to its action before any user-provided changes are applied - making it the right place to apply an action-level filter that callers cannot override. The privateapply_action_filter/2helper usesMap.get/2rather than direct field access so it is safe when called forcreateactions, which do not have afilterfield.lib/ash/actions/update/bulk.exandlib/ash/actions/destroy/bulk.exIn bulk operations, Ash runs a read query first to fetch the records it will update or destroy. By applying
action.filterto that read query viaAsh.Query.do_filter/2, we ensure only eligible records are ever fetched from the data layer. Without this, the filter would still be enforced per-changeset, but Ash would wastefully fetch ineligible records first and then reject them one by one. This satisfies the requirement in the issue that the filter be "lifted to the read action when used in a bulk update/destroy scenario."lib/ash/resource/change/builtins.exRenames
filter/1tofilter_where/1. This was required to resolve a compile error introduced by the DSL change above. When Spark processes afilters: [@filter]child entity inside@updateor@destroy, it auto-generates a module and injects afilter/1macro into the DSL scope of that action. The existingChange.Builtins.filter/1was also imported into that same scope, causing Elixir to raise a conflicting import error. Since Spark'simports:field only accepts plain module names with noexceptsupport, the only clean resolution is to rename the builtin.filter_whereis also a more descriptive name - it reads as "apply this change only where this condition is true."test/actions/bulk/bulk_update_test.exsandtest/actions/bulk/bulk_destroy_test.exsUpdated two test resources that used
change filter(...)to use the renamedchange filter_where(...)instead.