diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 3d5f7275..e8d9ca03 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -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 diff --git a/mix.exs b/mix.exs index 02628a3a..67a6d34f 100644 --- a/mix.exs +++ b/mix.exs @@ -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"}, diff --git a/mix.lock b/mix.lock index 9f049d0e..c24b1dc8 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/combination_test.exs b/test/combination_test.exs new file mode 100644 index 00000000..e5156ca2 --- /dev/null +++ b/test/combination_test.exs @@ -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 + 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 diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 6ddcb160..e459cfa1 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -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))))