Skip to content

improvement: support unions #543

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 3, 2025
Merged
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
48 changes: 40 additions & 8 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
@@ -615,6 +615,8 @@ defmodule AshPostgres.DataLayer do

@impl true
def can?(_, :async_engine), do: true
def can?(_, :combine), do: true
def can?(_, {:combine, _}), do: true
def can?(_, :bulk_create), do: true

def can?(_, :action_select), do: true
@@ -781,12 +783,24 @@ defmodule AshPostgres.DataLayer do
repo = AshSql.dynamic_repo(resource, AshPostgres.SqlImplementation, query)

with_savepoint(repo, query, fn ->
{:ok,
repo.all(
query,
AshSql.repo_opts(repo, AshPostgres.SqlImplementation, nil, nil, resource)
)
|> AshSql.Query.remap_mapped_fields(query)}
repo.all(
query,
AshSql.repo_opts(repo, AshPostgres.SqlImplementation, nil, nil, resource)
)
|> AshSql.Query.remap_mapped_fields(query)
|> then(fn results ->
if query.__ash_bindings__.context[:data_layer][:combination_of_queries?] do
Enum.map(results, fn result ->
struct(resource, result)
|> Map.put(:__meta__, %Ecto.Schema.Metadata{
state: :loaded
})
end)
else
results
end
end)
|> then(&{:ok, &1})
end)
end
rescue
@@ -1423,6 +1437,11 @@ defmodule AshPostgres.DataLayer do
AshSql.Query.resource_to_query(resource, AshPostgres.SqlImplementation, domain)
end

@impl true
def combination_of(combination_of, resource, domain) do
AshSql.Query.combination_of(combination_of, resource, domain, AshPostgres.SqlImplementation)
end

@impl true
def update_query(query, changeset, resource, options) do
repo = AshSql.dynamic_repo(resource, AshPostgres.SqlImplementation, changeset)
@@ -1627,7 +1646,7 @@ defmodule AshPostgres.DataLayer do

needs_to_join? =
requires_adding_inner_join? || query.distinct ||
query.limit || query.offset || has_exists?
query.limit || query.offset || has_exists? || query.combinations != []

query =
if needs_to_join? do
@@ -3253,7 +3272,20 @@ defmodule AshPostgres.DataLayer do

@impl true
def select(query, select, _resource) do
{:ok, from(row in query, select: struct(row, ^Enum.uniq(select)))}
if query.__ash_bindings__.context[:data_layer][:combination_query?] ||
query.__ash_bindings__.context[:data_layer][:combination_of_queries?] do
binding = query.__ash_bindings__.root_binding

query =
from(row in Ecto.Query.exclude(query, :select), select: %{})

Enum.reduce(select, query, fn field, query ->
from(row in query, select_merge: %{^field => field(as(^binding), ^field)})
end)
|> then(&{:ok, &1})
else
{:ok, from(row in query, select: struct(row, ^Enum.uniq(select)))}
end
end

