diff --git a/lib/ash/filter/and.ex b/filter/and.ex similarity index 100% rename from lib/ash/filter/and.ex rename to filter/and.ex diff --git a/lib/ash/filter/eq.ex b/filter/eq.ex similarity index 100% rename from lib/ash/filter/eq.ex rename to filter/eq.ex diff --git a/filter/filter.ex b/filter/filter.ex new file mode 100644 index 000000000..9117c1ba9 --- /dev/null +++ b/filter/filter.ex @@ -0,0 +1,1123 @@ +defmodule Ash.Filter do + @moduledoc false + + # A filter expression in Ash. + + # The way we represent filters may be strange, but its important to have it structured, + # as merging and checking filter subsets are used all through ash for things like + # authorization. The `ands` of a filter are not subject to its `ors`. The `not` of a filter + # is also *not* subject to its `ors`. + # For instance, if a filter `A` has two `ands`, `B` and `C` and two `ors`, `D` and `E`, and + # a `not` of F, the expression as can be represented as `(A or D or E) and NOT F and B and C`. + + # The filters `attributes` and `relationships`, *are* subject to the `ors` of that filter. + + # ` AND NOT AND ( OR )` + + # This probably needs to be refactored into something more representative of its behavior, + # like a series of nested boolean expression structs w/ a reference to the attribute/relationship + # it references. Maybe. This would be similar to Ecto's `BooleanExpr` structs. + + alias Ash.Actions.PrimaryKeyHelpers + alias Ash.Engine + alias Ash.Engine.Request + alias Ash.Filter.{And, Eq, In, Merge, NotEq, NotIn, Or} + + defstruct [ + :api, + :resource, + :not, + ands: [], + ors: [], + attributes: %{}, + relationships: %{}, + requests: [], + path: [], + errors: [], + impossible?: false + ] + + @type t :: %__MODULE__{ + api: Ash.api(), + resource: Ash.resource(), + ors: list(%__MODULE__{} | nil), + not: %__MODULE__{} | nil, + attributes: Keyword.t(), + relationships: map(), + path: list(atom), + impossible?: boolean, + errors: list(String.t()), + requests: list(Request.t()) + } + + @predicates %{ + not_eq: NotEq, + not_in: NotIn, + eq: Eq, + in: In, + and: And, + or: Or + } + + @spec parse( + Ash.resource(), + Keyword.t(), + Ash.api(), + relationship_path :: list(atom) + ) :: t() + @doc """ + Parse a filter from a filter expression + + The only rason to pass `api` would be if you intend to leverage + any engine requests that would be generated by this filter. + """ + def parse(resource, filter, api, path \\ []) + + def parse(resource, [], api, _), + do: %__MODULE__{ + api: api, + resource: resource + } + + def parse(_resource, %__MODULE__{} = filter, _, _) do + filter + end + + def parse(resource, filter, api, path) do + parsed_filter = do_parse(filter, %Ash.Filter{resource: resource, api: api, path: path}) + + source = + case path do + [] -> "filter" + path -> "related #{Enum.join(path, ".")} filter" + end + + if path == [] do + parsed_filter + else + query = + api + |> Ash.Query.new(resource) + |> Ash.Query.filter(parsed_filter) + + request = + Request.new( + resource: resource, + api: api, + query: query, + path: [:filter, path], + skip_unless_authorize?: true, + data: + Request.resolve( + [[:filter, path, :query]], + fn %{filter: %{^path => %{query: query}}} -> + data_layer_query = Ash.DataLayer.resource_to_query(resource) + + case Ash.DataLayer.filter(data_layer_query, query.filter, resource) do + {:ok, filtered_query} -> + Ash.DataLayer.run_query(filtered_query, resource) + + {:error, error} -> + {:error, error} + end + end + ), + action: Ash.primary_action!(resource, :read), + relationship: path, + name: source + ) + + add_request( + parsed_filter, + request + ) + end + end + + def optional_paths(filter) do + filter + |> do_optional_paths() + |> Enum.uniq() + end + + @doc """ + Returns true if the second argument is a strict subset (always returns the same or less data) of the first + """ + def strict_subset_of(nil, _), do: true + + def strict_subset_of(_, nil), do: false + + def strict_subset_of(%{resource: resource}, %{resource: other_resource}) + when resource != other_resource, + do: false + + def strict_subset_of(filter, candidate) do + if empty_filter?(filter) do + true + else + if empty_filter?(candidate) do + false + else + {filter, candidate} = cosimplify(filter, candidate) + + Ash.SatSolver.strict_filter_subset(filter, candidate) + end + end + end + + def strict_subset_of?(filter, candidate) do + strict_subset_of(filter, candidate) == true + end + + def primary_key_filter?(nil), do: false + + def primary_key_filter?(filter) do + cleared_pkey_filter = + filter.resource + |> Ash.primary_key() + |> Enum.map(fn key -> {key, nil} end) + + case cleared_pkey_filter do + [] -> + false + + cleared_pkey_filter -> + parsed_cleared_pkey_filter = parse(filter.resource, cleared_pkey_filter, filter.api) + + cleared_candidate_filter = clear_equality_values(filter) + + strict_subset_of?(parsed_cleared_pkey_filter, cleared_candidate_filter) + end + end + + def get_pkeys(%{query: nil, resource: resource}, api, %_{} = item) do + pkey_filter = + item + |> Map.take(Ash.primary_key(resource)) + |> Map.to_list() + + api + |> Ash.Query.new(resource) + |> Ash.Query.filter(pkey_filter) + end + + def get_pkeys(%{query: query}, _, %resource{} = item) do + pkey_filter = + item + |> Map.take(Ash.primary_key(resource)) + |> Map.to_list() + + Ash.Query.filter(query, pkey_filter) + end + + def cosimplify(left, right) do + {new_left, new_right} = simplify_lists(left, right) + + express_mutual_exclusion(new_left, new_right) + end + + defp simplify_lists(left, right) do + values = get_all_values(left, get_all_values(right, %{})) + + substitutions = + Enum.reduce(values, %{}, fn {key, values}, substitutions -> + value_substitutions = simplify_list_substitutions(values) + + Map.put(substitutions, key, value_substitutions) + end) + + {replace_values(left, substitutions), replace_values(right, substitutions)} + end + + defp simplify_list_substitutions(values) do + Enum.reduce(values, %{}, fn value, substitutions -> + case do_simplify_list(value) do + {:ok, substitution} -> + Map.put(substitutions, value, substitution) + + :error -> + substitutions + end + end) + end + + defp do_simplify_list(%In{values: []}), do: :error + + defp do_simplify_list(%In{values: [value]}) do + {:ok, %Eq{value: value}} + end + + defp do_simplify_list(%In{values: [value | rest]}) do + {:ok, + Enum.reduce(rest, %Eq{value: value}, fn value, other_values -> + Or.prebuilt_new(%Eq{value: value}, other_values) + end)} + end + + defp do_simplify_list(%NotIn{values: []}), do: :error + + defp do_simplify_list(%NotIn{values: [value]}) do + {:ok, %NotEq{value: value}} + end + + defp do_simplify_list(%NotIn{values: [value | rest]}) do + {:ok, + Enum.reduce(rest, %Eq{value: value}, fn value, other_values -> + And.prebuilt_new(%NotEq{value: value}, other_values) + end)} + end + + defp do_simplify_list(_), do: :error + + defp express_mutual_exclusion(left, right) do + values = get_all_values(left, get_all_values(right, %{})) + + substitutions = + Enum.reduce(values, %{}, fn {key, values}, substitutions -> + value_substitutions = express_mutual_exclusion_substitutions(values) + + Map.put(substitutions, key, value_substitutions) + end) + + {replace_values(left, substitutions), replace_values(right, substitutions)} + end + + defp express_mutual_exclusion_substitutions(values) do + Enum.reduce(values, %{}, fn value, substitutions -> + case do_express_mutual_exclusion(value, values) do + {:ok, substitution} -> + Map.put(substitutions, value, substitution) + + :error -> + substitutions + end + end) + end + + defp do_express_mutual_exclusion(%Eq{value: value} = eq_filter, values) do + values + |> Enum.filter(fn + %Eq{value: other_value} -> value != other_value + _ -> false + end) + |> case do + [] -> + :error + + [%{value: other_value}] -> + {:ok, And.prebuilt_new(eq_filter, %NotEq{value: other_value})} + + values -> + {:ok, + Enum.reduce(values, eq_filter, fn %{value: other_value}, expr -> + And.prebuilt_new(expr, %NotEq{value: other_value}) + end)} + end + end + + defp do_express_mutual_exclusion(_, _), do: :error + + defp get_all_values(filter, state) do + state = + filter.attributes + |> Enum.reduce(state, fn {field, value}, state -> + state + |> Map.put_new([filter.path, field], []) + |> Map.update!([filter.path, field], fn values -> + value + |> do_get_values() + |> Enum.reduce(values, fn value, values -> + [value | values] + end) + |> Enum.uniq() + end) + end) + + state = + Enum.reduce(filter.relationships, state, fn {_, relationship_filter}, new_state -> + get_all_values(relationship_filter, new_state) + end) + + state = + if filter.not do + get_all_values(filter.not, state) + else + state + end + + state = + Enum.reduce(filter.ors, state, fn or_filter, new_state -> + get_all_values(or_filter, new_state) + end) + + Enum.reduce(filter.ands, state, fn and_filter, new_state -> + get_all_values(and_filter, new_state) + end) + end + + defp do_get_values(%struct{left: left, right: right}) + when struct in [And, Or] do + do_get_values(left) ++ do_get_values(right) + end + + defp do_get_values(other), do: [other] + + defp replace_values(filter, substitutions) do + new_attrs = + Enum.reduce(filter.attributes, %{}, fn {field, value}, attributes -> + substitutions = Map.get(substitutions, [filter.path, field]) || %{} + + Map.put(attributes, field, do_replace_value(value, substitutions)) + end) + + new_relationships = + Enum.reduce(filter.relationships, %{}, fn {relationship, related_filter}, relationships -> + new_relationship_filter = replace_values(related_filter, substitutions) + + Map.put(relationships, relationship, new_relationship_filter) + end) + + new_not = + if filter.not do + replace_values(filter.not, substitutions) + else + filter.not + end + + new_ors = + Enum.reduce(filter.ors, [], fn or_filter, ors -> + new_or = replace_values(or_filter, substitutions) + + [new_or | ors] + end) + + new_ands = + Enum.reduce(filter.ands, [], fn and_filter, ands -> + new_and = replace_values(and_filter, substitutions) + + [new_and | ands] + end) + + %{ + filter + | attributes: new_attrs, + relationships: new_relationships, + not: new_not, + ors: Enum.reverse(new_ors), + ands: Enum.reverse(new_ands) + } + end + + defp do_replace_value(%struct{left: left, right: right} = compound, substitutions) + when struct in [And, Or] do + %{ + compound + | left: do_replace_value(left, substitutions), + right: do_replace_value(right, substitutions) + } + end + + defp do_replace_value(value, substitutions) do + case Map.fetch(substitutions, value) do + {:ok, new_value} -> + new_value + + _ -> + value + end + end + + defp clear_equality_values(filter) do + new_attrs = + Enum.reduce(filter.attributes, %{}, fn {field, value}, attributes -> + Map.put(attributes, field, do_clear_equality_value(value)) + end) + + new_relationships = + Enum.reduce(filter.relationships, %{}, fn {relationship, related_filter}, relationships -> + new_relationship_filter = clear_equality_values(related_filter) + + Map.put(relationships, relationship, new_relationship_filter) + end) + + new_not = + if filter.not do + clear_equality_values(filter) + else + filter.not + end + + new_ors = + Enum.reduce(filter.ors, [], fn or_filter, ors -> + new_or = clear_equality_values(or_filter) + + [new_or | ors] + end) + + new_ands = + Enum.reduce(filter.ands, [], fn and_filter, ands -> + new_and = clear_equality_values(and_filter) + + [new_and | ands] + end) + + %{ + filter + | attributes: new_attrs, + relationships: new_relationships, + not: new_not, + ors: Enum.reverse(new_ors), + ands: Enum.reverse(new_ands) + } + end + + defp do_clear_equality_value(%struct{left: left, right: right} = compound) + when struct in [And, Or] do + %{ + compound + | left: do_clear_equality_value(left), + right: do_clear_equality_value(right) + } + end + + defp do_clear_equality_value(%Eq{value: _} = filter), do: %{filter | value: nil} + defp do_clear_equality_value(%In{values: _}), do: %Eq{value: nil} + defp do_clear_equality_value(other), do: other + + defp do_optional_paths(%{relationships: relationships, requests: requests, ors: ors}) + when relationships == %{} and ors in [[], nil] do + Enum.map(requests, fn request -> + request.path + end) + end + + defp do_optional_paths(%{ors: [first | rest]} = filter) do + do_optional_paths(first) ++ do_optional_paths(%{filter | ors: rest}) + end + + defp do_optional_paths(%{relationships: relationships} = filter) when is_map(relationships) do + relationship_paths = + Enum.flat_map(relationships, fn {_, value} -> + do_optional_paths(value) + end) + + relationship_paths ++ do_optional_paths(%{filter | relationships: %{}}) + end + + def request_filter_for_fetch(filter, data) do + filter + |> optional_paths() + |> paths_and_data(data) + |> most_specific_paths() + |> Enum.reduce(filter, fn {path, %{data: related_data}}, filter -> + [:filter, relationship_path] = path + + filter + |> add_records_to_relationship_filter( + relationship_path, + List.wrap(related_data) + ) + |> lift_impossibility() + end) + end + + defp most_specific_paths(paths_and_data) do + Enum.reject(paths_and_data, fn {path, _} -> + Enum.any?(paths_and_data, &path_is_more_specific?(path, &1)) + end) + end + + # I don't think this is a possibility + defp path_is_more_specific?([], []), do: false + defp path_is_more_specific?(_, []), do: true + # first element of the search matches first element of candidate + defp path_is_more_specific?([part | rest], [part | candidate_rest]) do + path_is_more_specific?(rest, candidate_rest) + end + + defp path_is_more_specific?(_, _), do: false + + defp paths_and_data(paths, data) do + Enum.flat_map(paths, fn path -> + case Engine.fetch_nested_value(data, path) do + {:ok, related_data} -> [{path, related_data}] + :error -> [] + end + end) + end + + def empty_filter?(filter) do + filter.attributes == %{} and filter.relationships == %{} and filter.not == nil and + filter.ors in [[], nil] and filter.ands in [[], nil] and filter.impossible? == false + end + + defp add_records_to_relationship_filter(filter, [], records) do + case PrimaryKeyHelpers.values_to_primary_key_filters(filter.resource, records) do + {:error, error} -> + add_error(filter, error) + + {:ok, []} -> + if filter.ors in [[], nil] do + %{filter | impossible?: true} + else + filter + end + + {:ok, [single]} -> + do_parse(single, filter) + + {:ok, many} -> + do_parse([or: many], filter) + end + end + + defp add_records_to_relationship_filter(filter, [relationship | rest] = path, records) do + filter + |> Map.update!(:relationships, fn relationships -> + case Map.fetch(relationships, relationship) do + {:ok, related_filter} -> + Map.put( + relationships, + relationship, + add_records_to_relationship_filter(related_filter, rest, records) + ) + + :error -> + relationships + end + end) + |> Map.update!(:ors, fn ors -> + Enum.map(ors, &add_records_to_relationship_filter(&1, path, records)) + end) + end + + defp lift_impossibility(filter) do + filter = + filter + |> Map.update!(:relationships, fn relationships -> + Enum.reduce(relationships, relationships, fn {key, filter}, relationships -> + Map.put(relationships, key, lift_impossibility(filter)) + end) + end) + |> Map.update!(:ands, fn ands -> + Enum.map(ands, &lift_impossibility/1) + end) + |> Map.update!(:ors, fn ors -> + Enum.map(ors, &lift_impossibility/1) + end) + + with_related_impossibility = + if Enum.any?(filter.relationships || %{}, fn {_, val} -> Map.get(val, :impossible?) end) do + Map.put(filter, :impossible?, true) + else + filter + end + + if Enum.any?(with_related_impossibility.ands, &Map.get(&1, :impossible?)) do + Map.put(with_related_impossibility, :impossible?, true) + else + with_related_impossibility + end + end + + defp add_not_filter_info(filter) do + case filter.not do + nil -> + filter + + not_filter -> + filter + |> add_request(not_filter.requests) + |> add_error(not_filter.errors) + end + end + + def predicate_strict_subset_of?(attribute, %left_struct{} = left, right) do + left_struct.strict_subset_of?(attribute, left, right) + end + + def add_to_filter(filter, %__MODULE__{} = addition) do + cond do + empty_filter?(filter) -> + addition + + empty_filter?(addition) -> + filter + + true -> + %{addition | ands: [filter | addition.ands]} + |> lift_impossibility() + |> lift_if_empty() + |> add_not_filter_info() + end + end + + def add_to_filter(filter, additions) do + parsed = parse(filter.resource, additions, filter.api) + + add_to_filter(filter, parsed) + end + + defp do_parse(filter_statement, %{resource: resource} = filter) do + Enum.reduce(filter_statement, filter, fn + {key, value}, filter -> + cond do + key == :__impossible__ && value == true -> + %{filter | impossible?: true} + + key == :and -> + add_and_to_filter(filter, value) + + key == :or -> + add_or_to_filter(filter, value) + + key == :not -> + add_to_not_filter(filter, value) + + attr = Ash.attribute(resource, key) -> + add_attribute_filter(filter, attr, value) + + rel = Ash.relationship(resource, key) -> + add_relationship_filter(filter, rel, value) + + true -> + add_error( + filter, + "Attempted to filter on #{key} which is neither a relationship, nor a field of #{ + inspect(resource) + }" + ) + end + end) + |> lift_impossibility() + |> lift_if_empty() + |> add_not_filter_info() + end + + defp add_and_to_filter(filter, value) do + if Keyword.keyword?(value) do + %{filter | ands: [parse(filter.resource, value, filter.api) | filter.ands]} + else + empty_filter = parse(filter.resource, [], filter.api) + + filter_with_ands = %{ + empty_filter + | ands: Enum.map(value, &parse(filter.resource, &1, filter.api)) + } + + %{filter | ands: [filter_with_ands | filter.ands]} + end + end + + defp add_or_to_filter(filter, value) do + if Keyword.keyword?(value) do + %{filter | ors: [parse(filter.resource, value, filter.api) | filter.ors]} + else + [first_or | rest_ors] = Enum.map(value, &parse(filter.resource, &1, filter.api)) + + or_filter = + filter.resource + |> parse(first_or, filter.api) + |> Map.update!(:ors, &Kernel.++(&1, rest_ors)) + + %{filter | ands: [or_filter | filter.ands]} + end + end + + defp add_to_not_filter(filter, value) do + Map.update!(filter, :not, fn not_filter -> + if not_filter do + add_to_filter(not_filter, value) + else + parse(filter.resource, value, filter.api) + end + end) + end + + defp lift_if_empty(%{ + ors: [], + ands: [and_filter | rest], + attributes: attrs, + relationships: rels, + not: nil, + errors: errors + }) + when attrs == %{} and rels == %{} do + and_filter + |> Map.update!(:ands, &Kernel.++(&1, rest)) + |> lift_if_empty() + |> Map.update!(:errors, &Kernel.++(&1, errors)) + end + + defp lift_if_empty(%{ + ands: [], + ors: [or_filter | rest], + attributes: attrs, + relationships: rels, + not: nil, + errors: errors + }) + when attrs == %{} and rels == %{} do + or_filter + |> Map.update!(:ors, &Kernel.++(&1, rest)) + |> lift_if_empty() + |> Map.update!(:errors, &Kernel.++(&1, errors)) + end + + defp lift_if_empty(filter) do + filter + end + + defp add_attribute_filter(filter, attr, value) do + if Keyword.keyword?(value) do + Enum.reduce(value, filter, fn + {predicate_name, value}, filter -> + do_add_attribute_filter(filter, attr, predicate_name, value) + end) + else + add_attribute_filter(filter, attr, eq: value) + end + end + + defp do_add_attribute_filter( + %{attributes: attributes, resource: resource} = filter, + %{type: attr_type, name: attr_name}, + predicate_name, + value + ) do + case parse_predicate(resource, predicate_name, attr_name, attr_type, value) do + {:ok, predicate} -> + new_attributes = + Map.update( + attributes, + attr_name, + predicate, + &Merge.merge(&1, predicate) + ) + + %{filter | attributes: new_attributes} + + {:error, error} -> + add_error(filter, error) + end + end + + def parse_predicates(resource, keyword, attr_name, attr_type) do + Enum.reduce(keyword, {:ok, nil}, fn {predicate_name, value}, {:ok, existing_predicate} -> + case parse_predicate(resource, predicate_name, attr_name, attr_type, value) do + {:ok, predicate} when is_nil(existing_predicate) -> + {:ok, predicate} + + {:ok, predicate} -> + {:ok, Merge.merge(existing_predicate, predicate)} + + {:error, error} -> + {:error, error} + end + end) + end + + def count_of_clauses(nil), do: 0 + + def count_of_clauses(filter) do + relationship_clauses = + filter.relationships + |> Map.values() + |> Enum.map(fn related_filter -> + 1 + count_of_clauses(related_filter) + end) + |> Enum.sum() + + or_clauses = + filter.ors + |> Kernel.||([]) + |> Enum.map(&count_of_clauses/1) + |> Enum.sum() + + not_clauses = count_of_clauses(filter.not) + + and_clauses = + filter.ands + |> Enum.map(&count_of_clauses/1) + |> Enum.sum() + + Enum.count(filter.attributes) + relationship_clauses + or_clauses + not_clauses + and_clauses + end + + defp parse_predicate(resource, predicate_name, attr_name, attr_type, value) do + data_layer = Ash.data_layer(resource) + + data_layer_predicates = + Map.get(Ash.data_layer_filters(resource), Ash.Type.storage_type(attr_type), []) + + all_predicates = + Enum.reduce(data_layer_predicates, @predicates, fn {name, module}, all_predicates -> + Map.put(all_predicates, name, module) + end) + + with {:predicate_type, {:ok, predicate_type}} <- + {:predicate_type, Map.fetch(all_predicates, predicate_name)}, + {:type_can?, _, true} <- + {:type_can?, predicate_name, + Keyword.has_key?(data_layer_predicates, predicate_name) or + Ash.Type.supports_filter?(resource, attr_type, predicate_name, data_layer)}, + {:data_layer_can?, _, true} <- + {:data_layer_can?, predicate_name, + Ash.data_layer_can?(resource, {:filter, predicate_name})}, + {:predicate, _, {:ok, predicate}} <- + {:predicate, attr_name, predicate_type.new(resource, attr_name, attr_type, value)} do + {:ok, predicate} + else + {:predicate_type, :error} -> + {:error, :predicate_type, "No such filter type #{predicate_name}"} + + {:predicate, attr_name, {:error, error}} -> + {:error, Map.put(error, :field, attr_name)} + + {:type_can?, predicate_name, false} -> + {:error, + "Cannot use filter type #{inspect(predicate_name)} on type #{inspect(attr_type)}."} + + {:data_layer_can?, predicate_name, false} -> + {:error, "data layer not capable of provided filter: #{predicate_name}"} + end + end + + defp add_relationship_filter( + %{relationships: relationships} = filter, + %{destination: destination, name: name} = relationship, + value + ) do + case parse_relationship_filter(value, relationship) do + {:ok, provided_filter} -> + related_filter = parse(destination, provided_filter, filter.api, [name | filter.path]) + + new_relationships = + Map.update(relationships, name, related_filter, &Merge.merge(&1, related_filter)) + + filter + |> Map.put(:relationships, new_relationships) + |> add_relationship_compatibility_error(relationship) + |> add_error(related_filter.errors) + |> add_request(related_filter.requests) + + {:error, error} -> + add_error(filter, error) + end + end + + defp parse_relationship_filter(value, %{destination: destination} = relationship) do + cond do + match?(%__MODULE__{}, value) -> + {:ok, value} + + match?(%^destination{}, value) -> + PrimaryKeyHelpers.value_to_primary_key_filter(destination, value) + + is_map(value) -> + {:ok, Map.to_list(value)} + + Keyword.keyword?(value) -> + {:ok, value} + + is_list(value) -> + parse_relationship_list_filter(value, relationship) + + true -> + PrimaryKeyHelpers.value_to_primary_key_filter(destination, value) + end + end + + defp parse_relationship_list_filter(value, relationship) do + Enum.reduce_while(value, {:ok, []}, fn item, items -> + case parse_relationship_filter(item, relationship) do + {:ok, item_filter} -> {:cont, {:ok, [item_filter | items]}} + {:error, error} -> {:halt, {:error, error}} + end + end) + end + + defp add_relationship_compatibility_error(%{resource: resource} = filter, %{ + cardinality: cardinality, + destination: destination, + name: name + }) do + cond do + not Ash.data_layer_can?(resource, {:filter_related, cardinality}) -> + add_error( + filter, + "Cannot filter on relationship #{name}: #{inspect(Ash.data_layer(resource))} does not support it." + ) + + not (Ash.data_layer(destination) == Ash.data_layer(resource)) -> + add_error( + filter, + "Cannot filter on related entites unless they share a data layer, for now." + ) + + true -> + filter + end + end + + defp add_request(filter, requests) + when is_list(requests), + do: %{filter | requests: filter.requests ++ requests} + + defp add_request(%{requests: requests} = filter, request), + do: %{filter | requests: [request | requests]} + + defp add_error(%{errors: errors} = filter, errors) when is_list(errors), + do: %{filter | errors: filter.errors ++ errors} + + defp add_error(%{errors: errors} = filter, error), do: %{filter | errors: [error | errors]} +end + +defimpl Inspect, for: Ash.Filter do + import Inspect.Algebra + import Ash.Filter.InspectHelpers + + defguardp is_empty(val) when is_nil(val) or val == [] or val == %{} + + def inspect( + %Ash.Filter{ + not: not_filter, + ors: ors, + relationships: relationships, + attributes: attributes, + ands: ands + }, + opts + ) + when not is_nil(not_filter) and is_empty(ors) and is_empty(relationships) and + is_empty(attributes) and is_empty(ands) do + if root?(opts) do + concat(["#Filter"]) + else + concat(["not ", to_doc(not_filter, make_non_root(opts))]) + end + end + + def inspect(%Ash.Filter{not: not_filter} = filter, opts) when not is_nil(not_filter) do + if root?(opts) do + concat([ + "#Filter" + ]) + else + concat([ + "not ", + to_doc(not_filter, make_non_root(opts)), + " and ", + to_doc(%{filter | not: nil}, make_non_root(opts)) + ]) + end + end + + def inspect( + %Ash.Filter{ors: ors, relationships: relationships, attributes: attributes, ands: ands}, + opts + ) + when is_empty(ors) and is_empty(relationships) and is_empty(attributes) and is_empty(ands) do + if root?(opts) do + concat(["#Filter<", to_doc(nil, opts), ">"]) + else + to_doc(nil, opts) + end + end + + def inspect(filter, opts) do + rels = parse_relationships(filter, opts) + attrs = parse_attributes(filter, opts) + + and_container = + case attrs ++ rels do + [] -> + empty() + + [and_clause] -> + and_clause + + and_clauses -> + Inspect.Algebra.container_doc("(", and_clauses, ")", opts, fn term, _ -> term end, + break: :flex, + separator: " and" + ) + end + + with_or_container = + case Map.get(filter, :ors) do + nil -> + and_container + + [] -> + and_container + + ors -> + inspected_ors = Enum.map(ors, fn filter -> to_doc(filter, make_non_root(opts)) end) + + or_container = + Inspect.Algebra.container_doc( + "(", + inspected_ors, + ")", + opts, + fn term, _ -> term end, + break: :strict, + separator: " or " + ) + + if Enum.empty?(attrs) && Enum.empty?(rels) do + or_container + else + concat(["(", and_container, " or ", or_container, ")"]) + end + end + + all_container = + case filter.ands do + [] -> + with_or_container + + ands -> + docs = [with_or_container | Enum.map(ands, &Inspect.inspect(&1, make_non_root(opts)))] + + Inspect.Algebra.container_doc( + "(", + docs, + ")", + opts, + fn term, _ -> term end, + break: :strict, + separator: " and " + ) + end + + if root?(opts) do + concat(["#Filter<", all_container, ">"]) + else + all_container + end + end + + defp parse_relationships(%Ash.Filter{relationships: relationships}, _opts) + when relationships == %{}, + do: [] + + defp parse_relationships(filter, opts) do + filter + |> Map.fetch!(:relationships) + |> Enum.map(fn {key, value} -> to_doc(value, add_to_path(opts, key)) end) + end + + defp parse_attributes(%Ash.Filter{attributes: attributes}, _opts) when attributes == %{}, do: [] + + defp parse_attributes(filter, opts) do + filter + |> Map.fetch!(:attributes) + |> Enum.map(fn {key, value} -> to_doc(value, put_attr(opts, key)) end) + end +end diff --git a/lib/ash/filter/in.ex b/filter/in.ex similarity index 100% rename from lib/ash/filter/in.ex rename to filter/in.ex diff --git a/lib/ash/filter/inspect.ex b/filter/inspect.ex similarity index 100% rename from lib/ash/filter/inspect.ex rename to filter/inspect.ex diff --git a/lib/ash/filter/merge.ex b/filter/merge.ex similarity index 100% rename from lib/ash/filter/merge.ex rename to filter/merge.ex diff --git a/lib/ash/filter/not_eq.ex b/filter/not_eq.ex similarity index 100% rename from lib/ash/filter/not_eq.ex rename to filter/not_eq.ex diff --git a/lib/ash/filter/not_in.ex b/filter/not_in.ex similarity index 100% rename from lib/ash/filter/not_in.ex rename to filter/not_in.ex diff --git a/lib/ash/filter/or.ex b/filter/or.ex similarity index 100% rename from lib/ash/filter/or.ex rename to filter/or.ex diff --git a/lib/ash.ex b/lib/ash.ex index ea7e9d57c..4aabaccdb 100644 --- a/lib/ash.ex +++ b/lib/ash.ex @@ -175,6 +175,15 @@ defmodule Ash do |> Enum.find(&(&1.name == name)) end + def related(resource, []), do: resource + + def related(resource, [path | rest]) do + case relationship(resource, path) do + %{destination: destination} -> related(destination, rest) + nil -> nil + end + end + @doc "The data layer of the resource, or nil if it does not have one" @spec data_layer(resource()) :: data_layer() def data_layer(resource) do diff --git a/lib/ash/actions/read.ex b/lib/ash/actions/read.ex index 549127df8..0f57552ad 100644 --- a/lib/ash/actions/read.ex +++ b/lib/ash/actions/read.ex @@ -3,6 +3,7 @@ defmodule Ash.Actions.Read do alias Ash.Actions.SideLoad alias Ash.Engine alias Ash.Engine.Request + alias Ash.Filter require Logger def run(query, _action, opts \\ []) do @@ -40,31 +41,37 @@ defmodule Ash.Actions.Read do end defp requests(query, action, opts) do + filter_requests = + if Keyword.has_key?(opts, :actor) || opts[:authorize?] do + Filter.read_requests(query.filter) + else + [] + end + request = Request.new( resource: query.resource, api: query.api, query: query, action: action, - data: data_field(opts, query.filter, query.resource, query.data_layer_query), + data: data_field(opts, filter_requests, query.resource, query.data_layer_query), path: [:data], name: "#{action.type} - `#{action.name}`" ) - [request | Map.get(query.filter || %{}, :requests, [])] + [request | filter_requests] end - defp data_field(params, filter, resource, query) do + defp data_field(params, filter_requests, resource, query) do if params[:initial_data] do List.wrap(params[:initial_data]) else - Request.resolve( - [[:data, :query]], - Ash.Filter.optional_paths(filter), - fn %{data: %{query: ash_query}} = data -> - fetch_filter = Ash.Filter.request_filter_for_fetch(ash_query.filter, data) + relationship_filter_paths = Enum.map(filter_requests, &[&1.path, :authorization_filter]) - with {:ok, query} <- Ash.DataLayer.filter(query, fetch_filter, resource), + Request.resolve( + [[:data, :query] | relationship_filter_paths], + fn %{data: %{query: ash_query}} -> + with {:ok, query} <- Ash.DataLayer.filter(query, ash_query.filter, resource), {:ok, query} <- Ash.DataLayer.limit(query, ash_query.limit, resource), {:ok, query} <- Ash.DataLayer.offset(query, ash_query.offset, resource) do Ash.DataLayer.run_query(query, resource) diff --git a/lib/ash/actions/relationships.ex b/lib/ash/actions/relationships.ex index 7c4b3272c..e274a92aa 100644 --- a/lib/ash/actions/relationships.ex +++ b/lib/ash/actions/relationships.ex @@ -270,7 +270,7 @@ defmodule Ash.Actions.Relationships do {:ok, %{replace: identifier}} {:error, _} -> - {:error, "Relationship change invalid for #{relationship.name} 2"} + {:error, "Relationship change invalid map for #{relationship.name}"} end end @@ -283,7 +283,7 @@ defmodule Ash.Actions.Relationships do {:ok, %{replace: identifiers}} {:error, _} -> - {:error, "Relationship change invalid for #{relationship.name} 2"} + {:error, "Relationship change invalid list for #{relationship.name}"} end end diff --git a/lib/ash/data_layer/ets/ets.ex b/lib/ash/data_layer/ets/ets.ex index 6ebc44d66..5d10253af 100644 --- a/lib/ash/data_layer/ets/ets.ex +++ b/lib/ash/data_layer/ets/ets.ex @@ -5,7 +5,8 @@ defmodule Ash.DataLayer.Ets do This is used for testing. *Do not use this data layer in production* """ - alias Ash.Filter.{And, Eq, In, NotEq, NotIn, Or} + alias Ash.Filter.{Predicate, Expression, Not} + alias Ash.Filter.Predicate.{Eq, In} @behaviour Ash.DataLayer @@ -80,21 +81,13 @@ defmodule Ash.DataLayer.Ets do {:ok, %{query | sort: sort}} end - @impl true - def run_query(%Query{filter: %Ash.Filter{impossible?: true}}, _), do: {:ok, []} - @impl true def run_query( %Query{resource: resource, filter: filter, offset: offset, limit: limit, sort: sort}, _resource ) do - with {:ok, table} <- wrap_or_create_table(resource), - {:ok, records} <- ETS.Set.to_list(table) do - filtered_records = - records - |> Enum.map(&elem(&1, 1)) - |> filter_matches(filter) - + with {:ok, records} <- get_records(resource), + filtered_records <- filter_matches(records, filter) do offset_records = filtered_records |> do_sort(sort) @@ -113,110 +106,106 @@ defmodule Ash.DataLayer.Ets do end end - defp filter_matches(records, filter) do - Enum.filter(records, &matches_filter?(&1, filter)) - end - - defp matches_filter?(record, %{ands: [first | rest]} = filter) do - matches_filter?(record, first) and matches_filter?(record, %{filter | ands: rest}) - end - - defp matches_filter?(record, %{not: not_filter} = filter) when not is_nil(not_filter) do - not matches_filter?(record, not_filter) and matches_filter?(record, %{filter | not: nil}) - end - - defp matches_filter?(record, %{ors: [first | rest]} = filter) do - matches_filter?(record, first) or matches_filter?(record, %{filter | ors: rest}) - end - - defp matches_filter?(record, %{resource: resource} = filter) do - resource - |> relationships_to_attribute_filters(filter) - |> Map.get(:attributes) - |> Enum.all?(fn {key, predicate} -> - matches_predicate?(Map.get(record, key), predicate) - end) + defp get_records(resource) do + with {:ok, table} <- wrap_or_create_table(resource), + {:ok, record_tuples} <- ETS.Set.to_list(table) do + {:ok, Enum.map(record_tuples, &elem(&1, 1))} + end end - # alias Ash.Filter.{In, NotIn} - - defp matches_predicate?(value, %Eq{value: predicate_value}) do - value == predicate_value - end + defp filter_matches(records, nil), do: records - defp matches_predicate?(value, %NotEq{value: predicate_value}) do - value != predicate_value + defp filter_matches(records, filter) do + Enum.filter(records, &matches_filter?(&1, filter.expression)) end - defp matches_predicate?(value, %In{values: predicate_value}) do - value in predicate_value + defp matches_filter?( + record, + %Predicate{ + predicate: predicate, + attribute: %{name: name}, + relationship_path: [] + } + ) do + matches_predicate?(record, name, predicate) end - defp matches_predicate?(value, %NotIn{values: predicate_value}) do - value not in predicate_value + defp matches_filter?( + record, + %Predicate{ + predicate: predicate, + attribute: %{name: name}, + relationship_path: path + } + ) do + record + |> get_related(path) + |> Enum.any?(&matches_predicate?(&1, name, predicate)) end - defp matches_predicate?(value, %And{left: left, right: right}) do - matches_predicate?(value, left) and matches_predicate?(value, right) + defp matches_filter?(record, %Expression{op: :and, left: left, right: right}) do + matches_filter?(record, left) && matches_filter?(record, right) end - defp matches_predicate?(value, %Or{left: left, right: right}) do - matches_predicate?(value, left) or matches_predicate?(value, right) + defp matches_filter?(record, %Expression{op: :or, left: left, right: right}) do + matches_filter?(record, left) || matches_filter?(record, right) end - defp relationships_to_attribute_filters(_, %{relationships: relationships} = filter) - when relationships in [nil, %{}] do - filter + defp matches_filter?(record, %Not{expression: expression}) do + not matches_filter?(record, expression) end - defp relationships_to_attribute_filters(resource, %{relationships: relationships} = filter) do - Enum.reduce(relationships, filter, fn {rel, related_filter}, filter -> - relationship = Ash.relationship(resource, rel) - - {field, parsed_related_filter} = related_ids_filter(relationship, related_filter) + defp get_related(record_or_records, []), do: List.wrap(record_or_records) + + defp get_related(%resource{} = record, [first | rest]) do + relationship = Ash.relationship(resource, first) + source_value = Map.get(record, relationship.source_field) + + related = + if is_nil(Map.get(record, relationship.source_field)) do + [] + else + case Ash.relationship(resource, first) do + %{type: :many_to_many} = relationship -> + {:ok, through_records} = get_records(relationship.through) + {:ok, destination_records} = get_records(relationship.destination) + + through_records + |> Enum.reject(&is_nil(Map.get(&1, relationship.destination_field_on_join_table))) + |> Enum.flat_map(fn through_record -> + if Map.get(through_record, relationship.source_field_on_join_table) == + source_value do + Enum.filter(destination_records, fn destination_record -> + Map.get(through_record, relationship.destination_field_on_join_table) == + Map.get(destination_record, relationship.destination_field) + end) + else + [] + end + end) + + relationship -> + {:ok, destination_records} = get_records(relationship.destination) + + Enum.filter(destination_records, fn destination_record -> + Map.get(destination_record, relationship.destination_field) == source_value + end) + end + end - Ash.Filter.add_to_filter(filter, [{field, parsed_related_filter}]) - end) + related + |> List.wrap() + |> Enum.flat_map(&get_related(&1, rest)) end - defp related_ids_filter(%{type: :many_to_many} = rel, filter) do - destination_query = %Query{ - resource: rel.destination, - filter: filter - } - - with {:ok, results} <- run_query(destination_query, rel.destination), - destination_values <- Enum.map(results, &Map.get(&1, rel.destination_field)), - %{errors: []} = through_filter <- - Ash.Filter.parse( - rel.through, - [ - {rel.destination_field_on_join_table, [in: destination_values]} - ], - filter.api - ), - {:ok, join_results} <- - run_query(%Query{resource: rel.through, filter: through_filter}, rel.through) do - {rel.source_field, - [in: Enum.map(join_results, &Map.get(&1, rel.source_field_on_join_table))]} - else - %{errors: errors} -> {:error, errors} - {:error, error} -> {:error, error} - end + defp matches_predicate?(record, field, %Eq{value: predicate_value}) do + Map.fetch(record, field) == {:ok, predicate_value} end - defp related_ids_filter(rel, filter) do - query = %Query{ - resource: rel.destination, - filter: filter - } - - case run_query(query, rel.destination) do - {:ok, results} -> - {rel.source_field, [in: Enum.map(results, &Map.get(&1, rel.destination_field))]} - - {:error, error} -> - {:error, error} + defp matches_predicate?(record, field, %In{values: predicate_values}) do + case Map.fetch(record, field) do + {:ok, value} -> value in predicate_values + :error -> false end end diff --git a/lib/ash/engine/engine.ex b/lib/ash/engine/engine.ex index f88debe9b..725c813c3 100644 --- a/lib/ash/engine/engine.ex +++ b/lib/ash/engine/engine.ex @@ -30,13 +30,6 @@ defmodule Ash.Engine do authorize? = opts[:authorize?] || Keyword.has_key?(opts, :actor) actor = opts[:actor] - requests = - if authorize? do - requests - else - Enum.reject(requests, & &1.skip_unless_authorize?) - end - case Request.validate_requests(requests) do :ok -> requests = diff --git a/lib/ash/engine/request.ex b/lib/ash/engine/request.ex index b220060dc..b4e4feaa4 100644 --- a/lib/ash/engine/request.ex +++ b/lib/ash/engine/request.ex @@ -43,9 +43,10 @@ defmodule Ash.Engine.Request do :data, :name, :api, + :authorization_filter, :query, :write_to_data?, - :skip_unless_authorize?, + :strict_check_only?, :verbose?, :state, :actor, @@ -106,7 +107,7 @@ defmodule Ash.Engine.Request do query: query, api: opts[:api], name: opts[:name], - skip_unless_authorize?: opts[:skip_unless_authorize?], + strict_check_only?: opts[:strict_check_only?], state: :strict_check, actor: opts[:actor], verbose?: opts[:verbose?] || false, @@ -401,19 +402,41 @@ defmodule Ash.Engine.Request do {:filter, filter} -> request |> Map.update!(:query, &Ash.Query.filter(&1, filter)) + |> Map.update(:authorization_filter, filter, fn authorization_filter -> + if authorization_filter do + Ash.Filter.add_to_filter(authorization_filter, filter) + else + filter + end + end) |> set_authorizer_state(authorizer, :authorized) |> try_resolve([request.path ++ [:query]], false, false) {:filter_and_continue, filter, new_authorizer_state} -> - new_request = - request - |> Map.update!(:query, &Ash.Query.filter(&1, filter)) - |> set_authorizer_state(authorizer, new_authorizer_state) - - {:ok, new_request} + if request.strict_check_only? do + {:error, "Request must pass strict check"} + else + new_request = + request + |> Map.update(:authorization_filter, filter, fn authorization_filter -> + if authorization_filter do + Ash.Filter.add_to_filter(authorization_filter, filter) + else + filter + end + end) + |> Map.update!(:query, &Ash.Query.filter(&1, filter)) + |> set_authorizer_state(authorizer, new_authorizer_state) + + {:ok, new_request} + end {:continue, authorizer_state} -> - {:ok, set_authorizer_state(request, authorizer, authorizer_state)} + if request.strict_check_only? do + {:error, "Request must pass strict check"} + else + {:ok, set_authorizer_state(request, authorizer, authorizer_state)} + end {:error, error} -> {:error, error} @@ -476,7 +499,15 @@ defmodule Ash.Engine.Request do {:ok, set_authorizer_state(request, authorizer, :authorized)} {:filter, filter} -> - runtime_filter(request, authorizer, filter) + request + |> Map.update(:authorization_filter, filter, fn authorization_filter -> + if authorization_filter do + Ash.Filter.add_to_filter(authorization_filter, filter) + else + filter + end + end) + |> runtime_filter(authorizer, filter) {:error, error} -> {:error, error} @@ -593,7 +624,7 @@ defmodule Ash.Engine.Request do authorized? = Enum.all?(Map.values(request.authorizer_state), &(&1 == :authorized)) # Don't fetch honor requests for dat until the request is authorized - if field in [:data, :query] and not authorized? and not internal? do + if field in [:data, :query, :authorization_filter] and not authorized? and not internal? do try_resolve_dependencies_of(request, field, internal?, optional?) else case Map.get(request, field) do diff --git a/lib/ash/error.ex b/lib/ash/error.ex index 17b328229..dd7df45b1 100644 --- a/lib/ash/error.ex +++ b/lib/ash/error.ex @@ -45,11 +45,20 @@ defmodule Ash.Error do end def choose_error(errors) do - [error | _other_errors] = Enum.sort_by(errors, &Map.get(@error_class_indices, &1.class)) + [error | other_errors] = + Enum.sort_by(errors, fn error -> + # the second element here sorts errors that are already parent errors + {Map.get(@error_class_indices, error.class), + @error_modules[error.class] != error.__struct__} + end) parent_error_module = @error_modules[error.class] - parent_error_module.exception(errors: errors) + if parent_error_module == error.__struct__ do + parent_error_module.exception(errors: (error.errors || []) ++ other_errors) + else + parent_error_module.exception(errors: errors) + end end def error_messages(errors) do diff --git a/lib/ash/error/filter/invalid_filter_value.ex b/lib/ash/error/filter/invalid_filter_value.ex index ee41d7ab0..974563cda 100644 --- a/lib/ash/error/filter/invalid_filter_value.ex +++ b/lib/ash/error/filter/invalid_filter_value.ex @@ -11,12 +11,12 @@ defmodule Ash.Error.Filter.InvalidFilterValue do def class(_), do: :invalid - def message(%{field: field, value: value, filter: filter}) do - "Invalid filter value #{inspect(value)} supplied for #{inspect(field)}#{inspect(filter)}" + def message(%{value: value, filter: filter}) do + "Invalid filter value `#{inspect(value)}` supplied in: `#{inspect(filter)}`" end - def description(%{field: field, filter: filter, value: value}) do - "Invalid filter value #{inspect(value)} supplied for #{inspect(field)}#{inspect(filter)}" + def description(%{filter: filter, value: value}) do + "Invalid filter value `#{inspect(value)}` supplied in: `#{inspect(filter)}`" end def stacktrace(_), do: nil diff --git a/lib/ash/filter2/expression.ex b/lib/ash/filter/expression.ex similarity index 90% rename from lib/ash/filter2/expression.ex rename to lib/ash/filter/expression.ex index a1f6716c0..c8074eb51 100644 --- a/lib/ash/filter2/expression.ex +++ b/lib/ash/filter/expression.ex @@ -1,6 +1,7 @@ -defmodule Ash.Filter2.Expression do +defmodule Ash.Filter.Expression do defstruct [:op, :left, :right] + def new(_, nil, nil), do: nil def new(_, nil, right), do: right def new(_, left, nil), do: left @@ -13,7 +14,7 @@ defmodule Ash.Filter2.Expression do end end -defimpl Inspect, for: Ash.Filter2.Expression do +defimpl Inspect, for: Ash.Filter.Expression do import Inspect.Algebra def inspect( diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index 9117c1ba9..35e3b38ae 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -1,1123 +1,363 @@ defmodule Ash.Filter do - @moduledoc false - - # A filter expression in Ash. - - # The way we represent filters may be strange, but its important to have it structured, - # as merging and checking filter subsets are used all through ash for things like - # authorization. The `ands` of a filter are not subject to its `ors`. The `not` of a filter - # is also *not* subject to its `ors`. - # For instance, if a filter `A` has two `ands`, `B` and `C` and two `ors`, `D` and `E`, and - # a `not` of F, the expression as can be represented as `(A or D or E) and NOT F and B and C`. - - # The filters `attributes` and `relationships`, *are* subject to the `ors` of that filter. - - # ` AND NOT AND ( OR )` - - # This probably needs to be refactored into something more representative of its behavior, - # like a series of nested boolean expression structs w/ a reference to the attribute/relationship - # it references. Maybe. This would be similar to Ecto's `BooleanExpr` structs. - - alias Ash.Actions.PrimaryKeyHelpers - alias Ash.Engine + alias Ash.Filter.Predicate.{Eq, In} + alias Ash.Filter.{Expression, Not, Predicate} alias Ash.Engine.Request - alias Ash.Filter.{And, Eq, In, Merge, NotEq, NotIn, Or} - defstruct [ - :api, - :resource, - :not, - ands: [], - ors: [], - attributes: %{}, - relationships: %{}, - requests: [], - path: [], - errors: [], - impossible?: false + @built_in_predicates [ + eq: Eq, + in: In ] - @type t :: %__MODULE__{ - api: Ash.api(), - resource: Ash.resource(), - ors: list(%__MODULE__{} | nil), - not: %__MODULE__{} | nil, - attributes: Keyword.t(), - relationships: map(), - path: list(atom), - impossible?: boolean, - errors: list(String.t()), - requests: list(Request.t()) - } - - @predicates %{ - not_eq: NotEq, - not_in: NotIn, - eq: Eq, - in: In, - and: And, - or: Or - } + defstruct [:resource, :api, :expression] - @spec parse( - Ash.resource(), - Keyword.t(), - Ash.api(), - relationship_path :: list(atom) - ) :: t() - @doc """ - Parse a filter from a filter expression + def parse!(api, resource, statement) do + case parse(api, resource, statement) do + {:ok, filter} -> + filter - The only rason to pass `api` would be if you intend to leverage - any engine requests that would be generated by this filter. - """ - def parse(resource, filter, api, path \\ []) + {:error, error} -> + raise error + end + end - def parse(resource, [], api, _), - do: %__MODULE__{ + def parse(api, resource, statement) do + context = %{ + resource: resource, api: api, - resource: resource + relationship_path: [] } - def parse(_resource, %__MODULE__{} = filter, _, _) do - filter - end - - def parse(resource, filter, api, path) do - parsed_filter = do_parse(filter, %Ash.Filter{resource: resource, api: api, path: path}) - - source = - case path do - [] -> "filter" - path -> "related #{Enum.join(path, ".")} filter" - end - - if path == [] do - parsed_filter - else - query = - api - |> Ash.Query.new(resource) - |> Ash.Query.filter(parsed_filter) - - request = - Request.new( - resource: resource, - api: api, - query: query, - path: [:filter, path], - skip_unless_authorize?: true, - data: - Request.resolve( - [[:filter, path, :query]], - fn %{filter: %{^path => %{query: query}}} -> - data_layer_query = Ash.DataLayer.resource_to_query(resource) - - case Ash.DataLayer.filter(data_layer_query, query.filter, resource) do - {:ok, filtered_query} -> - Ash.DataLayer.run_query(filtered_query, resource) + case parse_expression(statement, context) do + {:ok, expression} -> + {:ok, %__MODULE__{expression: expression, resource: resource, api: api}} - {:error, error} -> - {:error, error} - end - end - ), - action: Ash.primary_action!(resource, :read), - relationship: path, - name: source - ) - - add_request( - parsed_filter, - request - ) + {:error, error} -> + {:error, error} end end - def optional_paths(filter) do - filter - |> do_optional_paths() + def relationship_paths(filter) do + filter.expression + |> do_relationship_paths() + |> List.wrap() + |> List.flatten() |> Enum.uniq() + |> Enum.map(fn {path} -> path end) end - @doc """ - Returns true if the second argument is a strict subset (always returns the same or less data) of the first - """ - def strict_subset_of(nil, _), do: true - - def strict_subset_of(_, nil), do: false - - def strict_subset_of(%{resource: resource}, %{resource: other_resource}) - when resource != other_resource, - do: false - - def strict_subset_of(filter, candidate) do - if empty_filter?(filter) do - true - else - if empty_filter?(candidate) do - false - else - {filter, candidate} = cosimplify(filter, candidate) - - Ash.SatSolver.strict_filter_subset(filter, candidate) - end - end - end - - def strict_subset_of?(filter, candidate) do - strict_subset_of(filter, candidate) == true - end - - def primary_key_filter?(nil), do: false - - def primary_key_filter?(filter) do - cleared_pkey_filter = - filter.resource - |> Ash.primary_key() - |> Enum.map(fn key -> {key, nil} end) - - case cleared_pkey_filter do - [] -> - false - - cleared_pkey_filter -> - parsed_cleared_pkey_filter = parse(filter.resource, cleared_pkey_filter, filter.api) - - cleared_candidate_filter = clear_equality_values(filter) - - strict_subset_of?(parsed_cleared_pkey_filter, cleared_candidate_filter) - end - end - - def get_pkeys(%{query: nil, resource: resource}, api, %_{} = item) do - pkey_filter = - item - |> Map.take(Ash.primary_key(resource)) - |> Map.to_list() - - api - |> Ash.Query.new(resource) - |> Ash.Query.filter(pkey_filter) - end - - def get_pkeys(%{query: query}, _, %resource{} = item) do - pkey_filter = - item - |> Map.take(Ash.primary_key(resource)) - |> Map.to_list() - - Ash.Query.filter(query, pkey_filter) - end - - def cosimplify(left, right) do - {new_left, new_right} = simplify_lists(left, right) - - express_mutual_exclusion(new_left, new_right) - end - - defp simplify_lists(left, right) do - values = get_all_values(left, get_all_values(right, %{})) - - substitutions = - Enum.reduce(values, %{}, fn {key, values}, substitutions -> - value_substitutions = simplify_list_substitutions(values) - - Map.put(substitutions, key, value_substitutions) - end) - - {replace_values(left, substitutions), replace_values(right, substitutions)} - end - - defp simplify_list_substitutions(values) do - Enum.reduce(values, %{}, fn value, substitutions -> - case do_simplify_list(value) do - {:ok, substitution} -> - Map.put(substitutions, value, substitution) - - :error -> - substitutions - end - end) - end - - defp do_simplify_list(%In{values: []}), do: :error - - defp do_simplify_list(%In{values: [value]}) do - {:ok, %Eq{value: value}} - end - - defp do_simplify_list(%In{values: [value | rest]}) do - {:ok, - Enum.reduce(rest, %Eq{value: value}, fn value, other_values -> - Or.prebuilt_new(%Eq{value: value}, other_values) - end)} + def add_to_filter( + %__MODULE__{resource: resource, api: api} = base, + %__MODULE__{resource: resource, api: api} = addition + ) do + {:ok, Expression.new(:and, base.expression, addition.expression)} end - defp do_simplify_list(%NotIn{values: []}), do: :error - - defp do_simplify_list(%NotIn{values: [value]}) do - {:ok, %NotEq{value: value}} + def add_to_filter(%__MODULE__{api: api} = base, %__MODULE__{api: api} = addition) do + {:error, + "Cannot add filter for resource #{inspect(addition.resource)} to filter with resource #{ + inspect(base.resource) + }"} end - defp do_simplify_list(%NotIn{values: [value | rest]}) do - {:ok, - Enum.reduce(rest, %Eq{value: value}, fn value, other_values -> - And.prebuilt_new(%NotEq{value: value}, other_values) - end)} + def add_to_filter( + %__MODULE__{resource: resource} = base, + %__MODULE__{resource: resource} = addition + ) do + {:error, + "Cannot add filter for api #{inspect(addition.api)} to filter with api #{inspect(base.api)}"} end - defp do_simplify_list(_), do: :error - - defp express_mutual_exclusion(left, right) do - values = get_all_values(left, get_all_values(right, %{})) - - substitutions = - Enum.reduce(values, %{}, fn {key, values}, substitutions -> - value_substitutions = express_mutual_exclusion_substitutions(values) - - Map.put(substitutions, key, value_substitutions) - end) - - {replace_values(left, substitutions), replace_values(right, substitutions)} + def add_to_filter(%__MODULE__{} = base, statement) do + case parse(base.api, base.resource, statement) do + {:ok, filter} -> add_to_filter(base, filter) + {:error, error} -> {:error, error} + end end - defp express_mutual_exclusion_substitutions(values) do - Enum.reduce(values, %{}, fn value, substitutions -> - case do_express_mutual_exclusion(value, values) do - {:ok, substitution} -> - Map.put(substitutions, value, substitution) - - :error -> - substitutions - end - end) + def relationship_filter_request_paths(filter) do + filter + |> relationship_paths() + |> Enum.map(&[:filter, &1]) end - defp do_express_mutual_exclusion(%Eq{value: value} = eq_filter, values) do - values - |> Enum.filter(fn - %Eq{value: other_value} -> value != other_value - _ -> false + def read_requests(filter) do + filter + |> Ash.Filter.relationship_paths() + |> Enum.map(fn path -> + {path, filter_expression_by_relationship_path(filter, path)} end) - |> case do - [] -> - :error - - [%{value: other_value}] -> - {:ok, And.prebuilt_new(eq_filter, %NotEq{value: other_value})} - - values -> - {:ok, - Enum.reduce(values, eq_filter, fn %{value: other_value}, expr -> - And.prebuilt_new(expr, %NotEq{value: other_value}) - end)} - end - end - - defp do_express_mutual_exclusion(_, _), do: :error - - defp get_all_values(filter, state) do - state = - filter.attributes - |> Enum.reduce(state, fn {field, value}, state -> - state - |> Map.put_new([filter.path, field], []) - |> Map.update!([filter.path, field], fn values -> - value - |> do_get_values() - |> Enum.reduce(values, fn value, values -> - [value | values] - end) - |> Enum.uniq() - end) - end) - - state = - Enum.reduce(filter.relationships, state, fn {_, relationship_filter}, new_state -> - get_all_values(relationship_filter, new_state) - end) + |> Enum.reduce_while({:ok, []}, fn {path, scoped_filter}, {:ok, requests} -> + %{api: api, resource: resource} = scoped_filter + + with %{errors: []} = query <- Ash.Query.new(api, resource), + %{errors: []} = query <- Ash.Query.filter(query, scoped_filter), + {:action, action} when not is_nil(action) <- + {:action, Ash.primary_action(resource, :read)} do + request = + Request.new( + resource: resource, + api: api, + query: query, + path: [:filter, path], + strict_check_only?: true, + action: action, + data: [] + ) - state = - if filter.not do - get_all_values(filter.not, state) + {:cont, {:ok, [request | requests]}} else - state + {:error, error} -> {:halt, {:error, error}} + %{errors: errors} -> {:halt, {:error, errors}} + {:action, nil} -> {:halt, {:error, "Default read action required"}} end - - state = - Enum.reduce(filter.ors, state, fn or_filter, new_state -> - get_all_values(or_filter, new_state) - end) - - Enum.reduce(filter.ands, state, fn and_filter, new_state -> - get_all_values(and_filter, new_state) end) end - defp do_get_values(%struct{left: left, right: right}) - when struct in [And, Or] do - do_get_values(left) ++ do_get_values(right) - end - - defp do_get_values(other), do: [other] - - defp replace_values(filter, substitutions) do - new_attrs = - Enum.reduce(filter.attributes, %{}, fn {field, value}, attributes -> - substitutions = Map.get(substitutions, [filter.path, field]) || %{} - - Map.put(attributes, field, do_replace_value(value, substitutions)) - end) - - new_relationships = - Enum.reduce(filter.relationships, %{}, fn {relationship, related_filter}, relationships -> - new_relationship_filter = replace_values(related_filter, substitutions) - - Map.put(relationships, relationship, new_relationship_filter) - end) - - new_not = - if filter.not do - replace_values(filter.not, substitutions) - else - filter.not - end - - new_ors = - Enum.reduce(filter.ors, [], fn or_filter, ors -> - new_or = replace_values(or_filter, substitutions) - - [new_or | ors] - end) - - new_ands = - Enum.reduce(filter.ands, [], fn and_filter, ands -> - new_and = replace_values(and_filter, substitutions) - - [new_and | ands] - end) - - %{ - filter - | attributes: new_attrs, - relationships: new_relationships, - not: new_not, - ors: Enum.reverse(new_ors), - ands: Enum.reverse(new_ands) - } - end - - defp do_replace_value(%struct{left: left, right: right} = compound, substitutions) - when struct in [And, Or] do - %{ - compound - | left: do_replace_value(left, substitutions), - right: do_replace_value(right, substitutions) + defp filter_expression_by_relationship_path(filter, path) do + %__MODULE__{ + api: filter.api, + resource: Ash.related(filter.resource, path), + expression: do_filter_expression_by_relationship_path(filter.expression, path) } end - defp do_replace_value(value, substitutions) do - case Map.fetch(substitutions, value) do - {:ok, new_value} -> - new_value - - _ -> - value - end - end - - defp clear_equality_values(filter) do - new_attrs = - Enum.reduce(filter.attributes, %{}, fn {field, value}, attributes -> - Map.put(attributes, field, do_clear_equality_value(value)) - end) - - new_relationships = - Enum.reduce(filter.relationships, %{}, fn {relationship, related_filter}, relationships -> - new_relationship_filter = clear_equality_values(related_filter) - - Map.put(relationships, relationship, new_relationship_filter) - end) - - new_not = - if filter.not do - clear_equality_values(filter) - else - filter.not - end - - new_ors = - Enum.reduce(filter.ors, [], fn or_filter, ors -> - new_or = clear_equality_values(or_filter) - - [new_or | ors] - end) - - new_ands = - Enum.reduce(filter.ands, [], fn and_filter, ands -> - new_and = clear_equality_values(and_filter) - - [new_and | ands] - end) - - %{ - filter - | attributes: new_attrs, - relationships: new_relationships, - not: new_not, - ors: Enum.reverse(new_ors), - ands: Enum.reverse(new_ands) - } - end + defp do_filter_expression_by_relationship_path( + %Expression{op: op, left: left, right: right}, + path + ) do + new_left = do_filter_expression_by_relationship_path(left, path) + new_right = do_filter_expression_by_relationship_path(right, path) - defp do_clear_equality_value(%struct{left: left, right: right} = compound) - when struct in [And, Or] do - %{ - compound - | left: do_clear_equality_value(left), - right: do_clear_equality_value(right) - } + Expression.new(op, new_left, new_right) end - defp do_clear_equality_value(%Eq{value: _} = filter), do: %{filter | value: nil} - defp do_clear_equality_value(%In{values: _}), do: %Eq{value: nil} - defp do_clear_equality_value(other), do: other - - defp do_optional_paths(%{relationships: relationships, requests: requests, ors: ors}) - when relationships == %{} and ors in [[], nil] do - Enum.map(requests, fn request -> - request.path - end) + defp do_filter_expression_by_relationship_path(%Not{expression: expression} = not_expr, path) do + new_expression = do_filter_expression_by_relationship_path(expression, path) + %{not_expr | expression: new_expression} end - defp do_optional_paths(%{ors: [first | rest]} = filter) do - do_optional_paths(first) ++ do_optional_paths(%{filter | ors: rest}) + defp do_filter_expression_by_relationship_path( + %Predicate{relationship_path: predicate_path} = predicate, + path + ) do + if List.starts_with?(predicate_path, path) do + predicate + else + nil + end end - defp do_optional_paths(%{relationships: relationships} = filter) when is_map(relationships) do - relationship_paths = - Enum.flat_map(relationships, fn {_, value} -> - do_optional_paths(value) - end) - - relationship_paths ++ do_optional_paths(%{filter | relationships: %{}}) + defp do_relationship_paths(%Predicate{relationship_path: []}) do + [] end - def request_filter_for_fetch(filter, data) do - filter - |> optional_paths() - |> paths_and_data(data) - |> most_specific_paths() - |> Enum.reduce(filter, fn {path, %{data: related_data}}, filter -> - [:filter, relationship_path] = path - - filter - |> add_records_to_relationship_filter( - relationship_path, - List.wrap(related_data) - ) - |> lift_impossibility() - end) + defp do_relationship_paths(%Predicate{relationship_path: path}) do + {path} end - defp most_specific_paths(paths_and_data) do - Enum.reject(paths_and_data, fn {path, _} -> - Enum.any?(paths_and_data, &path_is_more_specific?(path, &1)) - end) + defp do_relationship_paths(%Expression{left: left, right: right}) do + [do_relationship_paths(left), do_relationship_paths(right)] end - # I don't think this is a possibility - defp path_is_more_specific?([], []), do: false - defp path_is_more_specific?(_, []), do: true - # first element of the search matches first element of candidate - defp path_is_more_specific?([part | rest], [part | candidate_rest]) do - path_is_more_specific?(rest, candidate_rest) - end + defp parse_expression(%__MODULE__{expression: expression}, context), + do: {:ok, add_to_predicate_path(expression, context)} - defp path_is_more_specific?(_, _), do: false + defp parse_expression(statement, context) when is_map(statement) or is_list(statement) do + Enum.reduce_while(statement, {:ok, nil}, fn expression_part, {:ok, expression} -> + case add_expression_part(expression_part, context, expression) do + {:ok, new_expression} -> + {:cont, {:ok, new_expression}} - defp paths_and_data(paths, data) do - Enum.flat_map(paths, fn path -> - case Engine.fetch_nested_value(data, path) do - {:ok, related_data} -> [{path, related_data}] - :error -> [] + {:error, error} -> + {:halt, {:error, error}} end end) end - def empty_filter?(filter) do - filter.attributes == %{} and filter.relationships == %{} and filter.not == nil and - filter.ors in [[], nil] and filter.ands in [[], nil] and filter.impossible? == false + defp parse_expression(statement, context) do + parse_expression([statement], context) end - defp add_records_to_relationship_filter(filter, [], records) do - case PrimaryKeyHelpers.values_to_primary_key_filters(filter.resource, records) do - {:error, error} -> - add_error(filter, error) - - {:ok, []} -> - if filter.ors in [[], nil] do - %{filter | impossible?: true} - else - filter - end - - {:ok, [single]} -> - do_parse(single, filter) - - {:ok, many} -> - do_parse([or: many], filter) - end - end - - defp add_records_to_relationship_filter(filter, [relationship | rest] = path, records) do - filter - |> Map.update!(:relationships, fn relationships -> - case Map.fetch(relationships, relationship) do - {:ok, related_filter} -> - Map.put( - relationships, - relationship, - add_records_to_relationship_filter(related_filter, rest, records) - ) - - :error -> - relationships - end - end) - |> Map.update!(:ors, fn ors -> - Enum.map(ors, &add_records_to_relationship_filter(&1, path, records)) - end) + defp add_expression_part(%__MODULE__{expression: adding_expression}, context, expression) do + {:ok, Expression.new(:and, expression, add_to_predicate_path(adding_expression, context))} end - defp lift_impossibility(filter) do - filter = - filter - |> Map.update!(:relationships, fn relationships -> - Enum.reduce(relationships, relationships, fn {key, filter}, relationships -> - Map.put(relationships, key, lift_impossibility(filter)) - end) - end) - |> Map.update!(:ands, fn ands -> - Enum.map(ands, &lift_impossibility/1) - end) - |> Map.update!(:ors, fn ors -> - Enum.map(ors, &lift_impossibility/1) - end) - - with_related_impossibility = - if Enum.any?(filter.relationships || %{}, fn {_, val} -> Map.get(val, :impossible?) end) do - Map.put(filter, :impossible?, true) - else - filter - end - - if Enum.any?(with_related_impossibility.ands, &Map.get(&1, :impossible?)) do - Map.put(with_related_impossibility, :impossible?, true) + defp add_expression_part(%resource{} = record, context, expression) do + if resource == context.resource do + pkey_filter = record |> Map.take(Ash.primary_key(resource)) |> Map.to_list() + add_expression_part(pkey_filter, context, expression) else - with_related_impossibility + {:error, "Invalid filter value provided: #{inspect(record)}"} end end - defp add_not_filter_info(filter) do - case filter.not do - nil -> - filter + defp add_expression_part({:not, nested_statement}, context, expression) do + case parse_expression(nested_statement, context) do + {:ok, nested_expression} -> + {:ok, Expression.new(:and, expression, Not.new(nested_expression))} - not_filter -> - filter - |> add_request(not_filter.requests) - |> add_error(not_filter.errors) + {:error, error} -> + {:error, error} end end - def predicate_strict_subset_of?(attribute, %left_struct{} = left, right) do - left_struct.strict_subset_of?(attribute, left, right) - end - - def add_to_filter(filter, %__MODULE__{} = addition) do - cond do - empty_filter?(filter) -> - addition - - empty_filter?(addition) -> - filter + defp add_expression_part({op, nested_statements}, context, expression) when op in [:or, :and] do + case parse_and_join(nested_statements, op, context) do + {:ok, nested_expression} -> + {:ok, Expression.new(:and, expression, nested_expression)} - true -> - %{addition | ands: [filter | addition.ands]} - |> lift_impossibility() - |> lift_if_empty() - |> add_not_filter_info() + {:error, error} -> + {:error, error} end end - def add_to_filter(filter, additions) do - parsed = parse(filter.resource, additions, filter.api) - - add_to_filter(filter, parsed) - end - - defp do_parse(filter_statement, %{resource: resource} = filter) do - Enum.reduce(filter_statement, filter, fn - {key, value}, filter -> - cond do - key == :__impossible__ && value == true -> - %{filter | impossible?: true} - - key == :and -> - add_and_to_filter(filter, value) - - key == :or -> - add_or_to_filter(filter, value) + defp add_expression_part({field, nested_statement}, context, expression) + when is_atom(field) or is_binary(field) do + cond do + attr = Ash.attribute(context.resource, field) -> + case parse_predicates(nested_statement, attr, context) do + {:ok, nested_statement} -> + {:ok, Expression.new(:and, expression, nested_statement)} - key == :not -> - add_to_not_filter(filter, value) + {:error, error} -> + {:error, error} + end - attr = Ash.attribute(resource, key) -> - add_attribute_filter(filter, attr, value) + rel = Ash.relationship(context.resource, field) -> + context = + context + |> Map.update!(:relationship_path, fn path -> path ++ [rel.name] end) + |> Map.put(:resource, rel.destination) - rel = Ash.relationship(resource, key) -> - add_relationship_filter(filter, rel, value) + if is_list(nested_statement) || is_map(nested_statement) do + case parse_expression(nested_statement, context) do + {:ok, nested_expression} -> + {:ok, Expression.new(:and, expression, nested_expression)} - true -> - add_error( - filter, - "Attempted to filter on #{key} which is neither a relationship, nor a field of #{ - inspect(resource) - }" - ) + {:error, error} -> + {:error, error} + end + else + with [field] <- Ash.primary_key(context.resource), + attribute <- Ash.attribute(context.resource, field), + {:ok, casted} <- Ash.Type.cast_input(attribute.type, nested_statement) do + add_expression_part({field, casted}, context, expression) + else + _other -> + {:error, "Invalid filter value provided: #{inspect(nested_statement)}"} + end end - end) - |> lift_impossibility() - |> lift_if_empty() - |> add_not_filter_info() - end - defp add_and_to_filter(filter, value) do - if Keyword.keyword?(value) do - %{filter | ands: [parse(filter.resource, value, filter.api) | filter.ands]} - else - empty_filter = parse(filter.resource, [], filter.api) - - filter_with_ands = %{ - empty_filter - | ands: Enum.map(value, &parse(filter.resource, &1, filter.api)) - } - - %{filter | ands: [filter_with_ands | filter.ands]} + true -> + {:error, "No such attribute or relationship #{field} on #{inspect(context.resource)}"} end end - defp add_or_to_filter(filter, value) do - if Keyword.keyword?(value) do - %{filter | ors: [parse(filter.resource, value, filter.api) | filter.ors]} - else - [first_or | rest_ors] = Enum.map(value, &parse(filter.resource, &1, filter.api)) - - or_filter = - filter.resource - |> parse(first_or, filter.api) - |> Map.update!(:ors, &Kernel.++(&1, rest_ors)) + defp add_expression_part(value, context, expression) when is_map(value) do + # Can't call `parse_expression/2` here because it will loop - %{filter | ands: [or_filter | filter.ands]} - end - end + value + |> Map.to_list() + |> Enum.reduce_while({:ok, nil}, fn {key, value}, {:ok, expression} -> + case add_expression_part({key, value}, context, expression) do + {:ok, new_expression} -> + {:cont, {:ok, new_expression}} - defp add_to_not_filter(filter, value) do - Map.update!(filter, :not, fn not_filter -> - if not_filter do - add_to_filter(not_filter, value) - else - parse(filter.resource, value, filter.api) + {:error, error} -> + {:halt, {:error, error}} end end) + |> case do + {:ok, new_expression} -> {:ok, Expression.new(:and, expression, new_expression)} + {:error, error} -> {:error, error} + end end - defp lift_if_empty(%{ - ors: [], - ands: [and_filter | rest], - attributes: attrs, - relationships: rels, - not: nil, - errors: errors - }) - when attrs == %{} and rels == %{} do - and_filter - |> Map.update!(:ands, &Kernel.++(&1, rest)) - |> lift_if_empty() - |> Map.update!(:errors, &Kernel.++(&1, errors)) - end - - defp lift_if_empty(%{ - ands: [], - ors: [or_filter | rest], - attributes: attrs, - relationships: rels, - not: nil, - errors: errors - }) - when attrs == %{} and rels == %{} do - or_filter - |> Map.update!(:ors, &Kernel.++(&1, rest)) - |> lift_if_empty() - |> Map.update!(:errors, &Kernel.++(&1, errors)) - end - - defp lift_if_empty(filter) do - filter - end - - defp add_attribute_filter(filter, attr, value) do - if Keyword.keyword?(value) do - Enum.reduce(value, filter, fn - {predicate_name, value}, filter -> - do_add_attribute_filter(filter, attr, predicate_name, value) - end) - else - add_attribute_filter(filter, attr, eq: value) - end + defp add_expression_part(value, _, _) do + {:error, "Invalid filter value provided: #{inspect(value)}"} end - defp do_add_attribute_filter( - %{attributes: attributes, resource: resource} = filter, - %{type: attr_type, name: attr_name}, - predicate_name, - value - ) do - case parse_predicate(resource, predicate_name, attr_name, attr_type, value) do - {:ok, predicate} -> - new_attributes = - Map.update( - attributes, - attr_name, - predicate, - &Merge.merge(&1, predicate) - ) + defp add_to_predicate_path(expression, context) do + case expression do + %Not{expression: expression} = not_expr -> + %{not_expr | expression: add_to_predicate_path(expression, context)} - %{filter | attributes: new_attributes} + %Expression{left: left, right: right} = expression -> + %{ + expression + | left: add_to_predicate_path(left, context), + right: add_to_predicate_path(right, context) + } - {:error, error} -> - add_error(filter, error) + %Predicate{relationship_path: relationship_path} = pred -> + %{pred | relationship_path: context.relationship_path ++ relationship_path} end end - def parse_predicates(resource, keyword, attr_name, attr_type) do - Enum.reduce(keyword, {:ok, nil}, fn {predicate_name, value}, {:ok, existing_predicate} -> - case parse_predicate(resource, predicate_name, attr_name, attr_type, value) do - {:ok, predicate} when is_nil(existing_predicate) -> - {:ok, predicate} - - {:ok, predicate} -> - {:ok, Merge.merge(existing_predicate, predicate)} + defp parse_and_join(statements, op, context) do + Enum.reduce_while(statements, {:ok, nil}, fn statement, {:ok, expression} -> + case parse_expression(statement, context) do + {:ok, nested_expression} -> + {:cont, {:ok, Expression.new(op, expression, nested_expression)}} {:error, error} -> - {:error, error} + {:halt, {:error, error}} end end) end - def count_of_clauses(nil), do: 0 - - def count_of_clauses(filter) do - relationship_clauses = - filter.relationships - |> Map.values() - |> Enum.map(fn related_filter -> - 1 + count_of_clauses(related_filter) - end) - |> Enum.sum() - - or_clauses = - filter.ors - |> Kernel.||([]) - |> Enum.map(&count_of_clauses/1) - |> Enum.sum() - - not_clauses = count_of_clauses(filter.not) - - and_clauses = - filter.ands - |> Enum.map(&count_of_clauses/1) - |> Enum.sum() - - Enum.count(filter.attributes) + relationship_clauses + or_clauses + not_clauses + and_clauses + defp parse_predicates(value, field, context) when not is_list(value) do + parse_predicates([eq: value], field, context) end - defp parse_predicate(resource, predicate_name, attr_name, attr_type, value) do - data_layer = Ash.data_layer(resource) - + defp parse_predicates(values, attr, context) do data_layer_predicates = - Map.get(Ash.data_layer_filters(resource), Ash.Type.storage_type(attr_type), []) - - all_predicates = - Enum.reduce(data_layer_predicates, @predicates, fn {name, module}, all_predicates -> - Map.put(all_predicates, name, module) - end) - - with {:predicate_type, {:ok, predicate_type}} <- - {:predicate_type, Map.fetch(all_predicates, predicate_name)}, - {:type_can?, _, true} <- - {:type_can?, predicate_name, - Keyword.has_key?(data_layer_predicates, predicate_name) or - Ash.Type.supports_filter?(resource, attr_type, predicate_name, data_layer)}, - {:data_layer_can?, _, true} <- - {:data_layer_can?, predicate_name, - Ash.data_layer_can?(resource, {:filter, predicate_name})}, - {:predicate, _, {:ok, predicate}} <- - {:predicate, attr_name, predicate_type.new(resource, attr_name, attr_type, value)} do - {:ok, predicate} - else - {:predicate_type, :error} -> - {:error, :predicate_type, "No such filter type #{predicate_name}"} - - {:predicate, attr_name, {:error, error}} -> - {:error, Map.put(error, :field, attr_name)} - - {:type_can?, predicate_name, false} -> - {:error, - "Cannot use filter type #{inspect(predicate_name)} on type #{inspect(attr_type)}."} - - {:data_layer_can?, predicate_name, false} -> - {:error, "data layer not capable of provided filter: #{predicate_name}"} - end - end - - defp add_relationship_filter( - %{relationships: relationships} = filter, - %{destination: destination, name: name} = relationship, - value - ) do - case parse_relationship_filter(value, relationship) do - {:ok, provided_filter} -> - related_filter = parse(destination, provided_filter, filter.api, [name | filter.path]) - - new_relationships = - Map.update(relationships, name, related_filter, &Merge.merge(&1, related_filter)) - - filter - |> Map.put(:relationships, new_relationships) - |> add_relationship_compatibility_error(relationship) - |> add_error(related_filter.errors) - |> add_request(related_filter.requests) - - {:error, error} -> - add_error(filter, error) - end - end - - defp parse_relationship_filter(value, %{destination: destination} = relationship) do - cond do - match?(%__MODULE__{}, value) -> - {:ok, value} - - match?(%^destination{}, value) -> - PrimaryKeyHelpers.value_to_primary_key_filter(destination, value) - - is_map(value) -> - {:ok, Map.to_list(value)} - - Keyword.keyword?(value) -> - {:ok, value} - - is_list(value) -> - parse_relationship_list_filter(value, relationship) - - true -> - PrimaryKeyHelpers.value_to_primary_key_filter(destination, value) - end - end - - defp parse_relationship_list_filter(value, relationship) do - Enum.reduce_while(value, {:ok, []}, fn item, items -> - case parse_relationship_filter(item, relationship) do - {:ok, item_filter} -> {:cont, {:ok, [item_filter | items]}} - {:error, error} -> {:halt, {:error, error}} - end - end) - end - - defp add_relationship_compatibility_error(%{resource: resource} = filter, %{ - cardinality: cardinality, - destination: destination, - name: name - }) do - cond do - not Ash.data_layer_can?(resource, {:filter_related, cardinality}) -> - add_error( - filter, - "Cannot filter on relationship #{name}: #{inspect(Ash.data_layer(resource))} does not support it." - ) - - not (Ash.data_layer(destination) == Ash.data_layer(resource)) -> - add_error( - filter, - "Cannot filter on related entites unless they share a data layer, for now." - ) - - true -> - filter - end - end - - defp add_request(filter, requests) - when is_list(requests), - do: %{filter | requests: filter.requests ++ requests} - - defp add_request(%{requests: requests} = filter, request), - do: %{filter | requests: [request | requests]} - - defp add_error(%{errors: errors} = filter, errors) when is_list(errors), - do: %{filter | errors: filter.errors ++ errors} - - defp add_error(%{errors: errors} = filter, error), do: %{filter | errors: [error | errors]} -end - -defimpl Inspect, for: Ash.Filter do - import Inspect.Algebra - import Ash.Filter.InspectHelpers - - defguardp is_empty(val) when is_nil(val) or val == [] or val == %{} - - def inspect( - %Ash.Filter{ - not: not_filter, - ors: ors, - relationships: relationships, - attributes: attributes, - ands: ands - }, - opts + Map.get( + Ash.data_layer_filters(context.resource), + Ash.Type.storage_type(attr.type), + [] ) - when not is_nil(not_filter) and is_empty(ors) and is_empty(relationships) and - is_empty(attributes) and is_empty(ands) do - if root?(opts) do - concat(["#Filter"]) - else - concat(["not ", to_doc(not_filter, make_non_root(opts))]) - end - end - - def inspect(%Ash.Filter{not: not_filter} = filter, opts) when not is_nil(not_filter) do - if root?(opts) do - concat([ - "#Filter" - ]) - else - concat([ - "not ", - to_doc(not_filter, make_non_root(opts)), - " and ", - to_doc(%{filter | not: nil}, make_non_root(opts)) - ]) - end - end - def inspect( - %Ash.Filter{ors: ors, relationships: relationships, attributes: attributes, ands: ands}, - opts - ) - when is_empty(ors) and is_empty(relationships) and is_empty(attributes) and is_empty(ands) do - if root?(opts) do - concat(["#Filter<", to_doc(nil, opts), ">"]) + if Keyword.keyword?(values) do + Enum.reduce_while(values, {:ok, nil}, fn {key, value}, {:ok, expression} -> + case @built_in_predicates[key] || data_layer_predicates[key] do + value when value in [nil, []] -> + {:halt, {:error, "No such filter predicate: #{inspect(key)}"}} + + predicate_module -> + case Predicate.new( + context.resource, + attr, + predicate_module, + value, + context.relationship_path + ) do + {:ok, predicate} -> + {:cont, {:ok, Expression.new(:and, expression, predicate)}} + + {:error, error} -> + {:halt, {:error, error}} + end + end + end) else - to_doc(nil, opts) + {:halt, {:error, "Invalid filter expression: #{inspect(values)}"}} end end - def inspect(filter, opts) do - rels = parse_relationships(filter, opts) - attrs = parse_attributes(filter, opts) - - and_container = - case attrs ++ rels do - [] -> - empty() - - [and_clause] -> - and_clause - - and_clauses -> - Inspect.Algebra.container_doc("(", and_clauses, ")", opts, fn term, _ -> term end, - break: :flex, - separator: " and" - ) - end - - with_or_container = - case Map.get(filter, :ors) do - nil -> - and_container - - [] -> - and_container + defimpl Inspect do + import Inspect.Algebra - ors -> - inspected_ors = Enum.map(ors, fn filter -> to_doc(filter, make_non_root(opts)) end) + @custom_colors [ + number: :cyan + ] - or_container = - Inspect.Algebra.container_doc( - "(", - inspected_ors, - ")", - opts, - fn term, _ -> term end, - break: :strict, - separator: " or " - ) - - if Enum.empty?(attrs) && Enum.empty?(rels) do - or_container - else - concat(["(", and_container, " or ", or_container, ")"]) - end - end - - all_container = - case filter.ands do - [] -> - with_or_container - - ands -> - docs = [with_or_container | Enum.map(ands, &Inspect.inspect(&1, make_non_root(opts)))] - - Inspect.Algebra.container_doc( - "(", - docs, - ")", - opts, - fn term, _ -> term end, - break: :strict, - separator: " and " - ) - end - - if root?(opts) do - concat(["#Filter<", all_container, ">"]) - else - all_container + def inspect( + %{expression: expression}, + opts + ) do + opts = %{opts | syntax_colors: Keyword.merge(opts.syntax_colors, @custom_colors)} + concat(["#Ash.Filter<", to_doc(expression, opts), ">"]) end end - - defp parse_relationships(%Ash.Filter{relationships: relationships}, _opts) - when relationships == %{}, - do: [] - - defp parse_relationships(filter, opts) do - filter - |> Map.fetch!(:relationships) - |> Enum.map(fn {key, value} -> to_doc(value, add_to_path(opts, key)) end) - end - - defp parse_attributes(%Ash.Filter{attributes: attributes}, _opts) when attributes == %{}, do: [] - - defp parse_attributes(filter, opts) do - filter - |> Map.fetch!(:attributes) - |> Enum.map(fn {key, value} -> to_doc(value, put_attr(opts, key)) end) - end end diff --git a/lib/ash/filter2/not.ex b/lib/ash/filter/not.ex similarity index 83% rename from lib/ash/filter2/not.ex rename to lib/ash/filter/not.ex index 7422fbed5..36d16ddc1 100644 --- a/lib/ash/filter2/not.ex +++ b/lib/ash/filter/not.ex @@ -1,7 +1,9 @@ -defmodule Ash.Filter2.Not do +defmodule Ash.Filter.Not do defstruct [:expression] - alias Ash.Filter2.Expression + alias Ash.Filter.Expression + + def new(nil), do: nil def new(expression) do %__MODULE__{expression: expression} diff --git a/lib/ash/filter/predicate.ex b/lib/ash/filter/predicate.ex new file mode 100644 index 000000000..56c0e58ae --- /dev/null +++ b/lib/ash/filter/predicate.ex @@ -0,0 +1,91 @@ +defmodule Ash.Filter.Predicate do + defstruct [:attribute, :relationship_path, :predicate] + + alias Ash.Filter.Predicate.Comparison + + @type predicate :: struct + + @type comparison :: + :exclusive + | :inclusive + | :equal + | {:simplify, Predicate.predicate()} + | {:simplify, Predicate.predicate(), Predicate.predicate()} + + @type t :: %__MODULE__{ + attribute: Ash.attribute(), + relationship_path: list(atom), + predicate: predicate + } + + @callback new(Ash.resource(), Ash.attribute(), term) :: {:ok, struct} | {:error, term} + @callback compare(predicate(), predicate()) :: Comparison.comparison() + + defmacro __using__(_opts) do + quote do + @behaviour Ash.Filter.Predicate + + @spec compare(Ash.Filter.Predicate.predicate(), Ash.Filter.Predicate.predicate()) :: + Ash.Filter.Predicate.comparison() | :unknown + def compare(_, _), do: :unknown + + defoverridable compare: 2 + end + end + + @spec compare(predicate(), predicate()) :: comparison() + def compare(left, right) do + with :unknown <- left.__struct__.compare(right), + :unknown <- right.__struct__.compare(left) do + :exclusive + end + end + + def new(resource, attribute, predicate, value, relationship_path) do + case predicate.new(resource, attribute, value) do + {:ok, predicate} -> + {:ok, + %__MODULE__{ + attribute: attribute, + predicate: predicate, + relationship_path: relationship_path + }} + + {:error, error} -> + {:error, error} + end + end + + def add_inspect_path(inspect_opts, field) do + case inspect_opts.custom_options[:relationship_path] do + empty when empty in [nil, []] -> to_string(field) + path -> Enum.join(path, ".") <> "." <> to_string(field) + end + end + + defimpl Inspect do + import Inspect.Algebra + + def inspect( + %{relationship_path: relationship_path, predicate: predicate}, + opts + ) do + opts = %{ + opts + | syntax_colors: [ + atom: :yellow, + binary: :green, + boolean: :pink, + list: :orange, + map: :magenta, + number: :red, + regex: :violet, + tuple: :white + ], + custom_options: Keyword.put(opts.custom_options, :relationship_path, relationship_path) + } + + to_doc(predicate, opts) + end + end +end diff --git a/lib/ash/filter/predicate/eq.ex b/lib/ash/filter/predicate/eq.ex new file mode 100644 index 000000000..149b50e46 --- /dev/null +++ b/lib/ash/filter/predicate/eq.ex @@ -0,0 +1,37 @@ +defmodule Ash.Filter.Predicate.Eq do + @moduledoc false + defstruct [:field, :value] + + use Ash.Filter.Predicate + + alias Ash.Error.Filter.InvalidFilterValue + + def new(_resource, attribute, value) do + case Ash.Type.cast_input(attribute.type, value) do + {:ok, value} -> + {:ok, %__MODULE__{field: attribute.name, value: value}} + + :error -> + {:error, + InvalidFilterValue.exception( + filter: %__MODULE__{field: attribute.name, value: value}, + value: value, + field: attribute.name + )} + end + end + + def compare(%__MODULE__{value: value}, %__MODULE__{value: value}), do: :equal + + defimpl Inspect do + import Inspect.Algebra + + def inspect(predicate, opts) do + concat([ + Ash.Filter.Predicate.add_inspect_path(opts, predicate.field), + " == ", + to_doc(predicate.value, opts) + ]) + end + end +end diff --git a/lib/ash/filter2/predicate/in.ex b/lib/ash/filter/predicate/in.ex similarity index 56% rename from lib/ash/filter2/predicate/in.ex rename to lib/ash/filter/predicate/in.ex index 2bb6e9ee5..6a42408e9 100644 --- a/lib/ash/filter2/predicate/in.ex +++ b/lib/ash/filter/predicate/in.ex @@ -1,29 +1,42 @@ -defmodule Ash.Filter2.Predicate.In do +defmodule Ash.Filter.Predicate.In do @moduledoc false - defstruct [:values] - - alias Ash.Filter2.Predicate.Eq - - # alias Ash.Filter2.Predicate.NotEq - # alias Ash.Filter2.Predicate.NotIn - - use Ash.Filter2.Predicate - - def new(_type, []), do: {:ok, %__MODULE__{values: MapSet.new([])}} - - def new(type, [value]), do: {:ok, Eq.new(type, value)} - - def new(type, values) when is_list(values) do - values = MapSet.new(values) - - case MapSet.size(values) do - 1 -> {:ok, Eq.new(type, Enum.at(values, 0))} - _ -> {:ok, %__MODULE__{values: values}} - end + defstruct [:field, :values] + + alias Ash.Error.Filter.InvalidFilterValue + alias Ash.Filter.Predicate.Eq + + use Ash.Filter.Predicate + + def new(_resource, attribute, []), + do: {:ok, %__MODULE__{field: attribute.name, values: MapSet.new([])}} + + def new(resource, attribute, [value]), do: {:ok, Eq.new(resource, attribute, value)} + + def new(_resource, attribute, values) when is_list(values) do + Enum.reduce_while(values, {:ok, %__MODULE__{field: attribute.name, values: []}}, fn value, + predicate -> + case Ash.Type.cast_input(attribute.type, value) do + {:ok, casted} -> + {:cont, {:ok, %{predicate | values: [casted | predicate.values]}}} + + :error -> + {:error, + InvalidFilterValue.exception( + filter: %__MODULE__{field: attribute.name, values: values}, + value: value, + field: attribute.name + )} + end + end) end - def new(_type, values) do - {:error, "Invalid filter value provided for `in`: #{inspect(values)}"} + def new(_resource, attribute, values) do + {:error, + InvalidFilterValue.exception( + filter: %__MODULE__{field: attribute.name, values: values}, + value: values, + field: attribute.name + )} end # def compare(%__MODULE__{values: values}, %__MODULE__{values: values}), do: :equal @@ -33,11 +46,16 @@ defmodule Ash.Filter2.Predicate.In do # def compare(%__MODULE__{value: value}, %NotEq{value: other_value}) when value != other_value, # do: :inclusive - - def inspect(field, %{values: values}, opts) do + defimpl Inspect do import Inspect.Algebra - concat([field, " in ", to_doc(Enum.to_list(values), opts)]) + def inspect(predicate, opts) do + concat([ + Ash.Filter.Predicate.add_inspect_path(opts, predicate.field), + " in ", + to_doc(predicate.values, opts) + ]) + end end end diff --git a/lib/ash/filter2/filter2.ex b/lib/ash/filter2/filter2.ex deleted file mode 100644 index d87769e35..000000000 --- a/lib/ash/filter2/filter2.ex +++ /dev/null @@ -1,152 +0,0 @@ -defmodule Ash.Filter2 do - alias Ash.Filter2.Predicate - alias Ash.Filter2.Predicate.{Eq, In} - alias Ash.Filter2.Expression - alias Ash.Filter2.Not - - @built_in_predicates [ - eq: Eq, - in: In - ] - - defstruct [:resource, :api, :expression] - - def parse!(api, resource, statement) do - case parse(api, resource, statement) do - {:ok, filter} -> - filter - - {:error, error} -> - raise error - end - end - - def parse(api, resource, statement) do - context = %{ - resource: resource, - api: api, - relationship_path: [] - } - - case parse_expression(statement, context) do - {:ok, expression} -> - {:ok, %__MODULE__{expression: expression, resource: resource, api: api}} - - {:error, error} -> - error = Ash.Error.to_ash_error(error) - {:error, error} - end - end - - def parse_expression(statement, context) do - statement - |> List.wrap() - |> Enum.reduce_while({:ok, nil}, fn expression_part, {:ok, expression} -> - case expression_part do - {:not, nested_statement} -> - case parse_expression(nested_statement, context) do - {:ok, nested_expression} -> - {:cont, {:ok, Expression.new(:and, expression, Not.new(nested_expression))}} - - {:error, error} -> - {:halt, {:error, error}} - end - - {op, nested_statements} when op in [:or, :and] -> - case parse_and_join(nested_statements, op, context) do - {:ok, nested_expression} -> - {:cont, {:ok, Expression.new(:and, expression, nested_expression)}} - - {:error, error} -> - {:halt, {:error, error}} - end - - {field, nested_statement} when is_atom(field) or is_binary(field) -> - cond do - attr = Ash.attribute(context.resource, field) -> - case parse_predicates(nested_statement, attr, context) do - {:ok, nested_statement} -> - {:cont, {:ok, Expression.new(:and, expression, nested_statement)}} - - {:error, error} -> - {:halt, {:error, error}} - end - - rel = Ash.relationship(context.resource, field) -> - context = - context - |> Map.update!(:relationship_path, fn path -> path ++ [rel.name] end) - |> Map.put(:resource, rel.destination) - - case parse_expression(nested_statement, context) do - {:ok, nested_statement} -> - {:cont, {:ok, Expression.new(:and, expression, nested_statement)}} - - {:error, error} -> - {:halt, {:error, error}} - end - - true -> - {:halt, - {:error, - "No such attribute or relationship #{field} on #{inspect(context.resource)}"}} - end - end - end) - end - - defp parse_and_join(statements, op, context) do - Enum.reduce_while(statements, {:ok, nil}, fn statement, {:ok, expression} -> - case parse_expression(statement, context) do - {:ok, nested_expression} -> - {:cont, {:ok, Expression.new(op, expression, nested_expression)}} - - {:error, error} -> - {:halt, {:error, error}} - end - end) - end - - defp parse_predicates(value, field, context) when not is_list(value) do - parse_predicates([eq: value], field, context) - end - - defp parse_predicates(values, attr, context) do - if Keyword.keyword?(values) do - Enum.reduce_while(values, {:ok, nil}, fn {key, value}, {:ok, expression} -> - case @built_in_predicates[key] do - nil -> - {:halt, {:error, "No such filter predicate: #{inspect(key)}"}} - - predicate_module -> - case Predicate.new( - context.resource, - attr, - predicate_module, - value, - context.relationship_path - ) do - {:ok, predicate} -> - {:cont, {:ok, Expression.new(:and, expression, predicate)}} - - {:error, error} -> - {:halt, {:error, error}} - end - end - end) - else - {:halt, {:error, "Invalid filter expression: #{inspect(values)}"}} - end - end - - defimpl Inspect do - import Inspect.Algebra - - def inspect( - %{expression: expression}, - opts - ) do - concat(["#Ash.Filter<", to_doc(expression, opts), ">"]) - end - end -end diff --git a/lib/ash/filter2/predicate.ex b/lib/ash/filter2/predicate.ex deleted file mode 100644 index 00782a6a3..000000000 --- a/lib/ash/filter2/predicate.ex +++ /dev/null @@ -1,76 +0,0 @@ -defmodule Ash.Filter2.Predicate do - defstruct [:resource, :attribute, :relationship_path, :predicate] - - alias Ash.Filter2.Predicate.Comparison - - @type predicate :: struct - - @type comparison :: - :exclusive - | :inclusive - | :equal - | {:simplify, Predicate.predicate()} - | {:simplify, Predicate.predicate(), Predicate.predicate()} - - @type t :: %__MODULE__{ - resource: Ash.resource(), - attribute: Ash.attribute(), - relationship_path: list(atom), - predicate: predicate - } - - @callback new(Ash.type(), term) :: {:ok, struct} | {:error, term} - @callback inspect(String.t(), predicate(), Inspect.Opts.t()) :: Inspect.Algebra.t() - @callback compare(predicate(), predicate()) :: Comparison.comparison() - - defmacro __using__(_opts) do - quote do - @behaviour Ash.Filter2.Predicate - - @spec compare(Ash.Filter2.Predicate.predicate(), Ash.Filter2.Predicate.predicate()) :: - Ash.Filter.Predicate.comparison() | :unknown - def compare(_, _), do: :unknown - - defoverridable compare: 2 - end - end - - @spec compare(predicate(), predicate()) :: comparison() - def compare(left, right) do - with :unknown <- left.__struct__.compare(right), - :unknown <- right.__struct__.compare(left) do - :exclusive - end - end - - def new(resource, attribute, predicate, value, relationship_path) do - case predicate.new(attribute.type, value) do - {:ok, predicate} -> - {:ok, - %__MODULE__{ - resource: resource, - attribute: attribute, - predicate: predicate, - relationship_path: relationship_path - }} - - {:error, error} -> - {:error, error} - end - end -end - -defimpl Inspect, for: Ash.Filter2.Predicate do - def inspect( - %{attribute: attribute, relationship_path: relationship_path, predicate: predicate}, - opts - ) do - field = - case relationship_path do - [] -> to_string(attribute.name) - path -> Enum.join(path, ".") <> "." <> to_string(attribute.name) - end - - predicate.__struct__.inspect(field, predicate, opts) - end -end diff --git a/lib/ash/filter2/predicate/eq.ex b/lib/ash/filter2/predicate/eq.ex deleted file mode 100644 index 2db6a031e..000000000 --- a/lib/ash/filter2/predicate/eq.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Ash.Filter2.Predicate.Eq do - @moduledoc false - defstruct [:value] - - use Ash.Filter2.Predicate - - def new(_type, value) do - {:ok, %__MODULE__{value: value}} - end - - def compare(%__MODULE__{value: value}, %__MODULE__{value: value}), do: :equal - - def inspect(field, %{value: value}, opts) do - import Inspect.Algebra - - concat([field, " == ", to_doc(value, opts)]) - end -end diff --git a/lib/ash/query.ex b/lib/ash/query.ex index 1961f6e21..48be32543 100644 --- a/lib/ash/query.ex +++ b/lib/ash/query.ex @@ -59,7 +59,7 @@ defmodule Ash.Query do {:ok, resource} -> %__MODULE__{ api: api, - filter: Ash.Filter.parse(resource, [], api), + filter: nil, resource: resource } |> set_data_layer_query() @@ -67,7 +67,7 @@ defmodule Ash.Query do :error -> %__MODULE__{ api: api, - filter: Ash.Filter.parse(resource, [], api), + filter: nil, resource: resource } |> add_error(:resource, "does not exist") @@ -230,16 +230,19 @@ defmodule Ash.Query do new_filter = case query.filter do nil -> - filter + {:ok, filter} existing_filter -> Ash.Filter.add_to_filter(existing_filter, filter) end - new_filter.errors - |> Enum.reduce(query, &add_error(&2, :filter, &1)) - |> Map.put(:filter, new_filter) - |> set_data_layer_query() + case new_filter do + {:ok, filter} -> + set_data_layer_query(%{query | filter: filter}) + + {:error, error} -> + add_error(query, :filter, error) + end end def filter(query, statement) do @@ -247,46 +250,17 @@ defmodule Ash.Query do if query.filter do Ash.Filter.add_to_filter(query.filter, statement) else - Ash.Filter.parse(query.resource, statement, query.api) + Ash.Filter.parse(query.api, query.resource, statement) end - filter.errors - |> Enum.reduce(query, &add_error(&2, :filter, &1)) - |> Map.put(:filter, filter) - |> set_data_layer_query() - end - - def reject(query, statement) when is_list(statement) do - filter(query, not: statement) - end - - def reject(query, %Ash.Filter{} = filter) do - case query.filter do - nil -> - new_filter = - query.resource - |> Ash.Filter.parse([], query.api) - |> Map.put(:not, filter) - + case filter do + {:ok, filter} -> query - |> Map.put(:filter, new_filter) + |> Map.put(:filter, filter) |> set_data_layer_query() - existing_filter -> - new_filter_not = - case existing_filter.not do - nil -> - filter - - existing_not_filter -> - %{existing_not_filter | ands: [filter | existing_not_filter.ands]} - end - - new_filter = %{existing_filter | not: new_filter_not} - - query - |> Map.put(:filter, new_filter) - |> set_data_layer_query() + {:error, error} -> + add_error(query, :filter, error) end end @@ -349,6 +323,10 @@ defmodule Ash.Query do end end + defp maybe_filter(query, %{filter: nil}, _) do + {:ok, query} + end + defp maybe_filter(query, ash_query, opts) do case Ash.DataLayer.filter(query, ash_query.filter, ash_query.resource) do {:ok, filtered} -> diff --git a/lib/sat_solver.ex b/lib/sat_solver.ex index e418662ae..59dd628c1 100644 --- a/lib/sat_solver.ex +++ b/lib/sat_solver.ex @@ -1,98 +1,50 @@ defmodule Ash.SatSolver do @moduledoc false + alias Ash.Filter + alias Ash.Filter.{Expression, Not, Predicate} + def strict_filter_subset(filter, candidate) do filter_expr = filter_to_expr(filter) candidate_expr = filter_to_expr(candidate) - together = join_expr(filter_expr, candidate_expr, :and) + case {filter_expr, candidate_expr} do + {nil, nil} -> + true - separate = join_expr(negate(filter_expr), candidate_expr, :and) + {nil, _candidate_expr} -> + true - case solve_expression(together) do - {:error, :unsatisfiable} -> + {_filter_expr, nil} -> + # TODO: truesim check `filter_expr` false - {:ok, _} -> - case solve_expression(separate) do + {filter_expr, candidate_expr} -> + case solve_expression({:and, filter_expr, candidate_expr}) do {:error, :unsatisfiable} -> - true + false + + {:ok, _} -> + case solve_expression({:and, {:not, filter_expr}, candidate_expr}) do + {:error, :unsatisfiable} -> + true - _ -> - :maybe + _ -> + :maby + end end end end - defp negate(nil), do: nil - defp negate(expr), do: {:not, expr} - defp filter_to_expr(nil), do: nil - defp filter_to_expr(%{impossible?: true}), do: false - - defp filter_to_expr(%{ - attributes: attributes, - relationships: relationships, - not: not_filter, - ors: ors, - ands: ands, - path: path - }) do - expr = - Enum.reduce(attributes, nil, fn {attr, statement}, expr -> - join_expr( - expr, - tag_statement(statement_to_expr(statement), %{path: path, attr: attr}), - :and - ) - end) - - expr = - Enum.reduce(relationships, expr, fn {relationship, relationship_filter}, expr -> - join_expr(expr, {relationship, filter_to_expr(relationship_filter)}, :and) - end) - - expr = join_expr(negate(filter_to_expr(not_filter)), expr, :and) - - expr = - Enum.reduce(ors, expr, fn or_filter, expr -> - join_expr(filter_to_expr(or_filter), expr, :or) - end) - - Enum.reduce(ands, expr, fn and_filter, expr -> - join_expr(filter_to_expr(and_filter), expr, :and) - end) - end + defp filter_to_expr(%Filter{expression: expression}), do: filter_to_expr(expression) + defp filter_to_expr(%Predicate{} = predicate), do: predicate + defp filter_to_expr(%Not{expression: expression}), do: {:not, expression} - defp statement_to_expr(%Ash.Filter.NotIn{values: values}) do - {:not, %Ash.Filter.In{values: values}} + defp filter_to_expr(%Expression{op: op, left: left, right: right}) do + {op, filter_to_expr(left), filter_to_expr(right)} end - defp statement_to_expr(%Ash.Filter.NotEq{value: value}) do - {:not, %Ash.Filter.Eq{value: value}} - end - - defp statement_to_expr(%Ash.Filter.And{left: left, right: right}) do - {:and, statement_to_expr(left), statement_to_expr(right)} - end - - defp statement_to_expr(%Ash.Filter.Or{left: left, right: right}) do - {:or, statement_to_expr(left), statement_to_expr(right)} - end - - defp statement_to_expr(statement), do: statement - - defp tag_statement({:not, value}, tag), do: {:not, tag_statement(value, tag)} - - defp tag_statement({joiner, left_value, right_value}, tag) when joiner in [:and, :or], - do: {joiner, tag_statement(left_value, tag), tag_statement(right_value, tag)} - - defp tag_statement(statement, tag), do: {statement, tag} - - defp join_expr(nil, right, _joiner), do: right - defp join_expr(left, nil, _joiner), do: left - defp join_expr(left, right, joiner), do: {joiner, left, right} - def solve_expression(expression) do expression_with_constants = {:and, true, {:and, {:not, false}, expression}} diff --git a/test/actions/read_test.exs b/test/actions/read_test.exs index 0815f8795..2db6509ae 100644 --- a/test/actions/read_test.exs +++ b/test/actions/read_test.exs @@ -153,12 +153,16 @@ defmodule Ash.Test.Actions.ReadTest do end test "it raises on an error" do - assert_raise(Ash.Error.Invalid, ~r/Invalid filter value 10 supplied for :title == 10/, fn -> - Post - |> Api.query() - |> Ash.Query.filter(title: 10) - |> Api.read!() - end) + assert_raise( + Ash.Error.Invalid, + ~r/Invalid filter value `10` supplied in: `title == 10`/, + fn -> + Post + |> Api.query() + |> Ash.Query.filter(title: 10) + |> Api.read!() + end + ) end end