Skip to content

Commit fe14f3b

Browse files
committed
fix: don't optimize equality into in expressions
1 parent 27c9fa4 commit fe14f3b

File tree

8 files changed

+30
-98
lines changed

8 files changed

+30
-98
lines changed

lib/ash.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4047,4 +4047,15 @@ defmodule Ash do
40474047

40484048
@doc false
40494049
def stream_opt_keys, do: Keyword.keys(@stream_opts)
4050+
4051+
@doc false
4052+
def pkey_filter(records, pkey) do
4053+
case pkey do
4054+
[single_key] ->
4055+
[{single_key, [in: Enum.map(records, &Map.get(&1, single_key))]}]
4056+
4057+
composite_keys ->
4058+
[or: Enum.map(records, &Map.take(&1, composite_keys))]
4059+
end
4060+
end
40504061
end

lib/ash/actions/destroy/bulk.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ defmodule Ash.Actions.Destroy.Bulk do
10021002
opts,
10031003
ref,
10041004
fn batch ->
1005-
pkeys = [or: Enum.map(batch, &Map.take(&1, pkey))]
1005+
pkeys = Ash.pkey_filter(batch, pkey)
10061006

10071007
read_action = Ash.Actions.Update.Bulk.get_read_action(resource, action, opts).name
10081008

