mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 13:03:14 +12:00
feat: support expression based calculations
feat: support concat + if expressions improvement: various other improvements
This commit is contained in:
parent
32fdfe354a
commit
dae39f5fda
15 changed files with 966 additions and 58 deletions
|
@ -99,7 +99,7 @@
|
|||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, false},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
|
|
|
@ -273,7 +273,7 @@ defmodule AshPostgres.DataLayer do
|
|||
alias Ash.Filter
|
||||
alias Ash.Query.{BooleanExpression, Not, Ref}
|
||||
|
||||
alias Ash.Query.Function.{Ago, Contains}
|
||||
alias Ash.Query.Function.{Ago, Contains, If}
|
||||
alias Ash.Query.Operator.IsNil
|
||||
|
||||
alias AshPostgres.Functions.{Fragment, TrigramSimilarity, Type}
|
||||
|
@ -284,6 +284,13 @@ defmodule AshPostgres.DataLayer do
|
|||
|
||||
@sections [@postgres]
|
||||
|
||||
# This creates the atoms 0..500, which are used for calculations
|
||||
# If you know of a way to get around the fact that subquery select statement keys
|
||||
# *must* be atoms, please let me know so I can remove this :)
|
||||
for i <- 0..500 do
|
||||
:"#{i}"
|
||||
end
|
||||
|
||||
@moduledoc """
|
||||
A postgres data layer that levereges Ecto's postgres capabilities.
|
||||
|
||||
|
@ -353,6 +360,7 @@ defmodule AshPostgres.DataLayer do
|
|||
def can?(_, {:aggregate, :list}), do: true
|
||||
def can?(_, :aggregate_filter), do: true
|
||||
def can?(_, :aggregate_sort), do: true
|
||||
def can?(_, :expression_calculation), do: true
|
||||
def can?(_, :create), do: true
|
||||
def can?(_, :select), do: true
|
||||
def can?(_, :read), do: true
|
||||
|
@ -389,15 +397,21 @@ defmodule AshPostgres.DataLayer do
|
|||
|
||||
@impl true
|
||||
def set_context(resource, data_layer_query, context) do
|
||||
if context[:data_layer][:table] do
|
||||
{:ok,
|
||||
%{
|
||||
data_layer_query
|
||||
| from: %{data_layer_query.from | source: {context[:data_layer][:table], resource}}
|
||||
}}
|
||||
else
|
||||
{:ok, data_layer_query}
|
||||
end
|
||||
data_layer_query =
|
||||
if context[:data_layer][:table] do
|
||||
%{
|
||||
data_layer_query
|
||||
| from: %{data_layer_query.from | source: {context[:data_layer][:table], resource}}
|
||||
}
|
||||
else
|
||||
data_layer_query
|
||||
end
|
||||
|
||||
data_layer_query =
|
||||
data_layer_query
|
||||
|> default_bindings(resource, context)
|
||||
|
||||
{:ok, data_layer_query}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -1050,10 +1064,12 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp default_bindings(query, resource) do
|
||||
defp default_bindings(query, resource, context \\ %{}) do
|
||||
Map.put_new(query, :__ash_bindings__, %{
|
||||
current: Enum.count(query.joins) + 1,
|
||||
calculations: %{},
|
||||
aggregates: %{},
|
||||
context: context,
|
||||
bindings: %{0 => %{path: [], type: :root, source: resource}}
|
||||
})
|
||||
end
|
||||
|
@ -1136,7 +1152,7 @@ defmodule AshPostgres.DataLayer do
|
|||
defp can_inner_join?(_, _, _), do: false
|
||||
|
||||
@impl true
|
||||
def add_aggregate(query, aggregate, _resource) do
|
||||
def add_aggregate(query, aggregate, _resource, add_base? \\ true) do
|
||||
resource = aggregate.resource
|
||||
query = default_bindings(query, resource)
|
||||
|
||||
|
@ -1200,7 +1216,7 @@ defmodule AshPostgres.DataLayer do
|
|||
new_query =
|
||||
query_with_aggregate_binding
|
||||
|> add_aggregate_to_subquery(resource, aggregate, binding)
|
||||
|> select_aggregate(resource, aggregate)
|
||||
|> select_aggregate(resource, aggregate, add_base?)
|
||||
|
||||
{:ok, new_query}
|
||||
|
||||
|
@ -1209,8 +1225,9 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp select_aggregate(query, resource, aggregate) do
|
||||
binding = get_binding(resource, aggregate.relationship_path, query, :aggregate)
|
||||
@impl true
|
||||
def add_calculation(query, calculation, expression, resource) do
|
||||
query = default_bindings(query, resource)
|
||||
|
||||
query =
|
||||
if query.select do
|
||||
|
@ -1218,15 +1235,125 @@ defmodule AshPostgres.DataLayer do
|
|||
else
|
||||
from(row in query,
|
||||
select: row,
|
||||
select_merge: %{aggregates: %{}}
|
||||
select_merge: %{aggregates: %{}, calculations: %{}}
|
||||
)
|
||||
end
|
||||
|
||||
%{query | select: add_to_select(query.select, binding, aggregate)}
|
||||
{params, expr} =
|
||||
do_filter_to_expr(
|
||||
expression,
|
||||
query.__ash_bindings__,
|
||||
query.select.params
|
||||
)
|
||||
|
||||
{:ok,
|
||||
query
|
||||
|> Map.update!(:select, &add_to_calculation_select(&1, expr, List.wrap(params), calculation))}
|
||||
end
|
||||
|
||||
defp add_to_select(
|
||||
%{expr: {:merge, _, [first, {:%{}, _, [{:aggregates, {:%{}, [], fields}}]}]}} = select,
|
||||
defp select_aggregate(query, resource, aggregate, add_base?) do
|
||||
binding = get_binding(resource, aggregate.relationship_path, query, :aggregate)
|
||||
|
||||
query =
|
||||
if query.select do
|
||||
query
|
||||
else
|
||||
if add_base? do
|
||||
from(row in query,
|
||||
select: row,
|
||||
select_merge: %{aggregates: %{}, calculations: %{}}
|
||||
)
|
||||
else
|
||||
from(row in query, select: row)
|
||||
end
|
||||
end
|
||||
|
||||
%{query | select: add_to_aggregate_select(query.select, binding, aggregate)}
|
||||
end
|
||||
|
||||
defp add_to_calculation_select(
|
||||
%{
|
||||
expr:
|
||||
{:merge, _,
|
||||
[
|
||||
first,
|
||||
{:%{}, _,
|
||||
[{:aggregates, {:%{}, [], agg_fields}}, {:calculations, {:%{}, [], fields}}]}
|
||||
]}
|
||||
} = select,
|
||||
expr,
|
||||
params,
|
||||
%{load: nil} = calculation
|
||||
) do
|
||||
field =
|
||||
{:type, [],
|
||||
[
|
||||
expr,
|
||||
Ash.Type.ecto_type(calculation.type)
|
||||
]}
|
||||
|
||||
name =
|
||||
if calculation.sequence == 0 do
|
||||
calculation.name
|
||||
else
|
||||
String.to_existing_atom("#{calculation.sequence}")
|
||||
end
|
||||
|
||||
new_fields = [
|
||||
{name, field}
|
||||
| fields
|
||||
]
|
||||
|
||||
%{
|
||||
select
|
||||
| expr:
|
||||
{:merge, [],
|
||||
[
|
||||
first,
|
||||
{:%{}, [],
|
||||
[{:aggregates, {:%{}, [], agg_fields}}, {:calculations, {:%{}, [], new_fields}}]}
|
||||
]},
|
||||
params: params
|
||||
}
|
||||
end
|
||||
|
||||
defp add_to_calculation_select(
|
||||
%{expr: select_expr} = select,
|
||||
expr,
|
||||
params,
|
||||
%{load: load_as} = calculation
|
||||
) do
|
||||
field =
|
||||
{:type, [],
|
||||
[
|
||||
expr,
|
||||
Ash.Type.ecto_type(calculation.type)
|
||||
]}
|
||||
|
||||
load_as =
|
||||
if calculation.sequence == 0 do
|
||||
load_as
|
||||
else
|
||||
"#{load_as}_#{calculation.sequence}"
|
||||
end
|
||||
|
||||
%{
|
||||
select
|
||||
| expr: {:merge, [], [select_expr, {:%{}, [], [{load_as, field}]}]},
|
||||
params: params
|
||||
}
|
||||
end
|
||||
|
||||
defp add_to_aggregate_select(
|
||||
%{
|
||||
expr:
|
||||
{:merge, _,
|
||||
[
|
||||
first,
|
||||
{:%{}, _,
|
||||
[{:aggregates, {:%{}, [], fields}}, {:calculations, {:%{}, [], calc_fields}}]}
|
||||
]}
|
||||
} = select,
|
||||
binding,
|
||||
%{load: nil} = aggregate
|
||||
) do
|
||||
|
@ -1259,10 +1386,19 @@ defmodule AshPostgres.DataLayer do
|
|||
| fields
|
||||
]
|
||||
|
||||
%{select | expr: {:merge, [], [first, {:%{}, [], [{:aggregates, {:%{}, [], new_fields}}]}]}}
|
||||
%{
|
||||
select
|
||||
| expr:
|
||||
{:merge, [],
|
||||
[
|
||||
first,
|
||||
{:%{}, [],
|
||||
[{:aggregates, {:%{}, [], new_fields}}, {:calculations, {:%{}, [], calc_fields}}]}
|
||||
]}
|
||||
}
|
||||
end
|
||||
|
||||
defp add_to_select(
|
||||
defp add_to_aggregate_select(
|
||||
%{expr: expr} = select,
|
||||
binding,
|
||||
%{load: load_as} = aggregate
|
||||
|
@ -1415,7 +1551,7 @@ defmodule AshPostgres.DataLayer do
|
|||
{params, expr} =
|
||||
filter_to_expr(
|
||||
aggregate.query.filter,
|
||||
query.__ash_bindings__.bindings,
|
||||
query.__ash_bindings__,
|
||||
query.select.params
|
||||
)
|
||||
|
||||
|
@ -1492,7 +1628,7 @@ defmodule AshPostgres.DataLayer do
|
|||
{params, expr} =
|
||||
filter_to_expr(
|
||||
aggregate.query.filter,
|
||||
query.__ash_bindings__.bindings,
|
||||
query.__ash_bindings__,
|
||||
query.select.params
|
||||
)
|
||||
|
||||
|
@ -1522,7 +1658,7 @@ defmodule AshPostgres.DataLayer do
|
|||
{params, expr} =
|
||||
filter_to_expr(
|
||||
aggregate.query.filter,
|
||||
query.__ash_bindings__.bindings,
|
||||
query.__ash_bindings__,
|
||||
query.select.params
|
||||
)
|
||||
|
||||
|
@ -1669,9 +1805,9 @@ defmodule AshPostgres.DataLayer do
|
|||
join_relationship = Ash.Resource.Info.relationship(source, relationship.join_relationship)
|
||||
|
||||
with {:ok, relationship_through} <-
|
||||
maybe_get_resource_query(relationship.through, join_relationship),
|
||||
maybe_get_resource_query(relationship.through, join_relationship, query),
|
||||
{:ok, relationship_destination} <-
|
||||
maybe_get_resource_query(relationship.destination, relationship) do
|
||||
maybe_get_resource_query(relationship.destination, relationship, query) do
|
||||
relationship_through =
|
||||
relationship_through
|
||||
|> Ecto.Queryable.to_query()
|
||||
|
@ -1689,12 +1825,19 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end)
|
||||
|
||||
used_aggregates = Ash.Filter.used_aggregates(filter, path ++ [relationship.name])
|
||||
used_calculations =
|
||||
Ash.Filter.used_calculations(
|
||||
filter,
|
||||
relationship.destination,
|
||||
path ++ [relationship.name]
|
||||
)
|
||||
|
||||
used_aggregates = used_aggregates(filter, relationship, used_calculations, path)
|
||||
|
||||
Enum.reduce_while(used_aggregates, {:ok, relationship_destination}, fn agg, {:ok, query} ->
|
||||
agg = %{agg | load: agg.name}
|
||||
|
||||
case add_aggregate(query, agg, relationship.destination) do
|
||||
case add_aggregate(query, agg, relationship.destination, false) do
|
||||
{:ok, query} ->
|
||||
{:cont, {:ok, query}}
|
||||
|
||||
|
@ -1704,6 +1847,15 @@ defmodule AshPostgres.DataLayer do
|
|||
end)
|
||||
|> case do
|
||||
{:ok, relationship_destination} ->
|
||||
relationship_destination =
|
||||
case used_aggregates do
|
||||
[] ->
|
||||
relationship_destination
|
||||
|
||||
_ ->
|
||||
subquery(relationship_destination)
|
||||
end
|
||||
|
||||
new_query =
|
||||
case kind do
|
||||
{:aggregate, _, subquery} ->
|
||||
|
@ -1773,7 +1925,7 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
|
||||
defp do_join_relationship(query, relationship, path, kind, source, filter) do
|
||||
case maybe_get_resource_query(relationship.destination, relationship) do
|
||||
case maybe_get_resource_query(relationship.destination, relationship, query) do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
|
@ -1790,13 +1942,20 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end)
|
||||
|
||||
used_aggregates = Ash.Filter.used_aggregates(filter, path ++ [relationship.name])
|
||||
used_calculations =
|
||||
Ash.Filter.used_calculations(
|
||||
filter,
|
||||
relationship.destination,
|
||||
path ++ [relationship.name]
|
||||
)
|
||||
|
||||
used_aggregates = used_aggregates(filter, relationship, used_calculations, path)
|
||||
|
||||
Enum.reduce_while(used_aggregates, {:ok, relationship_destination}, fn agg,
|
||||
{:ok, query} ->
|
||||
agg = %{agg | load: agg.name}
|
||||
|
||||
case add_aggregate(query, agg, relationship.destination) do
|
||||
case add_aggregate(query, agg, relationship.destination, false) do
|
||||
{:ok, query} ->
|
||||
{:cont, {:ok, query}}
|
||||
|
||||
|
@ -1812,7 +1971,7 @@ defmodule AshPostgres.DataLayer do
|
|||
relationship_destination
|
||||
|
||||
_ ->
|
||||
subquery(clean_subquery_select(relationship_destination))
|
||||
subquery(relationship_destination)
|
||||
end
|
||||
|
||||
new_query =
|
||||
|
@ -1876,6 +2035,68 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp used_aggregates(filter, relationship, used_calculations, path) do
|
||||
Ash.Filter.used_aggregates(filter, path ++ [relationship.name]) ++
|
||||
Enum.flat_map(
|
||||
used_calculations,
|
||||
fn calculation ->
|
||||
case Ash.Filter.hydrate_refs(
|
||||
calculation.module.expression(calculation.opts, calculation.context),
|
||||
%{
|
||||
resource: relationship.destination,
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
public?: false
|
||||
}
|
||||
) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Filter.used_aggregates(hydrated)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
# defp add_calculations_to_destination(
|
||||
# {:ok, relationship_destination},
|
||||
# used_calculations,
|
||||
# relationship
|
||||
# ) do
|
||||
# Enum.reduce_while(used_calculations, {:ok, relationship_destination}, fn calculation,
|
||||
# {:ok, query} ->
|
||||
# calculation = %{calculation | load: calculation.name}
|
||||
|
||||
# calculation_context = calculation.context
|
||||
|
||||
# with {:ok, hydrated} <-
|
||||
# Ash.Filter.hydrate_refs(
|
||||
# calculation.module.expression(calculation.opts, calculation_context),
|
||||
# %{
|
||||
# resource: relationship.destination,
|
||||
# aggregates: %{},
|
||||
# calculations: %{},
|
||||
# public?: false
|
||||
# }
|
||||
# ),
|
||||
# {:ok, relationship_destination} <-
|
||||
# add_calculation(
|
||||
# query,
|
||||
# calculation,
|
||||
# hydrated,
|
||||
# relationship.destination
|
||||
# ) do
|
||||
# {:cont, {:ok, relationship_destination}}
|
||||
# else
|
||||
# {:error, error} ->
|
||||
# {:halt, {:error, error}}
|
||||
# end
|
||||
# end)
|
||||
# end
|
||||
|
||||
# defp add_calculations_to_destination({:error, error}, _, _), do: {:error, error}
|
||||
|
||||
defp set_join_prefix(join_query, query, resource) do
|
||||
if Ash.Resource.Info.multitenancy_strategy(resource) == :context do
|
||||
%{join_query | prefix: query.prefix}
|
||||
|
@ -1884,28 +2105,31 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp clean_subquery_select(
|
||||
%{
|
||||
select:
|
||||
%Ecto.Query.SelectExpr{
|
||||
expr:
|
||||
{:merge, [],
|
||||
[
|
||||
_,
|
||||
select
|
||||
]}
|
||||
} = expr
|
||||
} = query
|
||||
) do
|
||||
%{query | select: %{expr | expr: {:merge, [], [{:&, [], [0]}, select]}}}
|
||||
end
|
||||
# defp clean_subquery_select(
|
||||
# %{
|
||||
# select:
|
||||
# %Ecto.Query.SelectExpr{
|
||||
# expr:
|
||||
# {:merge, [],
|
||||
# [
|
||||
# left,
|
||||
# select
|
||||
# ]}
|
||||
# } = expr
|
||||
# } = query
|
||||
# ) do
|
||||
# do_clean(left)
|
||||
# %{query | select: %{expr | expr: {:merge, [], [{:&, [], [0]}, select]}}}
|
||||
# end
|
||||
|
||||
# defp clean_subquery_select(query), do: query
|
||||
|
||||
defp add_filter_expression(query, filter) do
|
||||
wheres =
|
||||
filter
|
||||
|> split_and_statements()
|
||||
|> Enum.map(fn filter ->
|
||||
{params, expr} = filter_to_expr(filter, query.__ash_bindings__.bindings, [])
|
||||
{params, expr} = filter_to_expr(filter, query.__ash_bindings__, [])
|
||||
|
||||
%Ecto.Query.BooleanExpr{
|
||||
expr: expr,
|
||||
|
@ -1958,7 +2182,7 @@ defmodule AshPostgres.DataLayer do
|
|||
do_filter_to_expr(expression, bindings, params, embedded?, type)
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(expr, bindings, params, embedded?, type \\ nil)
|
||||
defp do_filter_to_expr(expr, bindings, params, embedded? \\ false, type \\ nil)
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%BooleanExpression{op: op, left: left, right: right},
|
||||
|
@ -2038,11 +2262,35 @@ defmodule AshPostgres.DataLayer do
|
|||
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
|
||||
|
||||
{params, fragment_data} =
|
||||
Enum.reduce(arguments, {params, []}, fn
|
||||
{:raw, str}, {params, fragment_data} ->
|
||||
{params, fragment_data ++ [{:raw, str}]}
|
||||
|
||||
{:casted_expr, expr}, {params, fragment_data} ->
|
||||
{params, fragment_data ++ [{:expr, expr}]}
|
||||
|
||||
{:expr, expr}, {params, fragment_data} ->
|
||||
{params, expr} = do_filter_to_expr(expr, bindings, params, pred_embedded? || embedded?)
|
||||
{params, fragment_data ++ [{:expr, expr}]}
|
||||
|
@ -2131,6 +2379,88 @@ defmodule AshPostgres.DataLayer do
|
|||
)
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%If{arguments: [condition, when_true, when_false], embedded?: pred_embedded?},
|
||||
bindings,
|
||||
params,
|
||||
embedded?,
|
||||
type
|
||||
) do
|
||||
[condition_type, when_true_type, when_false_type] =
|
||||
determine_types(If, [condition, when_true, when_false])
|
||||
|
||||
{params, condition} =
|
||||
do_filter_to_expr(condition, bindings, params, pred_embedded? || embedded?, condition_type)
|
||||
|
||||
{params, when_true} =
|
||||
do_filter_to_expr(when_true, bindings, params, pred_embedded? || embedded?, when_true_type)
|
||||
|
||||
{params, when_false} =
|
||||
do_filter_to_expr(
|
||||
when_false,
|
||||
bindings,
|
||||
params,
|
||||
pred_embedded? || embedded?,
|
||||
when_false_type
|
||||
)
|
||||
|
||||
do_filter_to_expr(
|
||||
%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,
|
||||
params,
|
||||
embedded?,
|
||||
type
|
||||
)
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%mod{
|
||||
__predicate__?: _,
|
||||
left: left,
|
||||
right: right,
|
||||
embedded?: pred_embedded?,
|
||||
operator: :<>
|
||||
},
|
||||
bindings,
|
||||
params,
|
||||
embedded?,
|
||||
type
|
||||
) do
|
||||
[left_type, right_type] = determine_types(mod, [left, right])
|
||||
|
||||
{params, left_expr} =
|
||||
do_filter_to_expr(left, bindings, params, pred_embedded? || embedded?, left_type)
|
||||
|
||||
{params, right_expr} =
|
||||
do_filter_to_expr(right, bindings, params, pred_embedded? || embedded?, right_type)
|
||||
|
||||
do_filter_to_expr(
|
||||
%Fragment{
|
||||
embedded?: pred_embedded?,
|
||||
arguments: [
|
||||
casted_expr: left_expr,
|
||||
raw: " || ",
|
||||
casted_expr: right_expr
|
||||
]
|
||||
},
|
||||
bindings,
|
||||
params,
|
||||
embedded?,
|
||||
type
|
||||
)
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%mod{
|
||||
__predicate__?: _,
|
||||
|
@ -2160,6 +2490,90 @@ defmodule AshPostgres.DataLayer do
|
|||
]}}
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%Ref{
|
||||
attribute: %Ash.Query.Calculation{} = calculation,
|
||||
relationship_path: [],
|
||||
resource: resource
|
||||
},
|
||||
bindings,
|
||||
params,
|
||||
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_filter_to_expr(
|
||||
expression,
|
||||
bindings,
|
||||
params,
|
||||
embedded?,
|
||||
type
|
||||
)
|
||||
|
||||
{:error, _error} ->
|
||||
{params, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%Ref{
|
||||
attribute: %Ash.Query.Calculation{} = calculation,
|
||||
relationship_path: relationship_path
|
||||
} = ref,
|
||||
bindings,
|
||||
params,
|
||||
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} ->
|
||||
hydrated
|
||||
|> Ash.Filter.update_aggregates(fn aggregate, _ ->
|
||||
%{aggregate | relationship_path: []}
|
||||
end)
|
||||
|> do_filter_to_expr(
|
||||
%{bindings | bindings: temp_bindings},
|
||||
params,
|
||||
embedded?,
|
||||
type
|
||||
)
|
||||
|
||||
_ ->
|
||||
{params, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_filter_to_expr(
|
||||
%Ref{attribute: %{name: name}} = ref,
|
||||
bindings,
|
||||
|
@ -2180,7 +2594,7 @@ defmodule AshPostgres.DataLayer do
|
|||
embedded?: embedded?,
|
||||
arguments: [
|
||||
raw: "",
|
||||
expr: string,
|
||||
casted_expr: string,
|
||||
raw: "::citext"
|
||||
]
|
||||
},
|
||||
|
@ -2233,7 +2647,18 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
|
||||
defp determine_types(mod, values) do
|
||||
mod.types()
|
||||
Code.ensure_compiled(mod)
|
||||
|
||||
cond do
|
||||
:erlang.function_exported(mod, :types, 0) ->
|
||||
mod.types()
|
||||
|
||||
:erlang.function_exported(mod, :args, 0) ->
|
||||
mod.args()
|
||||
|
||||
true ->
|
||||
[:any]
|
||||
end
|
||||
|> Enum.map(fn types ->
|
||||
case types do
|
||||
:same ->
|
||||
|
@ -2332,19 +2757,31 @@ defmodule AshPostgres.DataLayer do
|
|||
%{attribute: %Ash.Query.Aggregate{} = aggregate, relationship_path: []},
|
||||
bindings
|
||||
) do
|
||||
Enum.find_value(bindings, fn {binding, data} ->
|
||||
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.Query.Calculation{}} = 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
|
||||
|
||||
defp ref_binding(%{attribute: %Ash.Resource.Attribute{}} = ref, bindings) do
|
||||
Enum.find_value(bindings, fn {binding, data} ->
|
||||
Enum.find_value(bindings.bindings, fn {binding, data} ->
|
||||
data.path == ref.relationship_path && data.type in [:inner, :left, :root] && binding
|
||||
end)
|
||||
end
|
||||
|
||||
defp ref_binding(%{attribute: %Ash.Query.Aggregate{}} = ref, bindings) do
|
||||
Enum.find_value(bindings, fn {binding, data} ->
|
||||
Enum.find_value(bindings.bindings, fn {binding, data} ->
|
||||
data.path == ref.relationship_path && data.type in [:inner, :left, :root] && binding
|
||||
end)
|
||||
end
|
||||
|
@ -2372,9 +2809,10 @@ defmodule AshPostgres.DataLayer do
|
|||
repo(resource).rollback(term)
|
||||
end
|
||||
|
||||
defp maybe_get_resource_query(resource, relationship) do
|
||||
defp maybe_get_resource_query(resource, relationship, root_query) do
|
||||
resource
|
||||
|> Ash.Query.new()
|
||||
|> Map.put(:context, root_query.__ash_bindings__.context)
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.sort(Map.get(relationship, :sort))
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -95,7 +95,7 @@ defmodule AshPostgres.MixProject do
|
|||
{:ecto_sql, "~> 3.5"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:ash, ash_version("~> 1.44")},
|
||||
{:ash, ash_version("~> 1.45.0-rc0")},
|
||||
{:git_ops, "~> 2.4.2", only: :dev},
|
||||
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
|
||||
{:ex_check, "~> 0.11.0", only: :dev},
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -1,5 +1,5 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "1.44.0", "fa52feb1410cb18f6df64bc4d90c0c2c456a73348069719be5a680c420d7d630", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "d9e37e40b46b1073c70a544cd0dea9d93000441f16e97f9973a17391fa932aa8"},
|
||||
"ash": {:hex, :ash, "1.45.0-rc0", "aa59fea5329fffe1a6624bfce5b9ad87ad7690b5d012700fdd7b610aa5db572f", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "ba924085c0312a1501443a81139988ac60ea29fa39eae02106b599600d436b14"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"uuid_generate_v4()\")",
|
||||
"generated?": false,
|
||||
"name": "id",
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"name": "first_name",
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"name": "last_name",
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"has_create_action": true,
|
||||
"hash": "538DC242254A39070CF9A5D032A9336A2270F8C7F07BEAE35F15BF5EB4D90F20",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshPostgres.TestRepo",
|
||||
"table": "authors"
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"name": "author_id",
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"destination_field": "id",
|
||||
"destination_field_default": null,
|
||||
"destination_field_generated": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "comments_author_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"table": "authors"
|
||||
},
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"name": "post_id",
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"destination_field": "id",
|
||||
"destination_field_default": null,
|
||||
"destination_field_generated": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "special_name_fkey",
|
||||
"on_delete": "delete",
|
||||
"on_update": "update",
|
||||
"table": "posts"
|
||||
},
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"uuid_generate_v4()\")",
|
||||
"generated?": false,
|
||||
"name": "id",
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"name": "title",
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"name": "likes",
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"type": "bigint"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"has_create_action": true,
|
||||
"hash": "F4CCCB7DA640B4C4E8C543CEE6D1F9C3A724E3F8DBE5AC69C4A175A6085599E0",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshPostgres.TestRepo",
|
||||
"table": "comments"
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
defmodule AshPostgres.TestRepo.Migrations.MigrateResources12 do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:authors, primary_key: false) do
|
||||
add :id, :uuid, null: false, default: fragment("uuid_generate_v4()"), primary_key: true
|
||||
add :first_name, :text
|
||||
add :last_name, :text
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:authors)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
defmodule AshPostgres.TestRepo.Migrations.MigrateResources13 do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:comments) do
|
||||
add :author_id,
|
||||
references(:authors, column: :id, name: "comments_author_id_fkey", type: :uuid)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:comments) do
|
||||
remove :author_id
|
||||
end
|
||||
end
|
||||
end
|
215
test/calculation_test.exs
Normal file
215
test/calculation_test.exs
Normal file
|
@ -0,0 +1,215 @@
|
|||
defmodule AshPostgres.CalculationTest do
|
||||
use AshPostgres.RepoCase, async: false
|
||||
alias AshPostgres.Test.{Api, Author, Comment, Post}
|
||||
|
||||
require Ash.Query
|
||||
|
||||
test "an expression calculation can be filtered on" do
|
||||
post =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "match"})
|
||||
|> Api.create!()
|
||||
|
||||
post2 =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "title2"})
|
||||
|> Api.create!()
|
||||
|
||||
post3 =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "title3"})
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "_"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "_"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "_"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|
||||
post
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.replace_relationship(:linked_posts, [post2, post3])
|
||||
|> Api.update!()
|
||||
|
||||
post2
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.replace_relationship(:linked_posts, [post3])
|
||||
|> Api.update!()
|
||||
|
||||
assert [%{c_times_p: 6, title: "match"}] =
|
||||
Post
|
||||
|> Ash.Query.load(:c_times_p)
|
||||
|> Api.read!()
|
||||
|> Enum.filter(&(&1.c_times_p == 6))
|
||||
|
||||
Application.put_env(:foo, :bar, true)
|
||||
|
||||
assert [
|
||||
%{c_times_p: %Ash.NotLoaded{}, title: "match"}
|
||||
] =
|
||||
Post
|
||||
|> Ash.Query.filter(c_times_p == 6)
|
||||
|> Api.read!()
|
||||
end
|
||||
|
||||
test "calculations can be used in related filters" do
|
||||
post =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "match"})
|
||||
|> Api.create!()
|
||||
|
||||
post2 =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "title2"})
|
||||
|> Api.create!()
|
||||
|
||||
post3 =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "title3"})
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "match"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "match"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "match"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "no_match"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post2)
|
||||
|> Api.create!()
|
||||
|
||||
post
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.replace_relationship(:linked_posts, [post2, post3])
|
||||
|> Api.update!()
|
||||
|
||||
post2
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.replace_relationship(:linked_posts, [post3])
|
||||
|> Api.update!()
|
||||
|
||||
posts_query =
|
||||
Post
|
||||
|> Ash.Query.load(:c_times_p)
|
||||
|
||||
assert %{post: %{c_times_p: 6}} =
|
||||
Comment
|
||||
|> Ash.Query.load(post: posts_query)
|
||||
|> Api.read!()
|
||||
|> Enum.filter(&(&1.post.c_times_p == 6))
|
||||
|> Enum.at(0)
|
||||
|
||||
query =
|
||||
Comment
|
||||
|> Ash.Query.filter(post.c_times_p == 6)
|
||||
|> Ash.Query.load(post: posts_query)
|
||||
|> Ash.Query.limit(1)
|
||||
|
||||
Application.put_env(:foo, :bar, true)
|
||||
|
||||
assert [
|
||||
%{post: %{c_times_p: 6, title: "match"}}
|
||||
] = Api.read!(query)
|
||||
end
|
||||
|
||||
test "concat calculation can be filtered on" do
|
||||
author =
|
||||
Author
|
||||
|> Ash.Changeset.new(%{first_name: "is", last_name: "match"})
|
||||
|> Api.create!()
|
||||
|
||||
Author
|
||||
|> Ash.Changeset.new(%{first_name: "not", last_name: "match"})
|
||||
|> Api.create!()
|
||||
|
||||
author_id = author.id
|
||||
|
||||
assert %{id: ^author_id} =
|
||||
Author
|
||||
|> Ash.Query.filter(full_name == "is match")
|
||||
|> Api.read_one!()
|
||||
end
|
||||
|
||||
test "conditional calculations can be filtered on" do
|
||||
author =
|
||||
Author
|
||||
|> Ash.Changeset.new(%{first_name: "tom"})
|
||||
|> Api.create!()
|
||||
|
||||
Author
|
||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
||||
|> Api.create!()
|
||||
|
||||
author_id = author.id
|
||||
|
||||
assert %{id: ^author_id} =
|
||||
Author
|
||||
|> Ash.Query.filter(conditional_full_name == "(none)")
|
||||
|> Api.read_one!()
|
||||
end
|
||||
|
||||
test "parameterized calculations can be filtered on" do
|
||||
Author
|
||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
||||
|> Api.create!()
|
||||
|
||||
assert %{param_full_name: "tom holland"} =
|
||||
Author
|
||||
|> Ash.Query.load(:param_full_name)
|
||||
|> Api.read_one!()
|
||||
|
||||
assert %{param_full_name: "tom~holland"} =
|
||||
Author
|
||||
|> Ash.Query.load(param_full_name: [separator: "~"])
|
||||
|> Api.read_one!()
|
||||
|
||||
assert %{} =
|
||||
Author
|
||||
|> Ash.Query.filter(param_full_name(separator: "~") == "tom~holland")
|
||||
|> Api.read_one!()
|
||||
end
|
||||
|
||||
test "parameterized related calculations can be filtered on" do
|
||||
author =
|
||||
Author
|
||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
||||
|> Api.create!()
|
||||
|
||||
Comment
|
||||
|> Ash.Changeset.new(%{title: "match"})
|
||||
|> Ash.Changeset.replace_relationship(:author, author)
|
||||
|> Api.create!()
|
||||
|
||||
assert %{title: "match"} =
|
||||
Comment
|
||||
|> Ash.Query.filter(author.param_full_name(separator: "~") == "tom~holland")
|
||||
|> Api.read_one!()
|
||||
|
||||
assert %{title: "match"} =
|
||||
Comment
|
||||
|> Ash.Query.filter(
|
||||
author.param_full_name(separator: "~") == "tom~holland" and
|
||||
author.param_full_name(separator: " ") == "tom holland"
|
||||
)
|
||||
|> Api.read_one!()
|
||||
end
|
||||
end
|
|
@ -8,5 +8,6 @@ defmodule AshPostgres.Test.Api do
|
|||
resource(AshPostgres.Test.IntegerPost)
|
||||
resource(AshPostgres.Test.Rating)
|
||||
resource(AshPostgres.Test.PostLink)
|
||||
resource(AshPostgres.Test.Author)
|
||||
end
|
||||
end
|
||||
|
|
35
test/support/concat.ex
Normal file
35
test/support/concat.ex
Normal file
|
@ -0,0 +1,35 @@
|
|||
defmodule AshPostgres.Test.Concat do
|
||||
@moduledoc false
|
||||
use Ash.Calculation
|
||||
require Ash.Query
|
||||
|
||||
def init(opts) do
|
||||
if opts[:keys] && is_list(opts[:keys]) && Enum.all?(opts[:keys], &is_atom/1) do
|
||||
{:ok, opts}
|
||||
else
|
||||
{:error, "Expected a `keys` option for which keys to concat"}
|
||||
end
|
||||
end
|
||||
|
||||
def expression(opts, %{separator: separator}) do
|
||||
Enum.reduce(opts[:keys], nil, fn key, expr ->
|
||||
if expr do
|
||||
if separator do
|
||||
Ash.Query.expr(^expr <> ^separator <> ref(^key))
|
||||
else
|
||||
Ash.Query.expr(^expr <> ref(^key))
|
||||
end
|
||||
else
|
||||
Ash.Query.expr(ref(^key))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def calculate(records, opts, %{separator: separator}) do
|
||||
Enum.map(records, fn record ->
|
||||
Enum.map_join(opts[:keys], separator, fn key ->
|
||||
to_string(Map.get(record, key))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
38
test/support/resources/author.ex
Normal file
38
test/support/resources/author.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
defmodule AshPostgres.Test.Author do
|
||||
@moduledoc false
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
table("authors")
|
||||
repo(AshPostgres.TestRepo)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id, writable?: true)
|
||||
attribute(:first_name, :string)
|
||||
attribute(:last_name, :string)
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate(:full_name, :string, expr(first_name <> " " <> last_name))
|
||||
|
||||
calculate(
|
||||
:conditional_full_name,
|
||||
:string,
|
||||
expr(
|
||||
if(
|
||||
is_nil(first_name) or is_nil(last_name),
|
||||
"(none)",
|
||||
first_name <> " " <> last_name
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
calculate :param_full_name,
|
||||
:string,
|
||||
{AshPostgres.Test.Concat, keys: [:first_name, :last_name]} do
|
||||
argument(:separator, :string, default: " ", constraints: [allow_empty?: true, trim?: false])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,6 +30,7 @@ defmodule AshPostgres.Test.Comment do
|
|||
|
||||
relationships do
|
||||
belongs_to(:post, AshPostgres.Test.Post)
|
||||
belongs_to(:author, AshPostgres.Test.Author)
|
||||
|
||||
has_many(:ratings, AshPostgres.Test.Rating,
|
||||
destination_field: :resource_id,
|
||||
|
|
|
@ -69,6 +69,12 @@ defmodule AshPostgres.Test.Post do
|
|||
)
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate(:c_times_p, :integer, expr(count_of_comments * count_of_linked_posts),
|
||||
load: [:count_of_comments, :count_of_linked_posts]
|
||||
)
|
||||
end
|
||||
|
||||
aggregates do
|
||||
count(:count_of_comments, :comments)
|
||||
count(:count_of_linked_posts, :linked_posts)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
ExUnit.start()
|
||||
ExUnit.configure(stacktrace_depth: 100)
|
||||
|
||||
AshPostgres.TestRepo.start_link()
|
||||
|
|
Loading…
Reference in a new issue