Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 =
Expand Down
85 changes: 85 additions & 0 deletions test/atomics_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading