ash_postgres/lib/sql_implementation.ex

302 lines
7.4 KiB
Elixir

defmodule AshPostgres.SqlImplementation do
@moduledoc false
use AshSql.Implementation
require Ecto.Query
@impl true
def manual_relationship_function, do: :ash_postgres_join
@impl true
def manual_relationship_subquery_function, do: :ash_postgres_subquery
@impl true
def require_ash_functions_for_or_and_and?, do: true
@impl true
def require_extension_for_citext, do: {true, "citext"}
@impl true
def storage_type(resource, field) do
case AshPostgres.DataLayer.Info.storage_types(resource)[field] do
nil ->
nil
{:array, type} ->
parameterized_type({:array, Ash.Type.get_type(type)}, [], false)
{:array, type, constraints} ->
parameterized_type({:array, Ash.Type.get_type(type)}, constraints, false)
{type, constraints} ->
parameterized_type(type, constraints, false)
type ->
parameterized_type(type, [], false)
end
end
@impl true
def expr(_query, [], _bindings, _embedded?, acc, type) when type in [:map, :jsonb] do
{:ok, Ecto.Query.dynamic(fragment("'[]'::jsonb")), acc}
end
def expr(
query,
%Ash.Query.UpsertConflict{attribute: attribute},
_bindings,
_embedded?,
acc,
_type
) do
query.__ash_bindings__.resource
{:ok,
Ecto.Query.dynamic(
[],
fragment(
"EXCLUDED.?",
literal(
^to_string(
AshPostgres.DataLayer.get_source_for_upsert_field(
attribute,
query.__ash_bindings__.resource
)
)
)
)
), acc}
end
def expr(query, %AshPostgres.Functions.Binding{}, _bindings, _embedded?, acc, _type) do
binding =
AshSql.Bindings.get_binding(
query.__ash_bindings__.resource,
[],
query,
[:left, :inner, :root]
)
if is_nil(binding) do
raise "Error while constructing explicit `binding()` reference."
end
{:ok, Ecto.Query.dynamic([{^binding, row}], row), acc}
end
def expr(
query,
%like{arguments: [arg1, arg2], embedded?: pred_embedded?},
bindings,
embedded?,
acc,
type
)
when like in [AshPostgres.Functions.Like, AshPostgres.Functions.ILike] do
{arg1, acc} =
AshSql.Expr.dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string, acc)
{arg2, acc} =
AshSql.Expr.dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?, :string, acc)
inner_dyn =
if like == AshPostgres.Functions.Like do
Ecto.Query.dynamic(like(^arg1, ^arg2))
else
Ecto.Query.dynamic(ilike(^arg1, ^arg2))
end
if type != Ash.Type.Boolean do
{:ok, inner_dyn, acc}
else
{:ok, Ecto.Query.dynamic(type(^inner_dyn, ^type)), acc}
end
end
def expr(
query,
%AshPostgres.Functions.TrigramSimilarity{
arguments: [arg1, arg2],
embedded?: pred_embedded?
},
bindings,
embedded?,
acc,
_type
) do
{arg1, acc} =
AshSql.Expr.dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string, acc)
{arg2, acc} =
AshSql.Expr.dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?, :string, acc)
{:ok, Ecto.Query.dynamic(fragment("similarity(?, ?)", ^arg1, ^arg2)), acc}
end
def expr(
query,
%AshPostgres.Functions.VectorCosineDistance{
arguments: [arg1, arg2],
embedded?: pred_embedded?
},
bindings,
embedded?,
acc,
_type
) do
{arg1, acc} =
AshSql.Expr.dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string, acc)
{arg2, acc} =
AshSql.Expr.dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?, :string, acc)
{:ok, Ecto.Query.dynamic(fragment("(? <=> ?)", ^arg1, ^arg2)), acc}
end
def expr(
_query,
_expr,
_bindings,
_embedded?,
_acc,
_type
) do
:error
end
@impl true
def table(resource) do
AshPostgres.DataLayer.Info.table(resource)
end
@impl true
def schema(resource) do
AshPostgres.DataLayer.Info.schema(resource)
end
@impl true
def repo(resource, kind) do
AshPostgres.DataLayer.Info.repo(resource, kind)
end
@impl true
def simple_join_first_aggregates(resource) do
AshPostgres.DataLayer.Info.simple_join_first_aggregates(resource)
end
@impl true
def list_aggregate(resource) do
if AshPostgres.DataLayer.Info.pg_version_matches?(resource, ">= 16.0.0") do
"any_value"
else
"array_agg"
end
end
@impl true
def parameterized_type(type, constraints, no_maps? \\ true)
def parameterized_type({:parameterized, _} = type, _, _) do
type
end
def parameterized_type({:parameterized, _, _} = type, _, _) do
type
end
def parameterized_type({:in, type}, constraints, no_maps?) do
parameterized_type({:array, type}, constraints, no_maps?)
end
def parameterized_type({:array, type}, constraints, _) do
case parameterized_type(type, constraints[:items] || [], false) do
nil ->
nil
type ->
{:array, type}
end
end
def parameterized_type(Ash.Type.CiString, constraints, no_maps?) do
parameterized_type(AshPostgres.Type.CiStringWrapper, constraints, no_maps?)
end
def parameterized_type(Ash.Type.String, constraints, no_maps?) do
parameterized_type(AshPostgres.Type.StringWrapper, constraints, no_maps?)
end
def parameterized_type(:tsquery, constraints, no_maps?) do
parameterized_type(AshPostgres.Tsquery, constraints, no_maps?)
end
def parameterized_type(type, _constraints, false)
when type in [Ash.Type.Map, Ash.Type.Map.EctoType],
do: :map
def parameterized_type(type, _constraints, true)
when type in [Ash.Type.Map, Ash.Type.Map.EctoType],
do: nil
def parameterized_type(type, constraints, no_maps?) do
if Ash.Type.ash_type?(type) do
cast_in_query? =
if function_exported?(Ash.Type, :cast_in_query?, 2) do
Ash.Type.cast_in_query?(type, constraints)
else
Ash.Type.cast_in_query?(type)
end
if cast_in_query? do
type = Ash.Type.ecto_type(type)
parameterized_type(type, constraints, no_maps?)
else
nil
end
else
if is_atom(type) && :erlang.function_exported(type, :type, 1) do
if type == :ci_string do
:citext
else
case type.type(constraints || []) do
:ci_string ->
parameterized_type(AshPostgres.Type.CiStringWrapper, constraints, no_maps?)
_ ->
Ecto.ParameterizedType.init(type, constraints || [])
end
end
else
if type == :ci_string do
:citext
else
type
end
end
end
end
@impl true
def determine_types(mod, args, returns \\ nil) do
{types, new_returns} = Ash.Expr.determine_types(mod, args, returns)
new_returns =
case new_returns do
{:parameterized, _} = parameterized -> parameterized
{type, constraints} -> parameterized_type(type, constraints)
other -> other
end
{Enum.map(types, fn
{:parameterized, _} = parameterized ->
parameterized
{type, constraints} ->
parameterized_type(type, constraints)
other ->
other
end), new_returns || returns}
end
end