@impl true
6 changes: 4 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -166,8 +166,10 @@ defmodule AshPostgres.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash, ash_version("~> 3.4 and >= 3.4.69")},
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.72")},
# {:ash, ash_version("~> 3.4 and >= 3.4.69")},
# {:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.72")},
{:ash, ash_version(github: "ash-project/ash")},
{:ash_sql, ash_sql_version(github: "ash-project/ash_sql")},
{:igniter, "~> 0.5 and >= 0.5.16", optional: true},
{:ecto_sql, "~> 3.12"},
{:ecto, "~> 3.12 and >= 3.12.1"},
6 changes: 3 additions & 3 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
%{
"ash": {:hex, :ash, "3.5.8", "8c9fbc72b9739cd4659595f87685039a3ee373d87933399a6c14596962898989", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.24 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9a5196152b526795b1650972621b50c3ee0b45d10d6b200dd70e50e7407eb31"},
"ash_sql": {:hex, :ash_sql, "0.2.74", "f1e1effeb402c2e27680b9629b7ac5f6639474b4f8c074402209bd76ff07f56e", [:mix], [{:ash, "~> 3.5", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "16a1e57cc0a6616229630f2dae5be1f4e54d05ef87ad29d073174f15bbcf0edf"},
"ash": {:git, "https://github.com/ash-project/ash.git", "ef0a5193d142e004bb5af27c11c8a4e352cff478", []},
"ash_sql": {:git, "https://github.com/ash-project/ash_sql.git", "4cc9f2af6385300d14b51a5b104cd1ec64bed6ae", []},
"benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
@@ -23,7 +23,7 @@
"git_ops": {:hex, :git_ops, "2.7.2", "2d3c164a8bcaf13f129ab339e8e9f0a99c80ffa8f85dd0b344d7515275236dbc", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1dcd68b3f5bcd0999d69274cd21e74e652a90452e683b54d490fa5b26152945f"},
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"igniter": {:hex, :igniter, "0.5.49", "625bfd1cb8886a3fb729ea67515618e06fc890ef438baca56e5f3a12449510f0", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "a332d5700116d12517d4c2ddce225f0337429fd8cb2cb857dd530a720fa5df3b"},
"igniter": {:hex, :igniter, "0.5.50", "2f6f3a50e02835e961b6228bfcdebe96cd6e9371042939e7f080c83049057e57", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "2e992df458c044f3a18ff6347275743b21092d6677368fdb8dfded321b85cc7b"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
393 changes: 393 additions & 0 deletions test/combination_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,393 @@
defmodule AshPostgres.CombinationTest do
use AshPostgres.RepoCase, async: false
alias AshPostgres.Test.Post

require Ash.Query
import Ash.Expr

describe "combinations in actions" do
test "with no data" do
Post
|> Ash.Query.for_read(:first_and_last_post)
|> Ash.read!()
end

test "with data" do
Post
|> Ash.Changeset.for_create(:create, %{title: "title1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title3"})
|> Ash.create!()

assert [%{title: "title1"}, %{title: "title3"}] =
Post
|> Ash.Query.for_read(:first_and_last_post)
|> Ash.read!()
end

test "with data and sort" do
Post
|> Ash.Changeset.for_create(:create, %{title: "title1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title3"})
|> Ash.create!()

assert [%{title: "title3"}, %{title: "title1"}] =
Post
|> Ash.Query.for_read(:first_and_last_post)
|> Ash.Query.sort(title: :desc)
|> Ash.read!()
end

test "with data and sort, limit and filter" do
Post
|> Ash.Changeset.for_create(:create, %{title: "title1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title3"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title4"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "title5"})
|> Ash.create!()

assert ["title5", "title4", "title1"] =
Post
|> Ash.Query.for_read(:first_and_last_two_posts)
|> Ash.Query.sort(title: :desc)
|> Ash.Query.filter(title in ["title4", "title5", "title1"])
|> Ash.Query.limit(3)
|> Ash.read!()
|> Enum.map(& &1.title)

assert ["title5", "title4", "title2"] =
Post
|> Ash.Query.for_read(:first_and_last_two_posts)
|> Ash.Query.sort(title: :desc)
|> Ash.Query.filter(title in ["title4", "title5", "title2"])
|> Ash.Query.limit(3)
|> Ash.read!()
|> Enum.map(& &1.title)
end
end

describe "combinations" do
test "it combines multiple queries into one result set" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post3"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post4"})
|> Ash.create!()

assert [%Post{title: "post4"}, %Post{title: "post1"}] =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(
filter: expr(title == "post4"),
limit: 1
),
Ash.Query.Combination.union_all(
filter: expr(title == "post1"),
limit: 1
)
])
|> Ash.read!()
end

test "you can define computed properties" do

Check failure on line 128 in test/combination_test.exs

GitHub Actions / ash-ci (15) / mix test

test combinations you can define computed properties (AshPostgres.CombinationTest)

Check failure on line 128 in test/combination_test.exs

GitHub Actions / ash-ci (14) / mix test

test combinations you can define computed properties (AshPostgres.CombinationTest)

Check failure on line 128 in test/combination_test.exs

GitHub Actions / ash-ci (16) / mix test

test combinations you can define computed properties (AshPostgres.CombinationTest)
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post3"})
|> Ash.create!()

assert [%Post{title: "post3", calculations: %{post_group: 1}}] =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(
filter: expr(title == "post3"),
limit: 1,
calculations: %{
post_group: calc(1, type: :integer),
common_value: calc(1, type: :integer)
}
),
Ash.Query.Combination.union_all(
filter: expr(title == "post1"),
calculations: %{
post_group: calc(2, type: :integer),
common_value: calc(1, type: :integer)
},
limit: 1
)
])
|> Ash.Query.distinct_sort([{calc(^combinations(:common_value)), :asc}])
|> Ash.Query.sort([{calc(^combinations(:post_group)), :desc}])
|> Ash.Query.distinct([{calc(^combinations(:common_value)), :asc}])
|> Ash.Query.calculate(:post_group, :integer, expr(^combinations(:post_group)))
|> Ash.read!()
end

test "it handles combinations with intersect" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "shared"})
|> Ash.create!()

assert [%Post{title: "shared"}] =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(filter: expr(title in ["post1", "shared"])),
Ash.Query.Combination.intersect(filter: expr(title in ["post2", "shared"]))
])
|> Ash.read!()
end

test "it handles combinations with except" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "shared"})
|> Ash.create!()

result =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(filter: expr(title in ["post1", "shared"])),
Ash.Query.Combination.except(filter: expr(title == "shared"))
])
|> Ash.read!()

