From 33e814328656a23729a565c67c6b01b588eed4aa Mon Sep 17 00:00:00 2001 From: Stanley Gunawan Date: Wed, 20 Nov 2024 11:09:32 +0800 Subject: [PATCH 1/2] feat: add :or operator for compound field filter with = instead of like --- lib/flop/adapter/ecto.ex | 9 ++++++--- lib/flop/adapter/ecto/operators.ex | 12 ++++++++++++ lib/flop/filter.ex | 4 +++- lib/flop/schema.ex | 15 ++++++++------- test/base/flop/filter_test.exs | 3 ++- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/flop/adapter/ecto.ex b/lib/flop/adapter/ecto.ex index f5fec9bf..d937d8d8 100644 --- a/lib/flop/adapter/ecto.ex +++ b/lib/flop/adapter/ecto.ex @@ -34,7 +34,8 @@ defmodule Flop.Adapter.Ecto do :like_and, :like_or, :ilike_and, - :ilike_or + :ilike_or, + :or ] @backend_options [ @@ -158,7 +159,8 @@ defmodule Flop.Adapter.Ecto do :ilike_and, :ilike_or, :empty, - :not_empty + :not_empty, + :or ], extra: %{fields: fields, type: :compound} }} @@ -501,13 +503,14 @@ defmodule Flop.Adapter.Ecto do ## Filter query builder - for op <- [:like_and, :like_or, :ilike_and, :ilike_or] do + for op <- [:like_and, :like_or, :ilike_and, :ilike_or, :or] do {field_op, combinator} = case op do :ilike_and -> {:ilike, :and} :ilike_or -> {:ilike, :or} :like_and -> {:like, :and} :like_or -> {:like, :or} + :or -> {:==, :or} end defp build_op( diff --git a/lib/flop/adapter/ecto/operators.ex b/lib/flop/adapter/ecto/operators.ex index 68af32a3..0abf70a9 100644 --- a/lib/flop/adapter/ecto/operators.ex +++ b/lib/flop/adapter/ecto/operators.ex @@ -274,6 +274,18 @@ defmodule Flop.Adapter.Ecto.Operators do {fragment, prelude, combinator} end + def op_config(:or) do + fragment = + quote do + field(r, ^var!(field)) == ^var!(value) + end + + combinator = :or + prelude = prelude(:maybe_split_search_text) + + {fragment, prelude, combinator} + end + defp empty do quote do is_nil(field(r, ^var!(field))) == ^var!(value) diff --git a/lib/flop/filter.ex b/lib/flop/filter.ex index e7e7bc92..c93455e1 100644 --- a/lib/flop/filter.ex +++ b/lib/flop/filter.ex @@ -96,6 +96,7 @@ defmodule Flop.Filter do | :not_ilike | :ilike_and | :ilike_or + | :or @operators [ :==, @@ -118,7 +119,8 @@ defmodule Flop.Filter do :ilike, :not_ilike, :ilike_and, - :ilike_or + :ilike_or, + :or ] @primary_key false diff --git a/lib/flop/schema.ex b/lib/flop/schema.ex index b860c07d..7c909a02 100644 --- a/lib/flop/schema.ex +++ b/lib/flop/schema.ex @@ -108,7 +108,7 @@ defprotocol Flop.Schema do Setting the value to `nil` (default) allows all pagination types. - See also `t:Flop.option/0`. + See also `t:Flop.option/0`. ## Alias fields @@ -199,18 +199,18 @@ defprotocol Flop.Schema do ### Filter operator rules - - `:=~` `:like` `:not_like` `:like_and` `:like_or` `:ilike` `:not_ilike` `:ilike_and` `:ilike_or` + - `:=~` `:like` `:not_like` `:like_and` `:like_or` `:ilike` `:not_ilike` `:ilike_and` `:ilike_or` If a string value is passed it will be split at whitespace characters and each segment will be checked separately. If a list of strings is passed the individual strings are not split. The filter matches for a value if it matches for any of the fields. - - `:empty` + - `:empty` Matches if all fields of the compound field are `nil`. - - `:not_empty` + - `:not_empty` Matches if any field of the compound field is not `nil`. - - `:==` `:!=` `:<=` `:<` `:>=` `:>` `:in` `:not_in` `:contains` `:not_contains` + - `:==` `:!=` `:<=` `:<` `:>=` `:>` `:in` `:not_in` `:contains` `:not_contains` ** These filter operators are ignored for compound fields at the moment. - This will be added in a future version.** + This will be added in a future version.** The filter value is normalized by splitting the string at whitespaces and joining it with a space. The values of all fields of the compound field are split by whitespace character and joined with a space, and the resulting @@ -655,7 +655,8 @@ defprotocol Flop.Schema do :ilike_and, :ilike_or, :empty, - :not_empty + :not_empty, + :or ], extra: %{type: :compound, fields: [:family_name, :given_name]} } diff --git a/test/base/flop/filter_test.exs b/test/base/flop/filter_test.exs index 1117d241..4ff04ef2 100644 --- a/test/base/flop/filter_test.exs +++ b/test/base/flop/filter_test.exs @@ -177,7 +177,8 @@ defmodule Flop.FilterTest do :ilike_and, :ilike_or, :empty, - :not_empty + :not_empty, + :or ] end end From 7738d5fc4d936be9839369b35acb6fb35b82f3b8 Mon Sep 17 00:00:00 2001 From: Stanley Gunawan Date: Thu, 21 Nov 2024 17:48:40 +0800 Subject: [PATCH 2/2] minor: add test for :or operator for compound field --- lib/flop/adapter/ecto.ex | 22 ++++++++++++++++++++-- lib/flop/adapter/ecto/operators.ex | 3 +-- test/adapters/ecto/cases/flop_test.exs | 17 +++++++++++++++++ test/support/generators.ex | 3 +++ test/support/test_util.ex | 17 +++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/lib/flop/adapter/ecto.ex b/lib/flop/adapter/ecto.ex index d937d8d8..e770d9cd 100644 --- a/lib/flop/adapter/ecto.ex +++ b/lib/flop/adapter/ecto.ex @@ -503,14 +503,13 @@ defmodule Flop.Adapter.Ecto do ## Filter query builder - for op <- [:like_and, :like_or, :ilike_and, :ilike_or, :or] do + for op <- [:like_and, :like_or, :ilike_and, :ilike_or] do {field_op, combinator} = case op do :ilike_and -> {:ilike, :and} :ilike_or -> {:ilike, :or} :like_and -> {:like, :and} :like_or -> {:like, :or} - :or -> {:==, :or} end defp build_op( @@ -541,6 +540,25 @@ defmodule Flop.Adapter.Ecto do end end + defp build_op( + schema_struct, + %FieldInfo{extra: %{type: :compound, fields: fields}}, + %Filter{op: :or, value: value} + ) do + fields = Enum.map(fields, &get_field_info(schema_struct, &1)) + + Enum.reduce(fields, false, fn field, inner_dynamic -> + dynamic_for_field = + build_op(schema_struct, field, %Filter{ + field: field, + op: :==, + value: value + }) + + dynamic([r], ^inner_dynamic or ^dynamic_for_field) + end) + end + defp build_op( schema_struct, %FieldInfo{extra: %{type: :compound, fields: fields}}, diff --git a/lib/flop/adapter/ecto/operators.ex b/lib/flop/adapter/ecto/operators.ex index 0abf70a9..b8123f2a 100644 --- a/lib/flop/adapter/ecto/operators.ex +++ b/lib/flop/adapter/ecto/operators.ex @@ -281,9 +281,8 @@ defmodule Flop.Adapter.Ecto.Operators do end combinator = :or - prelude = prelude(:maybe_split_search_text) - {fragment, prelude, combinator} + {fragment, nil, combinator} end defp empty do diff --git a/test/adapters/ecto/cases/flop_test.exs b/test/adapters/ecto/cases/flop_test.exs index 5a4be393..8ccf79be 100644 --- a/test/adapters/ecto/cases/flop_test.exs +++ b/test/adapters/ecto/cases/flop_test.exs @@ -699,6 +699,23 @@ defmodule Flop.Adapters.Ecto.FlopTest do end end + @tag :or + property "applies :or operator on compound field" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- member_of([:pet_and_owner_name]), + pet <- member_of(pets), + value = Pet.get_field(pet, field) do + expected = filter_items(pets, field, :or, value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :or, value: value}] + }) == expected + + checkin_checkout() + end + end + property "custom field filter" do check all pet_count <- integer(@pet_count_range), pets = insert_list_and_sort(pet_count, :pet_with_owner), diff --git a/test/support/generators.ex b/test/support/generators.ex index 4834583e..5adfe355 100644 --- a/test/support/generators.ex +++ b/test/support/generators.ex @@ -118,6 +118,9 @@ defmodule Flop.Generators do def value_by_field(:owner_name), do: string(:alphanumeric, min_length: 1) + def value_by_field(:pet_and_owner_name), + do: string(:alphanumeric, min_length: 1) + def compare_value_by_field(:age), do: integer(1..30) def compare_value_by_field(:name), diff --git a/test/support/test_util.ex b/test/support/test_util.ex index d187c79a..2a32b0b7 100644 --- a/test/support/test_util.ex +++ b/test/support/test_util.ex @@ -160,6 +160,21 @@ defmodule Flop.TestUtil do end) end + defp apply_filter_to_compound_fields( + pet, + fields, + :or, + value, + ecto_adapter + ) do + filter_func = matches?(:or, value, ecto_adapter) + + Enum.any?(fields, fn field -> + field_info = Flop.Schema.field_info(%Pet{}, field) + pet |> get_field(field_info) |> filter_func.() + end) + end + defp apply_filter_to_compound_fields(pet, fields, op, value, ecto_adapter) do filter_func = matches?(op, value, ecto_adapter) @@ -267,6 +282,8 @@ defmodule Flop.TestUtil do &Enum.any?(values, fn v -> String.downcase(&1) =~ v end) end + defp matches?(:or, v, _), do: &(&1 == v) + defp empty?(nil), do: true defp empty?([]), do: true defp empty?(map) when map == %{}, do: true