lib/ash/actions/read/read.ex

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,18 +1700,12 @@ defmodule Ash.Actions.Read do
17001700
"""}
17011701
else
17021702
filter =
1703-
initial_data
1704-
|> List.wrap()
1705-
|> Enum.map(&Map.take(&1, primary_key))
1706-
|> case do
1703+
case List.wrap(initial_data) do
17071704
[] ->
17081705
false
17091706

1710-
[single] ->
1711-
[single]
1712-
1713-
multiple ->
1714-
[or: multiple]
1707+
records ->
1708+
Ash.pkey_filter(records, primary_key)
17151709
end
17161710

17171711
with %{valid?: true} = query <-

lib/ash/actions/update/bulk.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,7 @@ defmodule Ash.Actions.Update.Bulk do
12981298
opts,
12991299
ref,
13001300
fn batch ->
1301-
pkeys = [or: Enum.map(batch, &Map.take(&1, pkey))]
1301+
pkeys = Ash.pkey_filter(batch, pkey)
13021302

13031303
read_action = get_read_action(resource, action, opts).name
13041304

lib/ash/can.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -878,15 +878,14 @@ defmodule Ash.Can do
878878
data = List.wrap(opts[:data])
879879

880880
pkey = Ash.Resource.Info.primary_key(query.resource)
881-
pkey_values = Enum.map(data, &Map.take(&1, pkey))
882881

883-
if Enum.any?(pkey_values, fn pkey_value ->
884-
pkey_value |> Map.values() |> Enum.any?(&is_nil/1)
882+
if Enum.any?(data, fn record ->
883+
pkey |> Enum.map(&Map.get(record, &1)) |> Enum.any?(&is_nil/1)
885884
end) do
886885
{:ok, :maybe}
887886
else
888887
query
889-
|> Ash.Query.do_filter(or: pkey_values)
888+
|> Ash.Query.do_filter(Ash.pkey_filter(data, pkey))
890889
|> Ash.Query.select([])
891890
|> Ash.Query.set_tenant(tenant)
892891
|> Ash.Actions.Read.add_calc_context_to_query(

lib/ash/policy/filter_check.ex

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,7 @@ defmodule Ash.Policy.FilterCheck do
354354
def check(actor, data, authorizer, opts) do
355355
pkey = Ash.Resource.Info.primary_key(authorizer.resource)
356356

357-
filter =
358-
case data do
359-
[record] -> Map.take(record, pkey)
360-
records -> [or: Enum.map(data, &Map.take(&1, pkey))]
361-
end
357+
filter = Ash.pkey_filter(data, pkey)
362358

363359
authorizer.resource
364360
|> Ash.Query.filter(^filter)

lib/ash/query/boolean_expression.ex

Lines changed: 2 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ defmodule Ash.Query.BooleanExpression do
3333
# statements are contradictions. However, that would likely confuse users. For example:
3434
# `Ash.Query.filter(Resource, x == 1 and x in [2, 3])` becomes `#Ash.Query<filter: false>`
3535
# We may want to go down this route some day, but for now we simply use this to combine
36-
# statements where possible, which helps with authorization logic that leverages the query
37-
# For example, `x in [1, 2] or x == 3` becomes `x in [1, 2, 3]`, and `x in [1, 2, 3] and x != 1`
38-
# becomes `x in [2, 3]`
36+
# statements where possible, which helps with authorization logic that leverages the query.
37+
# For example, `x in [1, 2, 3] and x != 1` becomes `x in [2, 3]`
3938
def optimized_new(_, nil, nil), do: false
4039
def optimized_new(:and, false, _), do: false
4140
def optimized_new(:and, _, false), do: false
@@ -94,18 +93,6 @@ defmodule Ash.Query.BooleanExpression do
9493
do_new(op, left, right)
9594
end
9695

97-
def optimized_new(
98-
:or,
99-
%Eq{left: left, right: value} = left_op,
100-
%In{left: left, right: %MapSet{} = mapset} = right
101-
) do
102-
if can_optimize?(value) do
103-
%{right | right: MapSet.put(mapset, value)}
104-
else
105-
do_new(:or, left_op, right)
106-
end
107-
end
108-
10996
def optimized_new(
11097
:and,
11198
%Eq{left: left, right: value} = left_expr,
@@ -142,18 +129,6 @@ defmodule Ash.Query.BooleanExpression do
142129
optimized_new(op, right, left)
143130
end
144131

145-
def optimized_new(
146-
:or,
147-
%Eq{left: left, right: left_value} = left_expr,
148-
%Eq{left: left, right: right_value} = right_expr
149-
) do
150-
if can_optimize?(left_value) && can_optimize?(right_value) do
151-
%In{left: left, right: MapSet.new([left_value, right_value])}
152-
else
153-
do_new(:or, left_expr, right_expr)
154-
end
155-
end
156-
157132
def optimized_new(
158133
:and,
159134
%Eq{left: left, right: left_value} = left_expr,
@@ -178,14 +153,6 @@ defmodule Ash.Query.BooleanExpression do
178153
end
179154
end
180155

181-
def optimized_new(
182-
:or,
183-
%In{left: left, right: %MapSet{} = left_values},
184-
%In{left: left, right: %MapSet{} = right_values} = right_expr
185-
) do
186-
%{right_expr | right: MapSet.union(left_values, right_values)}
187-
end
188-
189156
def optimized_new(
190157
:and,
191158
%In{left: left, right: left_values} = left_expr,
@@ -204,41 +171,6 @@ defmodule Ash.Query.BooleanExpression do
204171
end
205172
end
206173

207-
def optimized_new(
208-
:or,
209-
%__MODULE__{op: :or, left: left, right: right} = left_expr,
210-
right_expr
211-
) do
212-
case right_expr do
213-
%In{} = in_op ->
214-
with {:left, nil} <- {:left, Ash.Filter.find(left, &simplify?(&1, in_op), true, false)},
215-
{:right, nil} <- {:right, Ash.Filter.find(right, &simplify?(&1, in_op), true, false)} do
216-
do_new(:or, left_expr, in_op)
217-
else
218-
{:left, _} ->
219-
%{left_expr | left: optimized_new(:or, left, in_op)}
220-
221-
{:right, _} ->
222-
%{left_expr | right: optimized_new(:or, right, in_op)}
223-
end
224-
225-
%Eq{} = eq_op ->
226-
with {:left, nil} <- {:left, Ash.Filter.find(left, &simplify?(&1, eq_op), true, false)},
227-
{:right, nil} <- {:right, Ash.Filter.find(right, &simplify?(&1, eq_op), true, false)} do
228-
do_new(:or, left_expr, eq_op)
229-
else
230-
{:left, _} ->
231-
%{left_expr | left: optimized_new(:or, left, eq_op)}
232-
233-
{:right, _} ->
234-
%{left_expr | right: optimized_new(:or, right, eq_op)}
235-
end
236-
237-
_ ->
238-
do_new(:or, left_expr, right_expr)
239-
end
240-
end
241-
242174
def optimized_new(
243175
:and,
244176
%__MODULE__{op: :and, left: left, right: right} = left_expr,

test/filter/filter_test.exs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,25 +298,25 @@ defmodule Ash.Test.Filter.FilterTest do
298298
describe "predicate optimization" do
299299
# Testing against the stringified query may be a bad idea, but its a quick win and we
300300
# can switch to actually checking the structure if this bites us
301-
test "equality simplifies to `in`" do
301+
test "equality does not simplify to `in`" do
302302
stringified_query =
303303
Post
304304
|> Ash.Query.filter(title == "foo" or title == "bar")
305305
|> inspect()
306306

307-
assert stringified_query =~ ~S(title in ["bar", "foo"])
307+
assert stringified_query =~ ~S(title == "foo" or title == "bar")
308308
end
309309

310-
test "in with equality simplifies to `in`" do
310+
test "in with equality does not simplify to `in`" do
311311
stringified_query =
312312
Post
313313
|> Ash.Query.filter(title in ["foo", "bar", "baz"] or title == "bar")
314314
|> inspect()
315315

316-
assert stringified_query =~ ~S(title in ["bar", "baz", "foo"])
316+
assert stringified_query =~ ~S(title == "bar" or title in ["bar", "baz", "foo"])
317317
end
318318

319-
test "multiple nested equality simplifies to `in`" do
319+
test "multiple nested equality does not simplify to `in`" do
320320
import Ash.Expr
321321
keys = [:id]
322322

@@ -340,7 +340,7 @@ defmodule Ash.Test.Filter.FilterTest do
340340
|> Ash.Query.filter(^expr)
341341
|> inspect()
342342

343-
assert stringified_query =~ ~S(id in ["bar", "baz", "buz", "foo"])
343+
assert stringified_query =~ ~S(id == "foo" or id == "bar" or id == "baz" or id == "buz")
344344
end
345345

346346
test "in across ands in ors isn't optimized" do
@@ -367,13 +367,13 @@ defmodule Ash.Test.Filter.FilterTest do
367367
assert stringified_query =~ ~S(title in ["baz", "foo"])
368368
end
369369

370-
test "in with or-in simplifies to `in`" do
370+
test "in with or-in does not simplify to `in`" do
371371
stringified_query =
372372
Post
373373
|> Ash.Query.filter(title in ["foo", "bar"] or title in ["bar", "baz"])
374374
|> inspect()
375375

376-
assert stringified_query =~ ~S(title in ["bar", "baz", "foo"])
376+
assert stringified_query =~ ~S(title in ["bar", "foo"] or title in ["bar", "baz"])
377377
end
378378

379379
test "in with and-in simplifies to `in` when multiple values overlap" do

0 commit comments

Comments
 (0)