Current section
Files
Jump to
Current section
Files
lib/query/where.ex
defmodule QueryBuilder.Query.Where do
@moduledoc false
require Ecto.Query
import QueryBuilder.Utils
@value_is_field_marker "@self"
def where(ecto_query, assoc_list, filters, or_filters) do
dynamic_query = build_dynamic_query(ecto_query, assoc_list, filters, or_filters)
Ecto.Query.where(ecto_query, ^dynamic_query)
end
def build_dynamic_query(ecto_query, assoc_list, filters, or_filters) do
resolve = &find_field_and_binding_from_token(assoc_list, &1)
build_dynamic_query_with_resolver(ecto_query, filters, or_filters, resolve)
end
def build_dynamic_query_with_resolver(ecto_query, filters, or_filters, resolve)
when is_function(resolve, 1) do
[filters | Keyword.get_values(or_filters, :or)]
|> Enum.reduce(nil, fn filters, or_acc ->
filters
|> List.wrap()
|> build_dynamic_group_with_resolver(ecto_query, resolve)
|> maybe_or_dynamic(or_acc)
end)
|> case do
nil -> Ecto.Query.dynamic(true)
dynamic_query -> dynamic_query
end
end
defp build_dynamic_group_with_resolver([], _ecto_query, _resolve), do: nil
defp build_dynamic_group_with_resolver([filter | tail], ecto_query, resolve) do
first = apply_filter_with_resolver(ecto_query, resolve, filter)
Enum.reduce(tail, first, fn filter, and_acc ->
filter_dynamic = apply_filter_with_resolver(ecto_query, resolve, filter)
Ecto.Query.dynamic(^and_acc and ^filter_dynamic)
end)
end
defp maybe_or_dynamic(nil, nil), do: nil
defp maybe_or_dynamic(nil, acc), do: acc
defp maybe_or_dynamic(dynamic, nil), do: dynamic
defp maybe_or_dynamic(dynamic, acc) do
Ecto.Query.dynamic(^acc or ^dynamic)
end
defp apply_filter_with_resolver(_query, _resolve, %QueryBuilder.Aggregate{} = aggregate) do
raise ArgumentError,
"invalid where filter: aggregate expression #{inspect(aggregate)} cannot be used in WHERE; " <>
"use HAVING (QueryBuilder.having/*) after GROUP BY (QueryBuilder.group_by/*) instead"
end
defp apply_filter_with_resolver(
_query,
_resolve,
{%QueryBuilder.Aggregate{} = aggregate, _value}
) do
raise ArgumentError,
"invalid where filter: aggregate expression #{inspect(aggregate)} cannot be used in WHERE; " <>
"use HAVING (QueryBuilder.having/*) after GROUP BY (QueryBuilder.group_by/*) instead"
end
defp apply_filter_with_resolver(
_query,
_resolve,
{%QueryBuilder.Aggregate{} = aggregate, _operator, _value}
) do
raise ArgumentError,
"invalid where filter: aggregate expression #{inspect(aggregate)} cannot be used in WHERE; " <>
"use HAVING (QueryBuilder.having/*) after GROUP BY (QueryBuilder.group_by/*) instead"
end
defp apply_filter_with_resolver(
_query,
_resolve,
{%QueryBuilder.Aggregate{} = aggregate, _operator, _value, _operator_opts}
) do
raise ArgumentError,
"invalid where filter: aggregate expression #{inspect(aggregate)} cannot be used in WHERE; " <>
"use HAVING (QueryBuilder.having/*) after GROUP BY (QueryBuilder.group_by/*) instead"
end
defp apply_filter_with_resolver(query, resolve, {field, value}) do
apply_filter_with_resolver(query, resolve, {field, :eq, value, []})
end
defp apply_filter_with_resolver(query, resolve, {field, operator, value}) do
apply_filter_with_resolver(query, resolve, {field, operator, value, []})
end
defp apply_filter_with_resolver(query, resolve, {field, operator, value, operator_opts})
when is_atom(value) do
{field, binding_field1} = resolve.(field)
if value_is_field(value) do
field2 = referenced_field_in_value(value)
{field2, binding_field2} = resolve.(field2)
do_where(
query,
binding_field1,
binding_field2,
{field, operator, field2, operator_opts}
)
else
do_where(query, binding_field1, {field, operator, value, operator_opts})
end
end
defp apply_filter_with_resolver(query, resolve, {field, operator, value, operator_opts}) do
{field, binding} = resolve.(field)
do_where(query, binding, {field, operator, value, operator_opts})
end
defp apply_filter_with_resolver(_query, resolve, custom_fun) when is_function(custom_fun, 1) do
custom_fun.(resolve)
end
defp apply_filter_with_resolver(_query, _resolve, other) do
raise ArgumentError,
"got an invalid filter entry: #{inspect(other)}. " <>
"Expected `{field, value}`, `{field, operator, value}`, `{field, operator, value, operator_opts}`, " <>
"or a 1-arity function."
end
defp do_where(_query, binding, {field, :in, values, []}) when is_list(values) do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) in ^values)
end
defp do_where(query, binding, {field, :in, subqueryable, []}) when is_struct(subqueryable) do
if Ecto.Queryable.impl_for(subqueryable) do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) in subquery(subqueryable))
else
raise Ecto.QueryError,
message:
"expected an Ecto.Queryable subquery for :in on #{inspect(field)}, got: #{inspect(subqueryable)}",
query: query
end
end
defp do_where(_query, binding, {field, :not_in, values, []}) when is_list(values) do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) not in ^values)
end
defp do_where(query, binding, {field, :not_in, subqueryable, []})
when is_struct(subqueryable) do
if Ecto.Queryable.impl_for(subqueryable) do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) not in subquery(subqueryable))
else
raise Ecto.QueryError,
message:
"expected an Ecto.Queryable subquery for :not_in on #{inspect(field)}, got: #{inspect(subqueryable)}",
query: query
end
end
defp do_where(_query, binding, {field, :include, value, []}) do
Ecto.Query.dynamic([{^binding, x}], ^value in field(x, ^field))
end
defp do_where(_query, binding, {field, :exclude, value, []}) do
Ecto.Query.dynamic([{^binding, x}], ^value not in field(x, ^field))
end
defp do_where(_query, binding, {field, operator, nil, []}) when operator in [:eq, :equal_to] do
Ecto.Query.dynamic([{^binding, x}], is_nil(field(x, ^field)))
end
defp do_where(_query, binding, {field, operator, value, []})
when operator in [:eq, :equal_to] do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) == ^value)
end
defp do_where(_query, binding, {field, operator, nil, []})
when operator in [:ne, :other_than] do
Ecto.Query.dynamic([{^binding, x}], not is_nil(field(x, ^field)))
end
defp do_where(_query, binding, {field, operator, value, []})
when operator in [:ne, :other_than] do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) != ^value)
end
defp do_where(_query, binding, {field, operator, value, []})
when operator in [:gt, :greater_than] do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) > ^value)
end
defp do_where(_query, binding, {field, operator, value, []})
when operator in [:ge, :greater_than_or_equal_to] do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) >= ^value)
end
defp do_where(_query, binding, {field, operator, value, []})
when operator in [:lt, :less_than] do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) < ^value)
end
defp do_where(_query, binding, {field, operator, value, []})
when operator in [:le, :less_than_or_equal_to] do
Ecto.Query.dynamic([{^binding, x}], field(x, ^field) <= ^value)
end
defp do_where(query, binding, {field, search_operation, value, operator_opts})
when search_operation in [:starts_with, :ends_with, :contains] do
unless is_binary(value) do
raise Ecto.QueryError,
message:
"expected a string for #{inspect(search_operation)} on #{inspect(field)}, got: #{inspect(value)}",
query: query
end
value =
value
|> String.replace("%", "\\%")
|> String.replace("_", "\\_")
value =
case search_operation do
:starts_with -> "#{value}%"
:ends_with -> "%#{value}"
:contains -> "%#{value}%"
end
case Keyword.get(operator_opts, :case, :sensitive) do
:sensitive ->
Ecto.Query.dynamic([{^binding, x}], like(field(x, ^field), ^value))
case_sensitivity when case_sensitivity in [:insensitive, :i] ->
Ecto.Query.dynamic([{^binding, x}], ilike(field(x, ^field), ^value))
other ->
raise Ecto.QueryError,
message:
"invalid :case option #{inspect(other)} for #{inspect(search_operation)} on #{inspect(field)} " <>
"(supported: :sensitive, :insensitive, :i)",
query: query
end
end
defp do_where(_query, binding, {field, :like, value, []}) do
Ecto.Query.dynamic([{^binding, x}], like(field(x, ^field), ^value))
end
defp do_where(_query, binding, {field, :ilike, value, []}) do
Ecto.Query.dynamic([{^binding, x}], ilike(field(x, ^field), ^value))
end
defp do_where(query, _binding, {field, operator, value, operator_opts}) do
raise Ecto.QueryError,
message:
"unsupported filter {#{inspect(field)}, #{inspect(operator)}, #{inspect(value)}, #{inspect(operator_opts)}}",
query: query
end
defp do_where(_query, b1, b2, {f1, operator, f2, []}) when operator in [:eq, :equal_to] do
Ecto.Query.dynamic([{^b1, x}, {^b2, y}], field(x, ^f1) == field(y, ^f2))
end
defp do_where(_query, b1, b2, {f1, operator, f2, []}) when operator in [:ne, :other_than] do
Ecto.Query.dynamic([{^b1, x}, {^b2, y}], field(x, ^f1) != field(y, ^f2))
end
defp do_where(_query, b1, b2, {f1, operator, f2, []}) when operator in [:gt, :greater_than] do
Ecto.Query.dynamic([{^b1, x}, {^b2, y}], field(x, ^f1) > field(y, ^f2))
end
defp do_where(_query, b1, b2, {f1, operator, f2, []})
when operator in [:ge, :greater_than_or_equal_to] do
Ecto.Query.dynamic([{^b1, x}, {^b2, y}], field(x, ^f1) >= field(y, ^f2))
end
defp do_where(_query, b1, b2, {f1, operator, f2, []}) when operator in [:lt, :less_than] do
Ecto.Query.dynamic([{^b1, x}, {^b2, y}], field(x, ^f1) < field(y, ^f2))
end
defp do_where(_query, b1, b2, {f1, operator, f2, []})
when operator in [:le, :less_than_or_equal_to] do
Ecto.Query.dynamic([{^b1, x}, {^b2, y}], field(x, ^f1) <= field(y, ^f2))
end
defp do_where(query, b1, b2, {f1, search_operation, f2, operator_opts})
when search_operation in [:starts_with, :ends_with, :contains] do
case Keyword.get(operator_opts, :case, :sensitive) do
:sensitive ->
case search_operation do
:starts_with ->
Ecto.Query.dynamic(
[{^b1, x}, {^b2, y}],
fragment("? like concat(?, '%')", field(x, ^f1), field(y, ^f2))
)
:ends_with ->
Ecto.Query.dynamic(
[{^b1, x}, {^b2, y}],
fragment("? like concat('%', ?)", field(x, ^f1), field(y, ^f2))
)
:contains ->
Ecto.Query.dynamic(
[{^b1, x}, {^b2, y}],
fragment("? like concat('%', ?, '%')", field(x, ^f1), field(y, ^f2))
)
end
case_sensitivity when case_sensitivity in [:insensitive, :i] ->
case search_operation do
:starts_with ->
Ecto.Query.dynamic(
[{^b1, x}, {^b2, y}],
fragment("? ilike concat(?, '%')", field(x, ^f1), field(y, ^f2))
)
:ends_with ->
Ecto.Query.dynamic(
[{^b1, x}, {^b2, y}],
fragment("? ilike concat('%', ?)", field(x, ^f1), field(y, ^f2))
)
:contains ->
Ecto.Query.dynamic(
[{^b1, x}, {^b2, y}],
fragment("? ilike concat('%', ?, '%')", field(x, ^f1), field(y, ^f2))
)
end
other ->
raise Ecto.QueryError,
message:
"invalid :case option #{inspect(other)} for field-to-field #{inspect(search_operation)} " <>
"on #{inspect(f1)} vs #{inspect(f2)} (supported: :sensitive, :insensitive, :i)",
query: query
end
end
defp do_where(query, _b1, _b2, {f1, operator, f2, operator_opts}) do
raise Ecto.QueryError,
message:
"unsupported field-to-field filter {#{inspect(f1)}, #{inspect(operator)}, #{inspect(f2)}, #{inspect(operator_opts)}}",
query: query
end
defp value_is_field(val) when val in [nil, false, true], do: false
defp value_is_field(val) do
val |> to_string() |> String.ends_with?(@value_is_field_marker)
end
defp referenced_field_in_value(val) do
str_val = to_string(val)
str_val
|> binary_part(0, byte_size(str_val) - byte_size(@value_is_field_marker))
end
end