assert length(result) == 1
assert hd(result).title == "post1"
end

test "combinations with multiple union_all" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post3"})
|> Ash.create!()

result =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(filter: expr(title == "post1")),
Ash.Query.Combination.union_all(filter: expr(title == "post2")),
Ash.Query.Combination.union_all(filter: expr(title == "post3"))
])
|> Ash.read!()

assert length(result) == 3
assert Enum.any?(result, &(&1.title == "post1"))
assert Enum.any?(result, &(&1.title == "post2"))
assert Enum.any?(result, &(&1.title == "post3"))
end

test "combination with offset" do
# Create posts with increasing title numbers for predictable sort order
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post3"})
|> Ash.create!()

result =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(
filter: expr(contains(title, "post")),
offset: 1,
limit: 2,
sort: [title: :asc]
)
])
|> Ash.read!()

assert length(result) == 2
assert hd(result).title == "post2"
assert List.last(result).title == "post3"
end

test "combinations with complex calculations" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

result =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(
filter: expr(title == "post1"),
calculations: %{
prefix: calc("first", type: :string),
full_title: calc("first-" <> title, type: :string)
}
),
Ash.Query.Combination.union_all(
filter: expr(title == "post2"),
calculations: %{
prefix: calc("second", type: :string),
full_title: calc("second-" <> title, type: :string)
}
)
])
|> Ash.Query.calculate(:title_prefix, :string, expr(^combinations(:prefix)))
|> Ash.Query.calculate(:display_title, :string, expr(^combinations(:full_title)))
|> Ash.read!()

post1 = Enum.find(result, &(&1.title == "post1"))
post2 = Enum.find(result, &(&1.title == "post2"))

assert post1.calculations.title_prefix == "first"
assert post1.calculations.display_title == "first-post1"
assert post2.calculations.title_prefix == "second"
assert post2.calculations.display_title == "second-post2"
end

test "combinations with sorting by calculation" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2"})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post3"})
|> Ash.create!()

result =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(calculations: %{sort_order: calc(3, type: :integer)}),
Ash.Query.Combination.union_all(
filter: expr(title == "post2"),
calculations: %{sort_order: calc(1, type: :integer)}
),
Ash.Query.Combination.union_all(
filter: expr(title == "post3"),
calculations: %{sort_order: calc(2, type: :integer)}
)
])
|> Ash.Query.sort([{calc(^combinations(:sort_order)), :asc}, {:title, :asc}])
|> Ash.Query.distinct(:title)
|> Ash.read!()

assert [first, second, third | _] = result
assert first.title == "post2"
assert second.title == "post3"
assert third.title == "post1"
end

test "combination with distinct" do
Post
|> Ash.Changeset.for_create(:create, %{title: "post1", score: 10})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post2", score: 10})
|> Ash.create!()

Post
|> Ash.Changeset.for_create(:create, %{title: "post3", score: 20})
|> Ash.create!()

result =
Post
|> Ash.Query.combination_of([
Ash.Query.Combination.base(
filter: expr(score == 10),
select: [:id, :score],
calculations: %{score_group: calc("low", type: :string)}
),
Ash.Query.Combination.union_all(
filter: expr(score == 20),
select: [:id, :score],
calculations: %{score_group: calc("high", type: :string)}
)
])
|> Ash.Query.distinct([{calc(^combinations(:score_group)), :asc}])
|> Ash.Query.calculate(:upper_title, :string, expr(fragment("UPPER(?)", title)))
|> Ash.read!()

assert Enum.all?(result, &(&1.calculations.upper_title == String.upcase(&1.title)))

# Should only have 2 results since we're distinct on score group
assert length(result) == 2

groups =
Enum.map(result, & &1.calculations[:score_group])

assert "low" in groups
assert "high" in groups
end
end
end
30 changes: 30 additions & 0 deletions test/support/resources/post.ex
Original file line number Diff line number Diff line change
@@ -154,6 +154,36 @@ defmodule AshPostgres.Test.Post do

defaults([:read, :destroy])

read :first_and_last_post do
prepare(fn query, _ ->
Ash.Query.combination_of(query, [
Ash.Query.Combination.base(
limit: 1,
sort: [created_at: :desc]
),
Ash.Query.Combination.union(
limit: 1,
sort: [created_at: :asc]
)
])
end)
end

read :first_and_last_two_posts do
prepare(fn query, _ ->
Ash.Query.combination_of(query, [
Ash.Query.Combination.base(
limit: 2,
sort: [created_at: :desc]
),
Ash.Query.Combination.union(
limit: 2,
sort: [created_at: :asc]
)
])
end)
end

update :add_to_limited_score do
argument(:amount, :integer, allow_nil?: false)
change(atomic_update(:limited_score, expr((limited_score || 0) + ^arg(:amount))))