I"ve been working on translating a few queries over to Ash and hitting a wall in regards to query expressions.
I have a resource that needs to perform some atomic updates given some conditions. This is how it looks like with ecto:
from(r in Resource,
where: r.id == ^some_id,
where: r.max_size >= ^attrs.size,
where: r.count + ^attrs.size <= r.some_max or r.is_unlimited,
update: [
inc: [count: ^attrs.size]
],
select: r
)
I started by using simple changes + atomics for this, but not sure how to tie in the where clauses to the atomic updates.
So, we’re only just now getting this feature set in Ash, it’s fresh and still needs some improvements. However, our bulk update syntax ought to work for this.
Resource
|> Ash.Query.filter(.., %{size: size})
|> YourApi.bulk_update(:increment_size, %{size: size})
Then in your update action:
update :increment_size do
accept []
argument :size, :integer, allow_nil?: false
change atomic_update(:count, expr(count + ^arg(:size)))
end
Probably more idiomatic would be explicitly stating the read action.
Resource
|> Ash.Query.for_read(:some_read_action, %{size: size})
|> YourApi.bulk_update(:increment_size, %{size: size})
@zachdaniel That looks awesome!
Do I have to wrap the increment_size
in another action for the bulk updates?
Ideally I would want something like Resource.some_domain_action() to do both the increment and bulk update.
@zachdaniel Just to add context, this is for updating 1 resource, its not really a bulk update, it was just done this way because of how atomic updates work on Ecto. What is happening here is that I have a resource that serves as an aggregate, and some fields need to be atomic on it to update its children resources.
Ah, okay, then you can simplify that into a single update, and use if
in the atomic update.
change atomic_update(:field, if .... do
value + something
else
value
end)
If you wanted to actually wrap a query + bulk update in an action, you’d create a generic action for that. AFAIK though we don’t currently have support for generic actions in AshJsonApi, but its more than possible.
1 Like
What can go in the if
clause? Can I check against all fields in the resource? Is value the current value of the atomic field?
Here is what a conditional increment would look like:
change atomic_update(:field, if some_other_field > ^arg(:some_argument) do
field + ^arg(:amount)
else
field
end)
field
refers to the current value of that field 
@zachdaniel Doesnt seem to compile when I reference any field(undefined variable)
right, sorry,
change atomic_update(:field, expr(if some_other_field > ^arg(:some_argument) do
field + ^arg(:amount)
else
field
end))
Nice, this works!
Is it possible to add an error when the expr conditions dont match? like adding an error in the else clause? Right now if there is no else, it nullifies the column
Hmm…it shouldn’t nullify it, it should set it to the previous value right?
Its currently setting the field to null, this is what I have:
change(fn changeset, _ ->
changeset =
Ash.Changeset.atomic_update(
changeset,
:count,
expr(
if(count + ^arg(:max_size) <= some_value or is_unlimited) do
count + ^arg(:max_size)
end
)
)
end)
I think what I want to happen is that if the preconditions of the atomic update are not met, I get some sort of error, ideally something I can set to callers can take action on the failed update
Keeping in mind that this is a brand new feature set, you can actually use the new error
expression.
change atomic_update(changeset, :count, expr(
if is_unlimited or count + ^arg(:max_size) <= some_value do
count + ^arg(:max_size)
else
error(
Ash.Error.Changes.InvalidArgument,
%{
field: :max_size,
value: ^arg(:max_size),
message: "some message using a %{replacement}",
vars: %{replacement: "foo"}
}
end
))
1 Like
That worked! Really nice. I did have to create a new migration for the “ash-functions” for it to work
hi @zachdaniel Not sure what changed with the recent version of Ash, but instead of returning an error tuple, actions with atomic updates using the error macro raise a {:error, :norollback, _someerror} error instead. Is this the new behaviour?
Here is an example stack trace:
** (Ash.Error.Unknown) Unknown Error
* ** (CaseClauseError) no case clause matching: {:error, :no_rollback, %Ash.Error.Changes.InvalidAttribute{field: :my_count, message: "error message", private_vars: nil, value: "my_count", changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}}
(ash 2.20.1) lib/ash/actions/update/update.ex:432: anonymous fn/5 in Ash.Actions.Update.commit/3
(ash 2.20.1) lib/ash/changeset/changeset.ex:2892: Ash.Changeset.run_around_actions/2
(ash 2.20.1) lib/ash/changeset/changeset.ex:2475: anonymous fn/3 in Ash.Changeset.with_hooks/3
(ecto_sql 3.11.1) lib/ecto/adapters/sql.ex:1358: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
(db_connection 2.6.0) lib/db_connection.ex:1710: DBConnection.run_transaction/4
(ash 2.20.1) lib/ash/changeset/changeset.ex:2473: anonymous fn/3 in Ash.Changeset.with_hooks/3
(ash 2.20.1) lib/ash/changeset/changeset.ex:2612: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
(ash 2.20.1) lib/ash/changeset/changeset.ex:2454: Ash.Changeset.with_hooks/3
(ash 2.20.1) lib/ash/actions/update/update.ex:354: Ash.Actions.Update.commit/3
(ash 2.20.1) lib/ash/actions/update/update.ex:229: Ash.Actions.Update.do_run/4
(ash 2.20.1) lib/ash/actions/update/update.ex:188: Ash.Actions.Update.run/4
Can you try main
of ash
? Just pushed up a fix.
Just tested this out on main and its fixed! Thanks for the quick fix!