defmodule Etso.ETS.MatchSpecification do @moduledoc """ The ETS Match Specifications module contains various functions which convert Ecto queries to [ETS Match Specifications](https://www.erlang.org/doc/apps/erts/match_spec.html) in order to execute the given queries with ETS with as much pushed down to ETS as possible. The basic shape of the match head is `[$1, $2, $3, …]` where each field is a named variable, the ordering of the fields is determined by `Etso.ETS.TableStructure`. Conditions are compiled according to the wheres in the underlying Ecto query, while the body is compiled based on the selected fields in the underlying Ecto query. """ def build(query, params) do {_, schema} = query.from.source field_names = Etso.ETS.TableStructure.field_names(schema) match_head = build_head(field_names) match_conditions = build_conditions(field_names, params, query) match_body = [build_body(field_names, query)] {match_head, match_conditions, match_body} end defp build_head(field_names) do List.to_tuple(Enum.map(1..length(field_names), fn x -> :"$#{x}" end)) end defp build_conditions(field_names, params, %Ecto.Query{wheres: wheres}) do Enum.reduce(wheres, [], fn %Ecto.Query.BooleanExpr{expr: expression}, acc -> [build_condition(field_names, params, expression) | acc] end) end defmacrop guard_operator(:and), do: :andalso defmacrop guard_operator(:or), do: :orelse defmacrop guard_operator(:!=), do: :"/=" defmacrop guard_operator(:<=), do: :"=<" defmacrop guard_operator(operator), do: operator for operator <- ~w(== != < > <= >= and or)a do defp build_condition(field_names, params, {unquote(operator), [], [lhs, rhs]}) do lhs_condition = build_condition(field_names, params, lhs) rhs_condition = build_condition(field_names, params, rhs) {guard_operator(unquote(operator)), lhs_condition, rhs_condition} end end for operator <- ~w(not)a do defp build_condition(field_names, params, {unquote(operator), [], [clause]}) do condition = build_condition(field_names, params, clause) {guard_operator(unquote(operator)), condition} end end defp build_condition(field_names, params, {:in, [], [field, values]}) do field_target = resolve_field_target(field_names, field) case resolve_param_values(params, values) do [] -> [] values -> List.to_tuple([:orelse | Enum.map(values, &{:==, field_target, &1})]) end end defp build_condition(field_names, params, {:is_nil, [], [field]}) do {:==, build_condition(field_names, params, field), nil} end defp build_condition(_, params, {:^, [], [index]}) do Enum.at(params, index) end defp build_condition(field_names, _params, field) when is_tuple(field) do resolve_field_target(field_names, field) end defp build_condition(_, _, value) do value end defp build_body(_, %Ecto.Query{select: nil}) do [] end defp build_body(field_names, %Ecto.Query{select: %{fields: fields}}) do for field <- fields do resolve_field_target(field_names, field) end end defp resolve_field_target(field_names, {:json_extract_path, [], [field, path]}) do field_target = resolve_field_target(field_names, field) resolve_field_target_path(field_target, path) end defp resolve_field_target(field_names, {{:., _, [{:&, [], [0]}, field_name]}, [], []}) do field_index = 1 + Enum.find_index(field_names, fn x -> x == field_name end) :"$#{field_index}" end defp resolve_field_target_path(field_target, path) do # - If the path component is a key, return {:map_get, key, target} # - If the path component is a number, return {:hd, target} outside as many {:tl, _} around # as required. For example, [:metadata, 0] would be {:hd, {:map_get, :metadata, field}}, # while [:metadata, 1] would be {:hd, {:tl, {:map_get, :metadata, field}}} (with one tl). at = fn self -> fn condition, 0 -> {:hd, condition} condition, index -> self.(self).({:tl, condition}, index - 1) end end Enum.reduce(path, field_target, fn key, condition when is_atom(key) or is_binary(key) -> {:map_get, key, condition} index, condition when is_integer(index) -> at.(at).(condition, index) end) end defp resolve_param_values(params, {:^, [], [index, count]}) do for index <- index..(index + count - 1) do Enum.at(params, index) end end defp resolve_param_values(params, {:^, [], [index]}) do Enum.at(params, index) end defp resolve_param_values(_params, values) when is_list(values) do values end end