diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 7fd5104..6c2a102 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -625,7 +625,11 @@ defmodule AshPostgres.DataLayer do def functions(resource) do config = AshPostgres.DataLayer.Info.repo(resource).config() - functions = [AshPostgres.Functions.Fragment] + functions = [ + AshPostgres.Functions.Fragment, + AshPostgres.Functions.Like, + AshPostgres.Functions.ILike + ] if "pg_trgm" in (config[:installed_extensions] || []) do functions ++ diff --git a/lib/expr.ex b/lib/expr.ex index 860be87..8488933 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -5,7 +5,7 @@ defmodule AshPostgres.Expr do alias Ash.Query.{BooleanExpression, Exists, Not, Ref} alias Ash.Query.Operator.IsNil alias Ash.Query.Function.{Ago, Contains, GetPath, If, Length, Now, Type} - alias AshPostgres.Functions.{Fragment, TrigramSimilarity} + alias AshPostgres.Functions.{Fragment, ILike, Like, TrigramSimilarity} require Ecto.Query @@ -51,6 +51,34 @@ defmodule AshPostgres.Expr do |> maybe_type(type, query) end + defp do_dynamic_expr( + query, + %Like{arguments: [arg1, arg2], embedded?: pred_embedded?}, + bindings, + embedded?, + type + ) do + arg1 = do_dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?) + arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?) + + Ecto.Query.dynamic(like(^arg1, ^arg2)) + |> maybe_type(type, query) + end + + defp do_dynamic_expr( + query, + %ILike{arguments: [arg1, arg2], embedded?: pred_embedded?}, + bindings, + embedded?, + type + ) do + arg1 = do_dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?) + arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?) + + Ecto.Query.dynamic(ilike(^arg1, ^arg2)) + |> maybe_type(type, query) + end + defp do_dynamic_expr( query, %IsNil{left: left, right: right, embedded?: pred_embedded?}, diff --git a/lib/functions/ilike.ex b/lib/functions/ilike.ex new file mode 100644 index 0000000..82e7cf0 --- /dev/null +++ b/lib/functions/ilike.ex @@ -0,0 +1,9 @@ +defmodule AshPostgres.Functions.ILike do + @moduledoc """ + Maps to the builtin postgres function `ilike`. + """ + + use Ash.Query.Function, name: :ilike + + def args, do: [[:string, :string]] +end diff --git a/lib/functions/like.ex b/lib/functions/like.ex new file mode 100644 index 0000000..bfd2bbd --- /dev/null +++ b/lib/functions/like.ex @@ -0,0 +1,9 @@ +defmodule AshPostgres.Functions.Like do + @moduledoc """ + Maps to the builtin postgres function `like`. + """ + + use Ash.Query.Function, name: :like + + def args, do: [[:string, :string]] +end diff --git a/test/filter_test.exs b/test/filter_test.exs index ea22706..d966f66 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -773,6 +773,48 @@ defmodule AshPostgres.FilterTest do end end + describe "like and ilike" do + test "like builds and matches" do + Post + |> Ash.Changeset.new(%{title: "MaTcH"}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(like(title, "%aTc%")) + |> Api.read!() + + assert [%Post{title: "MaTcH"}] = results + + results = + Post + |> Ash.Query.filter(like(title, "%atc%")) + |> Api.read!() + + assert [] = results + end + + test "ilike builds and matches" do + Post + |> Ash.Changeset.new(%{title: "MaTcH"}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(ilike(title, "%aTc%")) + |> Api.read!() + + assert [%Post{title: "MaTcH"}] = results + + results = + Post + |> Ash.Query.filter(ilike(title, "%atc%")) + |> Api.read!() + + assert [%Post{title: "MaTcH"}] = results + end + end + describe "trigram_similarity" do test "it works on matches" do Post