diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 3cf61ed5..fd44f44e 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -706,6 +706,9 @@ defmodule AshPostgres.DataLayer do def can?(resource, {:atomic, :upsert}), do: not AshPostgres.DataLayer.Info.repo(resource, :mutate).disable_atomic_actions?() + def can?(resource, {:atomic, :create}), + do: not AshPostgres.DataLayer.Info.repo(resource, :mutate).disable_atomic_actions?() + def can?(_, :upsert), do: true def can?(_, :changeset_filter), do: true @@ -2103,6 +2106,15 @@ defmodule AshPostgres.DataLayer do atomic_insert_values = if create_atomics != [] do + # Hydrate expressions to convert Ash.Query.Call structs to proper function structs + create_atomics = + Enum.map(create_atomics, fn {key, expr} -> + case Ash.Filter.hydrate_refs(expr, %{resource: resource, public?: false}) do + {:ok, hydrated_expr} -> {key, hydrated_expr} + {:error, error} -> raise Ash.Error.to_ash_error(error) + end + end) + query = from(row in source, as: ^0) query = diff --git a/test/atomics_test.exs b/test/atomics_test.exs index d5680ac8..ba705597 100644 --- a/test/atomics_test.exs +++ b/test/atomics_test.exs @@ -419,4 +419,89 @@ defmodule AshPostgres.AtomicsTest do end end ) + + describe "atomic create (create_atomics)" do + # Tests for atomic_set on create actions - supported in Ash 3.14+ + + test "atomic_set works on create with fragment subquery" do + # Create 3 initial posts + Enum.each(1..3, fn i -> + Post + |> Ash.Changeset.for_create(:create, %{title: "post_#{i}", price: i}) + |> Ash.create!() + end) + + # Use atomic_set to set score to count of existing posts + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "new_post", price: 10}) + |> Ash.Changeset.atomic_set( + :score, + expr(fragment("(SELECT count(*) FROM posts WHERE type = 'sponsored')")) + ) + |> Ash.create!() + + # Score should be 3 (count of existing sponsored posts when INSERT ran) + assert post.score == 3 + end + + test "atomic_set works on create with simple literal expression" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "test", price: 5}) + |> Ash.Changeset.atomic_set(:score, expr(42)) + |> Ash.create!() + + assert post.score == 42 + end + + test "atomic_set works on create with arithmetic expression" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "test", price: 10}) + |> Ash.Changeset.atomic_set(:score, expr(5 + 15)) + |> Ash.create!() + + assert post.score == 20 + end + + test "atomic_set on create overrides attributes when both are set" do + # If both attributes and atomic_set set a value, atomics should win + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "test", price: 5, score: 999}) + |> Ash.Changeset.atomic_set(:score, expr(100)) + |> Ash.create!() + + # The atomic expression should override the attribute value + assert post.score == 100 + end + + test "atomic_set on create works sequentially" do + # Create 2 initial posts + Enum.each(1..2, fn i -> + Post + |> Ash.Changeset.for_create(:create, %{title: "initial_#{i}", price: i}) + |> Ash.create!() + end) + + # Create posts one by one with atomic_set + results = + Enum.map(1..3, fn i -> + Post + |> Ash.Changeset.for_create(:create, %{title: "new_#{i}", price: i}) + |> Ash.Changeset.atomic_set( + :score, + expr(fragment("(SELECT count(*) FROM posts WHERE type = 'sponsored')")) + ) + |> Ash.create!() + end) + + # First post sees 2 existing posts + assert Enum.at(results, 0).score == 2 + # Subsequent posts see incrementing counts + assert Enum.at(results, 1).score == 3 + assert Enum.at(results, 2).score == 4 + end + end end