ash_postgres/lib/expr.ex

1045 lines
27 KiB
Elixir
Raw Normal View History

2021-12-21 16:19:24 +13:00
defmodule AshPostgres.Expr do
@moduledoc false
alias Ash.Filter
alias Ash.Query.{BooleanExpression, Exists, Not, Ref}
2022-01-25 11:59:31 +13:00
alias Ash.Query.Operator.IsNil
alias Ash.Query.Function.{Ago, Contains, GetPath, If, Length, Type}
alias AshPostgres.Functions.{Fragment, TrigramSimilarity}
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-06-05 08:57:54 +12:00
def dynamic_expr(_, nil, _, _, _), do: true
2021-12-21 16:19:24 +13:00
# A true filter means "everything"
2022-06-05 08:57:54 +12:00
def dynamic_expr(_, true, _, _, _), do: true
2021-12-21 16:19:24 +13:00
# A false filter means "nothing"
2022-06-05 08:57:54 +12: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
arg1 = do_dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?)
2022-01-25 11:59:31 +13:00
arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?)
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(fragment("similarity(?, ?)", ^arg1, ^arg2))
|> maybe_type(type, query)
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: [%Ref{attribute: %{type: type}}, right]
} = get_path,
bindings,
embedded?,
nil
)
when is_atom(type) and is_list(right) do
if Ash.Type.embedded_type?(type) do
type = determine_type_at_path(type, right)
do_get_path(query, get_path, bindings, embedded?, type)
else
do_get_path(query, get_path, bindings, embedded?)
end
end
defp do_dynamic_expr(
query,
%GetPath{
arguments: [%Ref{attribute: %{type: {:array, type}}}, right]
} = get_path,
bindings,
embedded?,
nil
)
when is_atom(type) and is_list(right) do
if Ash.Type.embedded_type?(type) do
type = determine_type_at_path(type, right)
do_get_path(query, get_path, bindings, embedded?, type)
else
do_get_path(query, get_path, bindings, embedded?)
end
end
defp do_dynamic_expr(
query,
%GetPath{} = get_path,
2022-02-08 10:48:36 +13:00
bindings,
embedded?,
type
) do
do_get_path(query, get_path, bindings, embedded?, type)
2022-02-08 10:48:36 +13:00
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
2022-08-24 11:56:46 +12:00
if "citext" in AshPostgres.DataLayer.Info.repo(query.__ash_bindings__.resource).installed_extensions() do
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "strpos((",
expr: left,
raw: "::citext), (",
expr: right,
raw: ")) > 0"
]
},
bindings,
embedded?,
type
)
else
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "strpos(lower(",
expr: left,
raw: "), lower(",
expr: right,
raw: ")) > 0"
]
},
bindings,
embedded?,
type
)
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
%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,
raw: "), (",
2021-12-21 16:19:24 +13:00
expr: right,
raw: ")) > 0"
2021-12-21 16:19:24 +13:00
]
},
bindings,
embedded?,
type
)
end
defp do_dynamic_expr(
query,
%Length{arguments: [list], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "array_length((",
expr: list,
raw: "), 1)"
]
},
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
%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)
|> case do
[condition_type, nil, nil] ->
[condition_type, type, type]
[condition_type, when_true, nil] ->
[condition_type, when_true, type]
[condition_type, nil, when_false] ->
[condition_type, type, when_false]
[condition_type, when_true, when_false] ->
[condition_type, when_true, when_false]
end
2022-02-17 16:04:54 +13:00
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: [
2022-09-29 09:47:07 +13:00
raw: "(CASE WHEN ",
2021-12-21 16:19:24 +13:00
casted_expr: condition,
raw: " THEN ",
casted_expr: when_true,
raw: " ELSE ",
casted_expr: when_false,
2022-09-29 09:47:07 +13:00
raw: " END)"
2021-12-21 16:19:24 +13:00
]
},
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
2021-12-21 16:19:24 +13:00
) 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, [{:raw, str} | fragment_data], count}
2021-12-21 16:19:24 +13:00
2022-02-01 09:18:57 +13:00
{:casted_expr, dynamic}, {params, fragment_data, count} ->
{[{dynamic, :any} | params], [{:expr, {:^, [], [count]}} | fragment_data], count + 1}
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?)
{[{dynamic, :any} | params], [{:expr, {:^, [], [count]}} | fragment_data], count + 1}
2021-12-21 16:19:24 +13:00
end)
frag_dynamic = %Ecto.Query.DynamicExpr{
2021-12-21 16:19:24 +13:00
fun: fn _query ->
{{:fragment, [], Enum.reverse(fragment_data)}, Enum.reverse(params), [], %{}}
2021-12-21 16:19:24 +13:00
end,
binding: [],
file: __ENV__.file,
line: __ENV__.line
}
if type do
Ecto.Query.dynamic(type(^frag_dynamic, ^type))
else
frag_dynamic
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
%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)
:/ ->
if float_type?(type) do
Ecto.Query.dynamic(type(^left_expr, ^type) / type(^right_expr, ^type))
else
Ecto.Query.dynamic(^left_expr / ^right_expr)
end
2021-12-21 16:19:24 +13:00
:* ->
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: [
raw: "(",
casted_expr: left_expr,
2021-12-21 16:19:24 +13:00
raw: " || ",
casted_expr: right_expr,
raw: ")"
2021-12-21 16:19:24 +13:00
]
},
bindings,
embedded?,
type
)
2022-07-21 06:19:06 +12:00
:|| ->
require_ash_functions!(query)
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "ash_elixir_or(",
casted_expr: left_expr,
raw: ", ",
casted_expr: right_expr,
raw: ")"
]
},
bindings,
embedded?,
type
)
:&& ->
require_ash_functions!(query)
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "ash_elixir_or(",
casted_expr: left_expr,
raw: ", ",
casted_expr: right_expr,
raw: ")"
]
},
bindings,
embedded?,
type
)
2021-12-21 16:19:24 +13:00
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-09-21 15:00:29 +12:00
defp do_dynamic_expr(
query,
%Ash.CiString{string: string} = expression,
bindings,
embedded?,
type
) do
2022-01-25 11:59:31 +13:00
string = do_dynamic_expr(query, string, bindings, embedded?)
2021-12-21 16:19:24 +13:00
2022-09-21 15:00:29 +12:00
require_extension!(query, "citext", expression)
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
2022-09-21 15:00:29 +12:00
} = type_expr,
2021-12-21 16:19:24 +13:00
bindings,
embedded?,
2022-05-21 05:42:20 +12:00
_type
2021-12-21 16:19:24 +13:00
) do
calculation = %{calculation | load: calculation.name}
2022-05-21 05:42:20 +12:00
type = AshPostgres.Types.parameterized_type(calculation.type, [])
2022-09-21 15:00:29 +12:00
validate_type!(query, type, type_expr)
2021-12-21 16:19:24 +13:00
case Ash.Filter.hydrate_refs(
calculation.module.expression(calculation.opts, calculation.context),
%{
resource: resource,
aggregates: %{},
calculations: %{},
public?: false
}
) do
{:ok, expression} ->
expr =
do_dynamic_expr(
query,
expression,
bindings,
embedded?,
type
)
Ecto.Query.dynamic(type(^expr, ^type))
2021-12-21 16:19:24 +13:00
{:error, _error} ->
raise "Failed to hydrate references in #{inspect(calculation.module.expression(calculation.opts, calculation.context))}"
end
end
defp do_dynamic_expr(
2022-09-21 15:00:29 +12: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, [])
2022-09-21 15:00:29 +12:00
validate_type!(query, type, ref)
2021-12-21 16:19:24 +13:00
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?,
2022-05-21 05:42:20 +12:00
_type
2021-12-21 16:19:24 +13:00
) 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}))
2022-05-21 05:42:20 +12:00
type = AshPostgres.Types.parameterized_type(calculation.type, [])
2022-09-21 15:00:29 +12:00
validate_type!(query, type, ref)
2021-12-21 16:19:24 +13:00
case Ash.Filter.hydrate_refs(
calculation.module.expression(calculation.opts, calculation.context),
%{
resource: ref.resource,
aggregates: %{},
calculations: %{},
public?: false
}
) do
{:ok, hydrated} ->
expr =
do_dynamic_expr(
query,
Ash.Filter.update_aggregates(hydrated, fn aggregate, _ ->
%{aggregate | relationship_path: []}
end),
%{bindings | bindings: temp_bindings},
embedded?,
type
)
Ecto.Query.dynamic(type(^expr, ^type))
2021-12-21 16:19:24 +13:00
_ ->
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,
2022-10-18 02:40:32 +13:00
%Type{arguments: [arg1, arg2, constraints]},
2021-12-21 16:19:24 +13:00
bindings,
2022-10-18 02:40:32 +13:00
embedded?,
2021-12-21 16:19:24 +13:00
_type
) do
2022-10-18 02:40:32 +13:00
arg2 = Ash.Type.get_type(arg2)
arg1 = maybe_uuid_to_binary(arg2, arg1, arg1)
2022-01-25 11:59:31 +13:00
type = AshPostgres.Types.parameterized_type(arg2, constraints)
2022-10-18 02:40:32 +13:00
do_dynamic_expr(query, arg1, bindings, embedded?, type)
2021-12-21 16:19:24 +13:00
end
defp do_dynamic_expr(
query,
%Exists{at_path: at_path, path: [first | rest], expr: expr},
bindings,
_embedded?,
_type
) do
resource = Ash.Resource.Info.related(query.__ash_bindings__.resource, at_path)
first_relationship = Ash.Resource.Info.relationship(resource, first)
filter = %Ash.Filter{expression: expr, resource: first_relationship.destination}
{:ok, source} =
AshPostgres.Join.maybe_get_resource_query(
first_relationship.destination,
first_relationship,
query
)
{:ok, filtered} =
AshPostgres.DataLayer.filter(
source,
Ash.Filter.move_to_relationship_path(filter, rest),
first_relationship.destination
)
source_ref =
ref_binding(
%Ref{
attribute: Ash.Resource.Info.attribute(resource, first_relationship.source_attribute),
relationship_path: at_path,
resource: resource
},
bindings
)
exists_query =
if first_relationship.type == :many_to_many do
through_relationship =
Ash.Resource.Info.relationship(resource, first_relationship.join_relationship)
{:ok, through} =
AshPostgres.Join.maybe_get_resource_query(
first_relationship.through,
through_relationship,
query
)
Ecto.Query.from(destination in filtered,
join: through in ^through,
on:
field(through, ^first_relationship.destination_attribute_on_join_resource) ==
field(destination, ^first_relationship.destination_attribute),
on:
field(parent_as(^source_ref), ^first_relationship.source_attribute) ==
field(through, ^first_relationship.source_attribute_on_join_resource),
select: 1
)
else
Ecto.Query.from(destination in filtered,
select: [1],
where:
field(parent_as(^source_ref), ^first_relationship.source_attribute) ==
field(destination, ^first_relationship.destination_attribute)
)
end
Ecto.Query.dynamic(exists(exists_query))
end
2021-12-21 16:19:24 +13:00
defp do_dynamic_expr(
2022-09-21 15:00:29 +12:00
query,
%Ref{attribute: %Ash.Resource.Attribute{name: name, type: attr_type}} = ref,
2021-12-21 16:19:24 +13:00
bindings,
_embedded?,
2022-09-21 15:00:29 +12:00
expr_type
2021-12-21 16:19:24 +13:00
) do
ref_binding = ref_binding(ref, bindings)
if is_nil(ref_binding) do
raise "Error while building reference: #{inspect(ref)}"
end
2022-09-21 15:00:29 +12:00
case AshPostgres.Types.parameterized_type(attr_type || expr_type, []) do
nil ->
Ecto.Query.dynamic(field(as(^ref_binding), ^name))
type ->
validate_type!(query, type, ref)
Ecto.Query.dynamic(type(field(as(^ref_binding), ^name), ^type))
end
2021-12-21 16:19:24 +13:00
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-09-21 15:00:29 +12: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)
2022-09-21 15:00:29 +12:00
validate_type!(query, type, 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-09-21 15:00:29 +12:00
defp do_dynamic_expr(query, value, _bindings, false, type) do
2022-02-17 16:30:19 +13:00
value = maybe_sanitize_list(value)
2022-10-18 02:40:32 +13:00
type = AshPostgres.Types.parameterized_type(type, [])
2022-09-21 15:00:29 +12:00
validate_type!(query, type, value)
2022-10-18 02:40:32 +13:00
2021-12-21 16:19:24 +13:00
Ecto.Query.dynamic(type(^value, ^type))
end
2022-10-18 02:40:32 +13:00
defp maybe_uuid_to_binary({:array, type}, value, _original_value) when is_list(value) do
Enum.map(value, &maybe_uuid_to_binary(type, &1, &1))
end
defp maybe_uuid_to_binary(type, value, original_value)
when type in [
Ash.Type.UUID.EctoType,
:uuid
] and is_binary(value) do
case Ecto.UUID.dump(value) do
{:ok, encoded} -> encoded
_ -> original_value
end
end
defp maybe_uuid_to_binary(_type, _value, original_value), do: original_value
2022-09-21 15:00:29 +12:00
defp validate_type!(query, type, context) do
case type do
{:parameterized, Ash.Type.CiStringWrapper.EctoType, _} ->
require_extension!(query, "citext", context)
:ci_string ->
require_extension!(query, "citext", context)
:citext ->
require_extension!(query, "citext", context)
_ ->
:ok
end
end
defp maybe_type(dynamic, nil, _query), do: dynamic
defp maybe_type(dynamic, type, query) do
type = AshPostgres.Types.parameterized_type(type, [])
validate_type!(query, type, type)
Ecto.Query.dynamic(type(^dynamic, ^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} ->
data.path == ref.relationship_path && data.type in [:inner, :left, :root] && binding
end) ||
Enum.find_value(bindings.bindings, fn {binding, data} ->
data.path == ref.relationship_path && data.type == :aggregate && binding
end)
2021-12-21 16:19:24 +13:00
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
2022-07-21 06:19:06 +12:00
defp do_get_path(
query,
2022-09-21 15:00:29 +12:00
%GetPath{arguments: [left, right], embedded?: pred_embedded?} = get_expr,
bindings,
embedded?,
type \\ nil
) do
path = Enum.map(right, &to_string/1)
expr =
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "(",
expr: left,
raw: " #>> ",
expr: path,
raw: ")"
]
},
bindings,
embedded?,
type
)
if type do
# If we know a type here we use it, since we're pulling out text
2022-09-21 15:00:29 +12:00
validate_type!(query, type, get_expr)
Ecto.Query.dynamic(type(^expr, ^type))
else
expr
end
end
2022-07-21 06:19:06 +12:00
defp require_ash_functions!(query) do
installed_extensions =
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info.repo(query.__ash_bindings__.resource).installed_extensions()
2022-07-21 06:19:06 +12:00
unless "ash-functions" in installed_extensions do
raise """
2022-07-21 06:20:05 +12:00
Cannot use `||` or `&&` operators without adding the extension `ash-functions` to your repo.
Add it to the list in `installed_extensions/0`
2022-07-21 06:19:06 +12:00
If you are using the migration generator, you will then need to generate migrations.
If not, you will need to copy the following into a migration:
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$
LANGUAGE SQL;
\"\"\")
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
AS $$ SELECT COALESCE($1, $2) $$
LANGUAGE SQL;
\"\"\")
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
SELECT CASE
WHEN $1 IS TRUE THEN $2
ELSE $1
END $$
LANGUAGE SQL;
\"\"\")
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
SELECT CASE
WHEN $1 IS NOT NULL THEN $2
ELSE $1
END $$
LANGUAGE SQL;
\"\"\")
"""
end
end
2022-09-21 15:00:29 +12:00
defp require_extension!(query, extension, context) do
repo = AshPostgres.DataLayer.Info.repo(query.__ash_bindings__.resource)
unless extension in repo.installed_extensions() do
raise Ash.Error.Query.InvalidExpression,
expression: context,
message:
"The #{extension} extension needs to be installed before #{inspect(context)} can be used. Please add \"#{extension}\" to the list of installed_extensions in #{inspect(repo)}."
end
end
defp float_type?({:parameterized, type, params}) when is_atom(type) do
type.type(params) in [:float, :decimal]
end
defp float_type?(_) do
false
end
defp determine_type_at_path(type, path) do
path
|> Enum.reject(&is_integer/1)
|> do_determine_type_at_path(type)
|> case do
nil ->
nil
{type, constraints} ->
AshPostgres.Types.parameterized_type(type, constraints)
end
end
defp do_determine_type_at_path([], _), do: nil
defp do_determine_type_at_path([item], type) do
case Ash.Resource.Info.attribute(type, item) do
nil ->
nil
%{type: {:array, type}, constraints: constraints} ->
constraints = constraints[:items] || []
{type, constraints}
%{type: type, constraints: constraints} ->
{type, constraints}
end
end
defp do_determine_type_at_path([item | rest], type) do
case Ash.Resource.Info.attribute(type, item) do
nil ->
nil
%{type: {:array, type}} ->
if Ash.Type.embedded_type?(type) do
type
else
nil
end
%{type: type} ->
if Ash.Type.embedded_type?(type) do
type
else
nil
end
end
|> case do
nil ->
nil
type ->
do_determine_type_at_path(rest, type)
end
end
2021-12-21 16:19:24 +13:00
end