ash_postgres/lib/expr.ex

669 lines
17 KiB
Elixir
Raw Normal View History

2021-12-21 16:19:24 +13:00
defmodule AshPostgres.Expr do
@moduledoc false
alias Ash.Filter
2022-01-25 11:59:31 +13:00
alias Ash.Query.{BooleanExpression, Not, Ref}
alias Ash.Query.Operator.IsNil
2022-02-08 10:48:36 +13:00
alias Ash.Query.Function.{Ago, Contains, GetPath, If}
2022-01-25 11:59:31 +13:00
alias AshPostgres.Functions.{Fragment, TrigramSimilarity, Type}
2021-12-21 16:19:24 +13:00
require Ecto.Query
2022-01-25 11:59:31 +13:00
def dynamic_expr(query, expr, bindings, embedded? \\ false, type \\ nil)
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
def dynamic_expr(query, %Filter{expression: expression}, bindings, embedded?, type) do
dynamic_expr(query, expression, bindings, embedded?, type)
2021-12-21 16:19:24 +13:00
end
# A nil filter means "everything"
2022-01-25 11:59:31 +13:00
def dynamic_expr(_, nil, _, _, _), do: {[], true}
2021-12-21 16:19:24 +13:00
# A true filter means "everything"
2022-01-25 11:59:31 +13:00
def dynamic_expr(_, true, _, _, _), do: {[], true}
2021-12-21 16:19:24 +13:00
# A false filter means "nothing"
2022-01-25 11:59:31 +13:00
def dynamic_expr(_, false, _, _, _), do: {[], false}
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
def dynamic_expr(query, expression, bindings, embedded?, type) do
do_dynamic_expr(query, expression, bindings, embedded?, type)
2021-12-21 16:19:24 +13:00
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(query, expr, bindings, embedded?, type \\ nil)
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(_, {:embed, other}, _bindings, _true, _type) do
2021-12-21 16:19:24 +13:00
other
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(query, %Not{expression: expression}, bindings, embedded?, _type) do
new_expression = do_dynamic_expr(query, expression, bindings, embedded?)
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(not (^new_expression))
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%TrigramSimilarity{arguments: [arg1, arg2], embedded?: pred_embedded?},
bindings,
embedded?,
2022-01-25 11:59:31 +13:00
type
2021-12-21 16:19:24 +13:00
) do
2022-01-25 11:59:31 +13:00
arg1 = do_dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string)
arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?)
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "similarity(",
expr: arg1,
raw: ", ",
expr: arg2,
raw: ")"
]
},
bindings,
embedded?,
type
)
2021-12-21 16:19:24 +13:00
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%IsNil{left: left, right: right, embedded?: pred_embedded?},
bindings,
embedded?,
_type
) do
2022-01-25 11:59:31 +13:00
left_expr = do_dynamic_expr(query, left, bindings, pred_embedded? || embedded?)
right_expr = do_dynamic_expr(query, right, bindings, pred_embedded? || embedded?)
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(is_nil(^left_expr) == ^right_expr)
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
_query,
2021-12-21 16:19:24 +13:00
%Ago{arguments: [left, right], embedded?: _pred_embedded?},
_bindings,
_embedded?,
_type
)
when is_integer(left) and (is_binary(right) or is_atom(right)) do
Ecto.Query.dynamic(datetime_add(^DateTime.utc_now(), ^left * -1, ^to_string(right)))
end
2022-02-08 10:48:36 +13:00
defp do_dynamic_expr(
query,
%GetPath{arguments: [left, right], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
path = Enum.map(right, &to_string/1)
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "(",
expr: left,
raw: " #> ",
expr: path,
raw: ")"
]
},
bindings,
embedded?,
type
)
end
2021-12-21 16:19:24 +13:00
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Contains{arguments: [left, %Ash.CiString{} = right], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "strpos(",
expr: left,
raw: "::citext, ",
expr: right,
raw: ") > 0"
]
},
bindings,
embedded?,
type
)
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Contains{arguments: [left, right], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Fragment{
embedded?: pred_embedded?,
arguments: [
2022-02-08 10:48:36 +13:00
raw: "strpos((",
2021-12-21 16:19:24 +13:00
expr: left,
2022-02-08 10:48:36 +13:00
raw: "), ",
2021-12-21 16:19:24 +13:00
expr: right,
raw: ") > 0"
]
},
bindings,
embedded?,
type
)
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%If{arguments: [condition, when_true, when_false], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
[condition_type, when_true_type, when_false_type] =
case AshPostgres.Types.determine_types(If, [condition, when_true, when_false]) do
[condition_type, when_true] ->
[condition_type, when_true, nil]
[condition_type, when_true, when_false] ->
[condition_type, when_true, when_false]
end
2022-02-17 16:14:17 +13:00
|> Enum.map(fn type ->
2022-02-17 16:17:39 +13:00
if type == :any || type == {:in, :any} do
2022-02-17 16:04:54 +13:00
nil
else
type
end
end)
2022-01-25 11:59:31 +13:00
condition =
do_dynamic_expr(query, condition, bindings, pred_embedded? || embedded?, condition_type)
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
when_true =
do_dynamic_expr(query, when_true, bindings, pred_embedded? || embedded?, when_true_type)
2021-12-21 16:19:24 +13:00
when_false =
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
when_false,
bindings,
pred_embedded? || embedded?,
when_false_type
)
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "CASE WHEN ",
casted_expr: condition,
raw: " THEN ",
casted_expr: when_true,
raw: " ELSE ",
casted_expr: when_false,
raw: " END"
]
},
bindings,
embedded?,
type
)
end
2022-01-25 11:59:31 +13:00
# Sorry :(
# This is bad to do, but is the only reasonable way I could find.
2021-12-21 16:19:24 +13:00
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Fragment{arguments: arguments, embedded?: pred_embedded?},
bindings,
embedded?,
_type
) do
arguments =
case arguments do
[{:raw, _} | _] ->
arguments
arguments ->
[{:raw, ""} | arguments]
end
arguments =
case List.last(arguments) do
nil ->
arguments
{:raw, _} ->
arguments
_ ->
arguments ++ [{:raw, ""}]
end
2022-02-01 10:07:12 +13:00
{params, fragment_data, _} =
2022-02-01 09:18:57 +13:00
Enum.reduce(arguments, {[], [], 0}, fn
{:raw, str}, {params, fragment_data, count} ->
{params, fragment_data ++ [{:raw, str}], count}
2021-12-21 16:19:24 +13:00
2022-02-01 09:18:57 +13:00
{:casted_expr, dynamic}, {params, fragment_data, count} ->
{expr, new_params, new_count} =
2022-01-25 11:59:31 +13:00
Ecto.Query.Builder.Dynamic.partially_expand(
:select,
query,
dynamic,
params,
2022-02-01 09:18:57 +13:00
count
2022-01-25 11:59:31 +13:00
)
2022-02-01 09:18:57 +13:00
{new_params, fragment_data ++ [{:expr, expr}], new_count}
2021-12-21 16:19:24 +13:00
2022-02-01 09:18:57 +13:00
{:expr, expr}, {params, fragment_data, count} ->
2022-01-25 11:59:31 +13:00
dynamic = do_dynamic_expr(query, expr, bindings, pred_embedded? || embedded?)
2022-02-01 09:18:57 +13:00
{expr, new_params, new_count} =
2022-01-25 11:59:31 +13:00
Ecto.Query.Builder.Dynamic.partially_expand(
:select,
query,
dynamic,
params,
2022-02-01 09:18:57 +13:00
count
2022-01-25 11:59:31 +13:00
)
2022-02-01 09:18:57 +13:00
{new_params, fragment_data ++ [{:expr, expr}], new_count}
2021-12-21 16:19:24 +13:00
end)
%Ecto.Query.DynamicExpr{
fun: fn _query ->
2022-01-25 11:59:31 +13:00
{{:fragment, [], fragment_data}, Enum.reverse(params), []}
2021-12-21 16:19:24 +13:00
end,
binding: [],
file: __ENV__.file,
line: __ENV__.line
}
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%BooleanExpression{op: op, left: left, right: right},
bindings,
embedded?,
_type
) do
2022-01-25 11:59:31 +13:00
left_expr = do_dynamic_expr(query, left, bindings, embedded?)
right_expr = do_dynamic_expr(query, right, bindings, embedded?)
2021-12-21 16:19:24 +13:00
case op do
:and ->
2022-01-25 11:59:31 +13:00
Ecto.Query.dynamic(^left_expr and ^right_expr)
2021-12-21 16:19:24 +13:00
:or ->
2022-01-25 11:59:31 +13:00
Ecto.Query.dynamic(^left_expr or ^right_expr)
2021-12-21 16:19:24 +13:00
end
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%mod{
__predicate__?: _,
left: left,
right: right,
embedded?: pred_embedded?,
operator: operator
},
bindings,
embedded?,
type
) do
2022-02-17 16:14:17 +13:00
[left_type, right_type] =
mod
|> AshPostgres.Types.determine_types([left, right])
|> Enum.map(fn type ->
2022-02-17 16:17:39 +13:00
if type == :any || type == {:in, :any} do
2022-02-17 16:14:17 +13:00
nil
else
type
end
end)
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
left_expr = do_dynamic_expr(query, left, bindings, pred_embedded? || embedded?, left_type)
2021-12-21 16:19:24 +13:00
2022-01-25 11:59:31 +13:00
right_expr = do_dynamic_expr(query, right, bindings, pred_embedded? || embedded?, right_type)
2021-12-21 16:19:24 +13:00
case operator do
:== ->
Ecto.Query.dynamic(^left_expr == ^right_expr)
2022-01-25 11:59:31 +13:00
:!= ->
Ecto.Query.dynamic(^left_expr != ^right_expr)
2021-12-21 16:19:24 +13:00
:> ->
Ecto.Query.dynamic(^left_expr > ^right_expr)
:< ->
Ecto.Query.dynamic(^left_expr < ^right_expr)
2022-01-25 11:59:31 +13:00
:>= ->
Ecto.Query.dynamic(^left_expr >= ^right_expr)
:<= ->
Ecto.Query.dynamic(^left_expr <= ^right_expr)
2021-12-21 16:19:24 +13:00
:in ->
Ecto.Query.dynamic(^left_expr in ^right_expr)
:+ ->
Ecto.Query.dynamic(^left_expr + ^right_expr)
:- ->
Ecto.Query.dynamic(^left_expr - ^right_expr)
:/ ->
Ecto.Query.dynamic(^left_expr / ^right_expr)
:* ->
Ecto.Query.dynamic(^left_expr * ^right_expr)
:<> ->
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Fragment{
embedded?: pred_embedded?,
arguments: [
2022-01-25 11:59:31 +13:00
casted_expr: Ecto.Query.dynamic(type(^left_expr, :string)),
2021-12-21 16:19:24 +13:00
raw: " || ",
2022-01-25 11:59:31 +13:00
casted_expr: Ecto.Query.dynamic(type(^right_expr, :string))
2021-12-21 16:19:24 +13:00
]
},
bindings,
embedded?,
type
)
other ->
raise "Operator not implemented #{other}"
end
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(query, %MapSet{} = mapset, bindings, embedded?, type) do
do_dynamic_expr(query, Enum.to_list(mapset), bindings, embedded?, type)
2021-12-21 16:19:24 +13:00
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(query, %Ash.CiString{string: string}, bindings, embedded?, type) do
string = do_dynamic_expr(query, string, bindings, embedded?)
2021-12-21 16:19:24 +13:00
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Fragment{
embedded?: embedded?,
arguments: [
raw: "",
casted_expr: string,
raw: "::citext"
]
},
bindings,
embedded?,
type
)
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Ref{
attribute: %Ash.Query.Calculation{} = calculation,
relationship_path: [],
resource: resource
},
bindings,
embedded?,
type
) do
calculation = %{calculation | load: calculation.name}
case Ash.Filter.hydrate_refs(
calculation.module.expression(calculation.opts, calculation.context),
%{
resource: resource,
aggregates: %{},
calculations: %{},
public?: false
}
) do
{:ok, expression} ->
do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
expression,
bindings,
embedded?,
type
)
{:error, _error} ->
raise "Failed to hydrate references in #{inspect(calculation.module.expression(calculation.opts, calculation.context))}"
end
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
_query,
2021-12-21 16:19:24 +13:00
%Ref{attribute: %Ash.Query.Aggregate{} = aggregate} = ref,
bindings,
_embedded?,
_type
) do
ref_binding = ref_binding(ref, bindings)
if is_nil(ref_binding) do
raise "Error while building reference: #{inspect(ref)}"
end
2021-12-21 16:19:24 +13:00
expr = Ecto.Query.dynamic(field(as(^ref_binding), ^aggregate.name))
type = AshPostgres.Types.parameterized_type(aggregate.type, [])
type =
2022-02-17 16:04:54 +13:00
if type && aggregate.kind == :list do
2021-12-21 16:19:24 +13:00
{:array, type}
else
type
end
2022-01-25 11:59:31 +13:00
coalesced =
if aggregate.default_value do
2022-02-17 16:04:54 +13:00
if type do
Ecto.Query.dynamic(coalesce(^expr, type(^aggregate.default_value, ^type)))
else
Ecto.Query.dynamic(coalesce(^expr, ^aggregate.default_value))
end
2022-01-25 11:59:31 +13:00
else
expr
end
2022-02-17 16:04:54 +13:00
if type do
Ecto.Query.dynamic(type(^coalesced, ^type))
else
coalesced
end
2021-12-21 16:19:24 +13:00
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Ref{
attribute: %Ash.Query.Calculation{} = calculation,
relationship_path: relationship_path
} = ref,
bindings,
embedded?,
type
) do
binding_to_replace =
Enum.find_value(bindings.bindings, fn {i, binding} ->
if binding.path == relationship_path do
i
end
end)
temp_bindings =
bindings.bindings
|> Map.delete(0)
|> Map.update!(binding_to_replace, &Map.merge(&1, %{path: [], type: :root}))
case Ash.Filter.hydrate_refs(
calculation.module.expression(calculation.opts, calculation.context),
%{
resource: ref.resource,
aggregates: %{},
calculations: %{},
public?: false
}
) do
{:ok, hydrated} ->
2022-01-25 11:59:31 +13:00
do_dynamic_expr(
query,
Ash.Filter.update_aggregates(hydrated, fn aggregate, _ ->
%{aggregate | relationship_path: []}
end),
2021-12-21 16:19:24 +13:00
%{bindings | bindings: temp_bindings},
embedded?,
type
)
_ ->
raise "Failed to hydrate references in #{inspect(calculation.module.expression(calculation.opts, calculation.context))}"
end
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Type{arguments: [arg1, arg2], embedded?: pred_embedded?},
bindings,
embedded?,
_type
) do
2022-01-25 11:59:31 +13:00
arg1 = do_dynamic_expr(query, arg1, bindings, false)
arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?)
type = AshPostgres.Types.parameterized_type(arg2, [])
2021-12-21 16:19:24 +13:00
2022-02-17 16:04:54 +13:00
if type do
Ecto.Query.dynamic(type(^arg1, ^type))
else
raise "Attempted to explicitly cast to a type that has `cast_in_query?` configured to `false`, or for which a type could not be determined."
end
2021-12-21 16:19:24 +13:00
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
query,
2021-12-21 16:19:24 +13:00
%Type{arguments: [arg1, arg2, constraints], embedded?: pred_embedded?},
bindings,
embedded?,
_type
) do
2022-01-25 11:59:31 +13:00
arg1 = do_dynamic_expr(query, arg1, bindings, false)
arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?)
type = AshPostgres.Types.parameterized_type(arg2, constraints)
2021-12-21 16:19:24 +13:00
2022-02-17 16:04:54 +13:00
if type do
Ecto.Query.dynamic(type(^arg1, ^type))
else
raise "Attempted to explicitly cast to a type that has `cast_in_query?` configured to `false`, or for which a type could not be determined."
end
2021-12-21 16:19:24 +13:00
end
defp do_dynamic_expr(
2022-01-25 11:59:31 +13:00
_query,
2021-12-21 16:19:24 +13:00
%Ref{attribute: %Ash.Resource.Attribute{name: name}} = ref,
bindings,
_embedded?,
_type
) do
ref_binding = ref_binding(ref, bindings)
if is_nil(ref_binding) do
raise "Error while building reference: #{inspect(ref)}"
end
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(field(as(^ref_binding), ^name))
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(_query, other, _bindings, true, _type) do
if other && is_atom(other) && !is_boolean(other) do
to_string(other)
else
other
end
2021-12-21 16:19:24 +13:00
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(_query, value, _bindings, false, {:in, type}) when is_list(value) do
2022-02-17 16:30:19 +13:00
value = maybe_sanitize_list(value)
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(type(^value, ^{:array, type}))
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(query, value, bindings, false, type)
when not is_nil(value) and is_atom(value) and not is_boolean(value) do
2022-01-25 11:59:31 +13:00
do_dynamic_expr(query, to_string(value), bindings, false, type)
2021-12-21 16:19:24 +13:00
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(_query, value, _bindings, false, type) when type == nil or type == :any do
2022-02-17 16:30:19 +13:00
value = maybe_sanitize_list(value)
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(^value)
end
2022-01-25 11:59:31 +13:00
defp do_dynamic_expr(_query, value, _bindings, false, type) do
2022-02-17 16:30:19 +13:00
value = maybe_sanitize_list(value)
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(type(^value, ^type))
end
2022-02-17 16:30:19 +13:00
defp maybe_sanitize_list(value) do
if is_list(value) do
Enum.map(value, fn value ->
if value && is_atom(value) && !is_boolean(value) do
2022-02-17 16:30:19 +13:00
to_string(value)
else
value
end
end)
else
value
end
end
2021-12-21 16:19:24 +13:00
defp ref_binding(
%{attribute: %Ash.Query.Aggregate{} = aggregate, relationship_path: []},
bindings
) do
Enum.find_value(bindings.bindings, fn {binding, data} ->
data.path == aggregate.relationship_path && data.type == :aggregate && binding
end) ||
Enum.find_value(bindings.bindings, fn {binding, data} ->
data.path == aggregate.relationship_path && data.type in [:inner, :left, :root] && binding
end)
end
defp ref_binding(%{attribute: %Ash.Resource.Attribute{}} = ref, bindings) do
Enum.find_value(bindings.bindings, fn {binding, data} ->
2022-02-12 10:06:51 +13:00
data.path == ref.relationship_path && data.type in [:inner, :left, :root, :aggregate] &&
binding
2021-12-21 16:19:24 +13:00
end)
end
defp ref_binding(%{attribute: %Ash.Query.Aggregate{}} = ref, bindings) do
Enum.find_value(bindings.bindings, fn {binding, data} ->
data.path == ref.relationship_path && data.type in [:inner, :left, :root] && binding
end)
end
end