I have the following Task resource. Is there a way to auto increment the position value in case somebody does a App.ToDoList.Task.create!(%{content: "Mow the lawn"}) without providing a value for position?
defmodule App.ToDoList.Task do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
postgres do
table "tasks"
repo App.Repo
end
attributes do
uuid_primary_key :id
attribute :content, :string, allow_nil?: false
attribute :position, :integer, allow_nil?: false
end
actions do
defaults [:create, :read, :update, :destroy]
end
code_interface do
define_for App.ToDoList
define :read
define :create
define :update
define :destroy
define :by_id, get_by: [:id], action: :read
end
end
Are tasks unique on content? If so, you can do this on upsert.
create :create do
upsert? true
upsert_identity :unique_content
change atomic_update(:position, position + 1)
end
If not, however, you will have to get a bit more clever. Ash doesn’t support (because most data layers don’t support) using upsets on non-unique fields.
attributes do
uuid_primary_key :id
attribute :content, :string, allow_nil?: false
attribute :position, :integer, allow_nil?: false
end
actions do
defaults [:read, :update, :destroy]
create :create do
upsert? true
upsert_identity :unique_content
change atomic_update(:position, position + 1)
end
end
I’m guessing that Stefan actually wants something like SERIAL or using a sequence in Postgres. With your example, wouldn’t all newly created resources have the value 1 for the position if it even works because the position would be null in that case?
The count aggregate only works on related resource, it might be nice to have it work on the resource it self too like count(self) then it could be something like atomic_change(:position, expr(count(self) + 1))
oh, I see. The upsert would actually update the original with a count + 1, which you’re saying isn’t the idea here. @wintermeyer are you looking to create a new task w/ the count + 1 to distinguish it from the other ones? If so, yeah you’re looking for something different.
I am looking for a way to create a new Task without knowing the next available position. The position should be set by some sort of magic. The first table entry would have the position 1 and ever new entry would be +1. In that scenario tasks wouldn’t be deleted.
If it’s for an entire table, then it’s relatively easy.
attribute :position, :integer do
allow_nil? false
generated? true
end
I’d have to double check but I believe that is all that you need to have ash_postgres generate an auto incrementing column as :bigserial in the database.
However, if you want to do it scoped by a particular column like :list_id, there in’t an elegant way to do that with the database automatically. You will likely be best off implementing it as application logic
create :create do
# usually better to put these in modules, i.e `change Setposition`,
# this is inline to be a better example
change fn changeset, _ ->
Ash.Changeset.before_action(changeset, fn changeset ->
position =
Task
|> Ash.Query.filter(list_id == ^Ash.Changeset.get_attribute(changeset, :list_id))
|> Ash.Query.sort(inserted_at: :desc)
|> Ash.Query.lock(:for_update) # lock it to make this concurrency safe
|> ToDoList.read_one!()
|> case do
nil -> 0
%{position: position} -> position + 1
end
Ash.Changeset.force_change_attribute(changeset, :position, position)
end)
end
end
has_many :co_list_items, __MODULE__ do
source_attribute :list_id
destination_attribute :list_id
end
actions do
create :create do
change atomic_update(:position, expr(count(co_list_items) + 1))
end
end
Ah, right I remembered why I didn’t recommend that originally though. atomic_update changes on create only take effect in the case of an upsert. We don’t have a good way without making upstream improvements to ecto (which I would like to do potentially) to use expressions to determine values when creating.
Note: since items in todo list might be deleted I think it is better to use max instead of count, it will definitely be unique and higher than any previous value:
actions do
defaults [:read, :update, :destroy]
create :create do
change fn changeset, _ ->
Ash.Changeset.before_action(changeset, fn changeset ->
position =
App.ToDoList.Task
|> Ash.Query.sort(position: :desc)
|> Ash.Query.limit(1)
|> Ash.Query.lock(:for_update)
|> App.ToDoList.read_one!()
|> case do
nil -> 0
%{position: position} -> position + 1
end
Ash.Changeset.force_change_attribute(changeset, :position, position)
end)
end
end
end
It resulted in this error:
iex(40)> App.ToDoList.Task.create!(%{content: "Buy milk"})
[debug] QUERY OK db=0.4ms idle=726.6ms
begin []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:2404
[debug] QUERY OK source="tasks" db=0.5ms
SELECT t0."id", t0."content", t0."position" FROM "tasks" AS t0 ORDER BY t0."position" DESC FOR UPDATE OF t0 []
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:686
[debug] QUERY OK db=0.5ms
rollback []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:2404
** (Ash.Error.Invalid) Input Invalid
* expected at most one result but got at least 2.
Please ensure your action is configured with an appropriate filter to ensure a single result is returned.
(elixir 1.16.0) lib/process.ex:860: Process.info/2
(ash 2.19.3) lib/ash/error/exception.ex:59: Ash.Error.Invalid.MultipleResults.exception/1
(ash 2.19.3) lib/ash/api/api.ex:2637: Ash.Api.unwrap_one/1
(ash 2.19.3) lib/ash/api/api.ex:2619: Ash.Api.unwrap_one/1
(ash 2.19.3) lib/ash/api/api.ex:2583: Ash.Api.read_one/3
(ash 2.19.3) lib/ash/api/api.ex:2569: Ash.Api.read_one!/3
(app 0.1.0) lib/app/to_do_list/resources/task.ex:32: anonymous fn/1 in App.ToDoList.Task.change_0_generated_C8BC88BD834A7AACE31008D1AFF809B1/2
(ash 2.19.3) lib/ash/changeset/changeset.ex:2692: anonymous fn/2 in Ash.Changeset.run_before_actions/1
(elixir 1.16.0) lib/enum.ex:4842: Enumerable.List.reduce/3
(elixir 1.16.0) lib/enum.ex:2582: Enum.reduce_while/3
(ash 2.19.3) lib/ash/changeset/changeset.ex:2669: Ash.Changeset.run_before_actions/1
(ash 2.19.3) lib/ash/changeset/changeset.ex:2806: Ash.Changeset.run_around_actions/2
(ash 2.19.3) lib/ash/changeset/changeset.ex:2406: anonymous fn/3 in Ash.Changeset.with_hooks/3
...
Something definitely seems off there. If updating ash/ash_postgres to latest versions doesn’t fix it, then I think it’s a bug of some kind. Try removing Ash.Query.lock. Even if that doesn’t solve it, there is a bug. Please open an issue and I will address as soon as I have a chance.