Current section
Files
Jump to
Current section
Files
lib/etso/ets/match_specification.ex
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