mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 21:13:19 +12:00
improvement: support new distinct
features from ash core
This commit is contained in:
parent
e5fd47164d
commit
c920b09277
8 changed files with 146 additions and 64 deletions
|
@ -1,2 +1,2 @@
|
||||||
erlang 25.3
|
erlang 26.0.2
|
||||||
elixir 1.15.2
|
elixir 1.15.4
|
||||||
|
|
|
@ -17,6 +17,7 @@ if Mix.env() == :dev do
|
||||||
end
|
end
|
||||||
|
|
||||||
if Mix.env() == :test do
|
if Mix.env() == :test do
|
||||||
|
config :ash, :validate_api_resource_inclusion?, false
|
||||||
config :ash, :validate_api_config_inclusion?, false
|
config :ash, :validate_api_config_inclusion?, false
|
||||||
|
|
||||||
config :ash_postgres, AshPostgres.TestRepo,
|
config :ash_postgres, AshPostgres.TestRepo,
|
||||||
|
|
|
@ -408,7 +408,7 @@ defmodule AshPostgres.DataLayer do
|
||||||
}
|
}
|
||||||
|
|
||||||
alias Ash.Filter
|
alias Ash.Filter
|
||||||
alias Ash.Query.{BooleanExpression, Not}
|
alias Ash.Query.{BooleanExpression, Not, Ref}
|
||||||
|
|
||||||
@behaviour Ash.DataLayer
|
@behaviour Ash.DataLayer
|
||||||
|
|
||||||
|
@ -1839,6 +1839,7 @@ defmodule AshPostgres.DataLayer do
|
||||||
{:error, distinct_statement} ->
|
{:error, distinct_statement} ->
|
||||||
distinct_query =
|
distinct_query =
|
||||||
query
|
query
|
||||||
|
|> Ecto.Query.exclude(:order_by)
|
||||||
|> default_bindings(resource)
|
|> default_bindings(resource)
|
||||||
|> Map.put(:distinct, distinct_statement)
|
|> Map.put(:distinct, distinct_statement)
|
||||||
|
|
||||||
|
@ -1854,8 +1855,28 @@ defmodule AshPostgres.DataLayer do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
joined_query_source =
|
||||||
|
Enum.reduce(
|
||||||
|
[
|
||||||
|
:join,
|
||||||
|
:group_by,
|
||||||
|
:having,
|
||||||
|
:distinct,
|
||||||
|
:select,
|
||||||
|
:combinations,
|
||||||
|
:with_ctes,
|
||||||
|
:limit,
|
||||||
|
:offset,
|
||||||
|
:lock,
|
||||||
|
:preload,
|
||||||
|
:update
|
||||||
|
],
|
||||||
|
query,
|
||||||
|
&Ecto.Query.exclude(&2, &1)
|
||||||
|
)
|
||||||
|
|
||||||
joined_query =
|
joined_query =
|
||||||
from(row in query.from.source,
|
from(row in joined_query_source,
|
||||||
join: distinct in subquery(distinct_query),
|
join: distinct in subquery(distinct_query),
|
||||||
on: ^on
|
on: ^on
|
||||||
)
|
)
|
||||||
|
@ -1878,23 +1899,28 @@ defmodule AshPostgres.DataLayer do
|
||||||
defp get_distinct_statement(query, distinct_on) do
|
defp get_distinct_statement(query, distinct_on) do
|
||||||
sort = query.__ash_bindings__.sort || []
|
sort = query.__ash_bindings__.sort || []
|
||||||
|
|
||||||
|
distinct =
|
||||||
|
query.distinct ||
|
||||||
|
%Ecto.Query.QueryExpr{
|
||||||
|
expr: [],
|
||||||
|
params: []
|
||||||
|
}
|
||||||
|
|
||||||
if sort == [] do
|
if sort == [] do
|
||||||
{:ok, default_distinct_statement(query, distinct_on)}
|
{:ok, default_distinct_statement(query, distinct_on)}
|
||||||
else
|
else
|
||||||
distinct_on
|
distinct_on
|
||||||
|> Enum.reduce_while({sort, []}, fn
|
|> Enum.reduce_while({sort, [], [], Enum.count(distinct.params)}, fn
|
||||||
_, {[], _distinct_statement} ->
|
_, {[], _distinct_statement, _, _count} ->
|
||||||
{:halt, :error}
|
{:halt, :error}
|
||||||
|
|
||||||
distinct_on, {[order_by | rest_order_by], distinct_statement} ->
|
distinct_on, {[order_by | rest_order_by], distinct_statement, params, count} ->
|
||||||
case order_by do
|
case order_by do
|
||||||
{^distinct_on, order} ->
|
{^distinct_on, order} ->
|
||||||
|
{distinct_expr, params, count} = distinct_on_expr(query, distinct_on, params, count)
|
||||||
|
|
||||||
{:cont,
|
{:cont,
|
||||||
{rest_order_by,
|
{rest_order_by, [{order, distinct_expr} | distinct_statement], params, count}}
|
||||||
[
|
|
||||||
{order, {{:., [], [{:&, [], [0]}, distinct_on]}, [], []}}
|
|
||||||
| distinct_statement
|
|
||||||
]}}
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
{:halt, :error}
|
{:halt, :error}
|
||||||
|
@ -1904,14 +1930,13 @@ defmodule AshPostgres.DataLayer do
|
||||||
:error ->
|
:error ->
|
||||||
{:error, default_distinct_statement(query, distinct_on)}
|
{:error, default_distinct_statement(query, distinct_on)}
|
||||||
|
|
||||||
{_, result} ->
|
{_, result, params, _} ->
|
||||||
distinct =
|
{:ok,
|
||||||
query.distinct ||
|
%{
|
||||||
%Ecto.Query.QueryExpr{
|
distinct
|
||||||
expr: []
|
| expr: distinct.expr ++ Enum.reverse(result),
|
||||||
}
|
params: distinct.params ++ Enum.reverse(params)
|
||||||
|
}}
|
||||||
{:ok, %{distinct | expr: distinct.expr ++ Enum.reverse(result)}}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1923,12 +1948,60 @@ defmodule AshPostgres.DataLayer do
|
||||||
expr: []
|
expr: []
|
||||||
}
|
}
|
||||||
|
|
||||||
expr =
|
{expr, params, _} =
|
||||||
Enum.map(distinct_on, fn distinct_on_field ->
|
Enum.reduce(distinct_on, {[], [], Enum.count(distinct.params)}, fn
|
||||||
{:asc, {{:., [], [{:&, [], [0]}, distinct_on_field]}, [], []}}
|
{distinct_on_field, order}, {expr, params, count} ->
|
||||||
|
{distinct_expr, params, count} =
|
||||||
|
distinct_on_expr(query, distinct_on_field, params, count)
|
||||||
|
|
||||||
|
{[{order, distinct_expr} | expr], params, count}
|
||||||
|
|
||||||
|
distinct_on_field, {expr, params, count} ->
|
||||||
|
{distinct_expr, params, count} =
|
||||||
|
distinct_on_expr(query, distinct_on_field, params, count)
|
||||||
|
|
||||||
|
{[{:asc, distinct_expr} | expr], params, count}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
%{distinct | expr: distinct.expr ++ expr}
|
%{
|
||||||
|
distinct
|
||||||
|
| expr: distinct.expr ++ Enum.reverse(expr),
|
||||||
|
params: distinct.params ++ Enum.reverse(params)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp distinct_on_expr(query, field, params, count) do
|
||||||
|
resource = query.__ash_bindings__.resource
|
||||||
|
|
||||||
|
ref =
|
||||||
|
case field do
|
||||||
|
%Ash.Query.Calculation{} = calc ->
|
||||||
|
%Ref{attribute: calc, relationship_path: [], resource: resource}
|
||||||
|
|
||||||
|
field ->
|
||||||
|
%Ref{
|
||||||
|
attribute: Ash.Resource.Info.field(resource, field),
|
||||||
|
relationship_path: [],
|
||||||
|
resource: resource
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
dynamic = AshPostgres.Expr.dynamic_expr(query, ref, query.__ash_bindings__)
|
||||||
|
|
||||||
|
result =
|
||||||
|
Ecto.Query.Builder.Dynamic.partially_expand(
|
||||||
|
:distinct,
|
||||||
|
query,
|
||||||
|
dynamic,
|
||||||
|
params,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
expr = elem(result, 0)
|
||||||
|
new_params = elem(result, 1)
|
||||||
|
new_count = result |> Tuple.to_list() |> List.last()
|
||||||
|
|
||||||
|
{expr, new_params, new_count}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
34
lib/expr.ex
34
lib/expr.ex
|
@ -787,40 +787,6 @@ defmodule AshPostgres.Expr do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_dynamic_expr(
|
|
||||||
query,
|
|
||||||
%Ref{
|
|
||||||
attribute: %Ash.Resource.Calculation{calculation: {module, opts}} = calculation
|
|
||||||
} = ref,
|
|
||||||
bindings,
|
|
||||||
embedded?,
|
|
||||||
type
|
|
||||||
) do
|
|
||||||
calc_type =
|
|
||||||
AshPostgres.Types.parameterized_type(
|
|
||||||
calculation.type,
|
|
||||||
Map.get(calculation, :constraints, [])
|
|
||||||
)
|
|
||||||
|
|
||||||
validate_type!(query, calc_type, ref)
|
|
||||||
|
|
||||||
{:ok, query_calc} =
|
|
||||||
Ash.Query.Calculation.new(
|
|
||||||
calculation.name,
|
|
||||||
module,
|
|
||||||
opts,
|
|
||||||
calculation.type
|
|
||||||
)
|
|
||||||
|
|
||||||
expr = do_dynamic_expr(query, %{ref | attribute: query_calc}, bindings, embedded?, type)
|
|
||||||
|
|
||||||
if calc_type do
|
|
||||||
Ecto.Query.dynamic(type(^expr, ^calc_type))
|
|
||||||
else
|
|
||||||
expr
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_dynamic_expr(
|
defp do_dynamic_expr(
|
||||||
query,
|
query,
|
||||||
%Ref{
|
%Ref{
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -161,7 +161,7 @@ defmodule AshPostgres.MixProject do
|
||||||
{:ecto, "~> 3.9"},
|
{:ecto, "~> 3.9"},
|
||||||
{:jason, "~> 1.0"},
|
{:jason, "~> 1.0"},
|
||||||
{:postgrex, ">= 0.0.0"},
|
{:postgrex, ">= 0.0.0"},
|
||||||
{:ash, ash_version("~> 2.11 and >= 2.11.9")},
|
{:ash, ash_version("~> 2.11 and >= 2.11.11")},
|
||||||
{:git_ops, "~> 2.5", only: [:dev, :test]},
|
{:git_ops, "~> 2.5", only: [:dev, :test]},
|
||||||
{:ex_doc, "~> 0.22", only: [:dev, :test], runtime: false},
|
{:ex_doc, "~> 0.22", only: [:dev, :test], runtime: false},
|
||||||
{:ex_check, "~> 0.14", only: [:dev, :test]},
|
{:ex_check, "~> 0.14", only: [:dev, :test]},
|
||||||
|
|
2
mix.lock
2
mix.lock
|
@ -1,5 +1,5 @@
|
||||||
%{
|
%{
|
||||||
"ash": {:hex, :ash, "2.11.9", "8f692fc38895d7eafbad981f3ef2b6dc22af3cabe18a847db10faa750cec0537", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e57e54083ead9eb4915b0755d15e335b610e595f1030eb338a3c9287b2241764"},
|
"ash": {:hex, :ash, "2.11.11", "84bdbc62f984ea10a9c0e30749ed1c015375657b4f1da32a85a7465d24ec45c3", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d7c9399ebe0322e93c785e6f9df8b66006326ea5745834ad3e830e5d4a0549e9"},
|
||||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||||
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
||||||
|
|
|
@ -7,19 +7,19 @@ defmodule AshPostgres.DistinctTest do
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.new(%{title: "title", score: 1})
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.new(%{title: "title", score: 1})
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "foo"})
|
|> Ash.Changeset.new(%{title: "foo", score: 2})
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "foo"})
|
|> Ash.Changeset.new(%{title: "foo", score: 2})
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
@ -77,4 +77,42 @@ defmodule AshPostgres.DistinctTest do
|
||||||
|
|
||||||
assert [_] = results
|
assert [_] = results
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "distinct can use calculations sort that does not match the distinct using a limit #2" do
|
||||||
|
results =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.distinct(:negative_score)
|
||||||
|
|> Ash.Query.sort(:negative_score)
|
||||||
|
|> Ash.Query.load(:negative_score)
|
||||||
|
|> Api.read!()
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{title: "foo", negative_score: -2},
|
||||||
|
%{title: "title", negative_score: -1}
|
||||||
|
] = results
|
||||||
|
|
||||||
|
results =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.distinct(:negative_score)
|
||||||
|
|> Ash.Query.sort(negative_score: :desc)
|
||||||
|
|> Ash.Query.load(:negative_score)
|
||||||
|
|> Api.read!()
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{title: "title", negative_score: -1},
|
||||||
|
%{title: "foo", negative_score: -2}
|
||||||
|
] = results
|
||||||
|
|
||||||
|
results =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.distinct(:negative_score)
|
||||||
|
|> Ash.Query.sort(:title)
|
||||||
|
|> Ash.Query.load(:negative_score)
|
||||||
|
|> Api.read!()
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{title: "foo", negative_score: -2},
|
||||||
|
%{title: "title", negative_score: -1}
|
||||||
|
] = results
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,6 +102,10 @@ defmodule AshPostgres.Test.Post do
|
||||||
|
|
||||||
has_many(:comments, AshPostgres.Test.Comment, destination_attribute: :post_id)
|
has_many(:comments, AshPostgres.Test.Comment, destination_attribute: :post_id)
|
||||||
|
|
||||||
|
has_many :comments_matching_post_title, AshPostgres.Test.Comment do
|
||||||
|
filter(expr(title == parent_expr(title)))
|
||||||
|
end
|
||||||
|
|
||||||
has_many :popular_comments, AshPostgres.Test.Comment do
|
has_many :popular_comments, AshPostgres.Test.Comment do
|
||||||
destination_attribute(:post_id)
|
destination_attribute(:post_id)
|
||||||
filter(expr(likes > 10))
|
filter(expr(likes > 10))
|
||||||
|
|
Loading…
Reference in a new issue