Skip to content

Commit 81a6a55

Browse files
committed
chore: add tests for new operator overload features
1 parent a53853e commit 81a6a55

File tree

6 files changed

+143
-3
lines changed

6 files changed

+143
-3
lines changed

config/config.exs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ if Mix.env() == :test do
3636
config :ash_postgres, :ash_domains, [AshPostgres.Test.Domain]
3737

3838
config :ash, :custom_expressions, [
39-
AshPostgres.Expressions.TrigramWordSimilarity
39+
AshPostgres.Expressions.TrigramWordSimilarity,
40+
AshPostgres.Test.OperatorExpression.IntListContains
4041
]
4142

42-
config :ash, :known_types, [AshPostgres.Timestamptz, AshPostgres.TimestamptzUsec]
43+
config :ash, :known_types, [
44+
AshPostgres.Timestamptz,
45+
AshPostgres.TimestamptzUsec,
46+
AshPostgres.Test.OperatorExpression.IntListType
47+
]
4348

4449
config :ash_postgres, AshPostgres.TestRepo,
4550
username: "postgres",

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%{
2-
"ash": {:hex, :ash, "3.21.3", "4cb8b05655664fe65daf6862285f0a6f91991f1a73ff6554b4609a484c1ceb8b", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {: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.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, 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, "~> 1.0", [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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "deec4223f11c7cbf3b35cb14b3c8df50b58b06a5c9f64de539a93469fa249b04"},
2+
"ash": {:hex, :ash, "3.23.0", "745a98c8e1f877fd90757637bbf9f1895d01a58516c4dffe634b71b29f86e8ad", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {: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.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, 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, "~> 1.0", [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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "174bc85aa6b4023b39b89dc635011e4fc79dca4d945cf1e079c17b286c1863b7"},
33
"ash_sql": {:hex, :ash_sql, "0.5.3", "0151ade6153b5d6ec152dea5c03743d8e468fa4696f3d9796d4d1e0209d200d4", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "6022d9ae5edfab0b4a5220546200aa5112fff897c980c9f46e49e5e939e5c085"},
44
"benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"},
55
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule AshPostgres.TestRepo.Migrations.AddCustomAnyFunction do
2+
use Ecto.Migration
3+
4+
def up do
5+
execute """
6+
CREATE OR REPLACE FUNCTION custom_any(value bigint, arr bigint[])
7+
RETURNS boolean AS $$
8+
SELECT value = ANY(arr);
9+
$$ LANGUAGE SQL IMMUTABLE;
10+
"""
11+
end
12+
13+
def down do
14+
execute "DROP FUNCTION IF EXISTS custom_any(bigint, bigint[]);"
15+
end
16+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule AshPostgres.Test.OperatorExpression.IntListContains do
2+
@moduledoc """
3+
Custom expression that replaces `value in list` with `value = ANY(array)` in postgres.
4+
5+
This demonstrates operator_expression/1 rewriting an operator to a custom expression
6+
that compiles to a different (potentially more efficient) SQL form.
7+
"""
8+
use Ash.CustomExpression,
9+
name: :int_list_contains,
10+
arguments: [
11+
[:integer, AshPostgres.Test.OperatorExpression.IntListType]
12+
]
13+
14+
def expression(AshPostgres.DataLayer, [value, list]) do
15+
{:ok, expr(fragment("custom_any(?, ?)", ^value, ^list))}
16+
end
17+
18+
def expression(data_layer, [value, list])
19+
when data_layer in [Ash.DataLayer.Ets, Ash.DataLayer.Simple] do
20+
{:ok, expr(fragment(&__MODULE__.contains/2, ^value, ^list))}
21+
end
22+
23+
def expression(_data_layer, _args), do: :unknown
24+
25+
def contains(value, list) when is_list(list) do
26+
value in list
27+
end
28+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule AshPostgres.Test.OperatorExpression.IntListType do
2+
@moduledoc """
3+
Custom type that overloads the `in` operator and rewrites it to a custom expression
4+
via operator_expression/1.
5+
"""
6+
use Ash.Type
7+
8+
@impl true
9+
def storage_type(_), do: {:array, :integer}
10+
11+
@impl true
12+
def cast_input(%MapSet{} = set, constraints), do: cast_input(MapSet.to_list(set), constraints)
13+
14+
def cast_input(list, _) when is_list(list) do
15+
if Enum.all?(list, &is_integer/1), do: {:ok, list}, else: :error
16+
end
17+
18+
def cast_input(nil, _), do: {:ok, nil}
19+
def cast_input(_, _), do: :error
20+
21+
@impl true
22+
def cast_stored(value, constraints), do: cast_input(value, constraints)
23+
24+
@impl true
25+
def dump_to_native(value, _) when is_list(value), do: {:ok, value}
26+
def dump_to_native(nil, _), do: {:ok, nil}
27+
def dump_to_native(_, _), do: :error
28+
29+
@impl true
30+
def operator_overloads do
31+
%{
32+
in: %{
33+
[:integer, __MODULE__] => __MODULE__
34+
}
35+
}
36+
end
37+
38+
@impl true
39+
def evaluate_operator(%Ash.Query.Operator.In{left: left, right: right})
40+
when is_list(right) do
41+
{:known, left in right}
42+
end
43+
44+
def evaluate_operator(_), do: :unknown
45+
46+
@impl true
47+
def operator_expression(%Ash.Query.Operator.In{}) do
48+
{:ok, AshPostgres.Test.OperatorExpression.IntListContains}
49+
end
50+
51+
def operator_expression(_), do: :unknown
52+
end

test/type_test.exs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,43 @@ defmodule AshPostgres.Test.TypeTest do
120120

121121
assert updated.response == :awaiting
122122
end
123+
124+
test "operator_expression rewrites operator to custom expression" do
125+
import ExUnit.CaptureLog
126+
import Ash.Expr
127+
128+
prev = Application.get_env(:ash_postgres, AshPostgres.TestRepo)
129+
Application.put_env(:ash_postgres, AshPostgres.TestRepo, Keyword.put(prev, :log, :info))
130+
131+
try do
132+
Post
133+
|> Ash.Changeset.for_create(:create, %{title: "low", score: 5})
134+
|> Ash.create!()
135+
136+
Post
137+
|> Ash.Changeset.for_create(:create, %{title: "mid", score: 25})
138+
|> Ash.create!()
139+
140+
Post
141+
|> Ash.Changeset.for_create(:create, %{title: "high", score: 50})
142+
|> Ash.create!()
143+
144+
range = Enum.to_list(20..30)
145+
146+
log =
147+
capture_log(fn ->
148+
results =
149+
Post
150+
|> Ash.Query.filter(expr(score in ^range))
151+
|> Ash.read!()
152+
153+
assert length(results) == 1
154+
assert hd(results).title == "mid"
155+
end)
156+
157+
assert log =~ "custom_any("
158+
after
159+
Application.put_env(:ash_postgres, AshPostgres.TestRepo, prev)
160+
end
161+
end
123162
end

0 commit comments

Comments
 (0)