Current section

Files

Jump to
query_builder lib query planner.ex
Raw

lib/query/planner.ex

defmodule QueryBuilder.Query.Planner do
@moduledoc false
alias QueryBuilder.AssocList
@barrier_ops [:top_n_per]
@preload_op :preload
@join_only_ops [:inner_join, :left_join]
alias QueryBuilder.Query.{
Distinct,
DistinctRoots,
GroupBy,
Having,
LeftJoinLatest,
LeftJoinTopN,
Limit,
Offset,
OrderBy,
Select,
SelectMerge,
TopNPer,
Where,
WhereExistsSubquery,
WhereNotExistsSubquery
}
def compile(%{ecto_query: ecto_query, operations: operations}) when is_list(operations) do
QueryBuilder.Utils.with_token_cache(fn ->
root_schema = QueryBuilder.Utils.root_schema(ecto_query)
operations = Enum.reverse(operations)
validate_select_operations!(operations)
{preload_ops, stage_ops} =
Enum.split_with(operations, &match?({@preload_op, _assocs, _args}, &1))
{ecto_query, output_assoc_list} = compile_stages(ecto_query, root_schema, stage_ops)
output_assoc_list = apply_preload_ops(root_schema, output_assoc_list, preload_ops)
validate_distinct_roots_preload_conflicts!(operations, output_assoc_list)
ecto_query =
if preload_ops == [] do
ecto_query
else
QueryBuilder.Query.Preload.preload(ecto_query, output_assoc_list)
end
{ecto_query, output_assoc_list}
end)
end
def compile(other) do
raise ArgumentError,
"QueryBuilder.Query.Planner.compile/1 expects a %QueryBuilder.Query{}, got: #{inspect(other)}"
end
defp validate_distinct_roots_preload_conflicts!(operations, assoc_list) do
distinct_roots? = Enum.any?(operations, &match?({:distinct_roots, _assocs, _args}, &1))
if distinct_roots? do
paths = to_many_through_join_preload_paths(assoc_list)
if paths != [] do
raise ArgumentError,
"distinct_roots/1 cannot be combined with `preload_through_join` on to-many associations, " <>
"because it collapses join rows and would drop association rows. " <>
"Use `preload_separate/*` instead. Conflicting preload paths: #{inspect(paths)}"
end
end
:ok
end
defp to_many_through_join_preload_paths(%AssocList{} = assoc_list) do
do_to_many_through_join_preload_paths(assoc_list.roots, [])
end
defp do_to_many_through_join_preload_paths(nodes_map, prefix) when is_map(nodes_map) do
Enum.flat_map(nodes_map, fn {_assoc_field, assoc_data} ->
current = prefix ++ [assoc_data.assoc_field]
matches =
case assoc_data.preload_spec do
%AssocList.PreloadSpec{strategy: :through_join} when assoc_data.cardinality == :many ->
[current]
_ ->
[]
end
matches ++ do_to_many_through_join_preload_paths(assoc_data.nested_assocs, current)
end)
end
defp apply_preload_ops(_root_schema, %AssocList{} = assoc_list, []), do: assoc_list
defp apply_preload_ops(root_schema, %AssocList{} = assoc_list, preload_ops)
when is_list(preload_ops) do
Enum.reduce(preload_ops, assoc_list, fn
{@preload_op, assocs, [%AssocList.PreloadSpec{} = preload_spec]}, assoc_list ->
AssocList.build(root_schema, assoc_list, assocs,
join: :none,
preload_spec: preload_spec
)
{@preload_op, _assocs, [other]}, _assoc_list ->
raise ArgumentError, "invalid preload spec: #{inspect(other)}"
{@preload_op, _assocs, args}, _assoc_list ->
raise ArgumentError, "internal error: invalid preload operation args: #{inspect(args)}"
other, _assoc_list ->
raise ArgumentError, "internal error: invalid preload operation: #{inspect(other)}"
end)
end
defp compile_stages(ecto_query, root_schema, operations) do
stages = split_into_stages(operations)
Enum.reduce(stages, {ecto_query, AssocList.new(root_schema)}, fn stage_ops,
{ecto_query,
_output_assoc_list} ->
stage_ends_with_barrier? =
case List.last(stage_ops) do
{type, _assocs, _args} when type in @barrier_ops -> true
_ -> false
end
{ecto_query, stage_assoc_list} = compile_stage(ecto_query, root_schema, stage_ops)
output_assoc_list =
if stage_ends_with_barrier? do
AssocList.new(root_schema)
else
stage_assoc_list
end
{ecto_query, output_assoc_list}
end)
end
defp split_into_stages([]), do: []
defp split_into_stages(operations) do
{stages, current_stage_rev} =
Enum.reduce(operations, {[], []}, fn {type, _assocs, _args} = op,
{stages, current_stage_rev} ->
current_stage_rev = [op | current_stage_rev]
if type in @barrier_ops do
{[Enum.reverse(current_stage_rev) | stages], []}
else
{stages, current_stage_rev}
end
end)
stages =
case current_stage_rev do
[] -> stages
current_stage_rev -> [Enum.reverse(current_stage_rev) | stages]
end
Enum.reverse(stages)
end
defp compile_stage(ecto_query, root_schema, stage_ops) do
assoc_list =
Enum.reduce(stage_ops, AssocList.new(root_schema), fn {_type, assocs, _args} = op,
assoc_list ->
case List.wrap(assocs) do
[] ->
assoc_list
assoc_fields ->
AssocList.build(root_schema, assoc_list, assoc_fields, assoc_build_opts(op))
end
end)
ecto_query = QueryBuilder.JoinMaker.make_joins(ecto_query, assoc_list)
ecto_query =
Enum.reduce(stage_ops, ecto_query, fn {type, _assocs, _args} = op, ecto_query ->
if type in @join_only_ops do
ecto_query
else
apply_operation(ecto_query, op, assoc_list)
end
end)
{ecto_query, assoc_list}
end
defp assoc_build_opts({:inner_join, _assocs, _args}), do: [join: :inner]
defp assoc_build_opts({:left_join, _assocs, [left_join_mode, join_filters]})
when left_join_mode in [:leaf, :path] and is_list(join_filters),
do: [join: :left, join_filters: join_filters, left_join_mode: left_join_mode]
defp assoc_build_opts({:left_join, _assocs, args}) do
raise ArgumentError, "internal error: invalid left_join operation args: #{inspect(args)}"
end
defp assoc_build_opts(_operation), do: []
defp apply_operation(ecto_query, {:where, _assocs, [filters, or_filters]}, assoc_list) do
Where.where(ecto_query, assoc_list, filters, or_filters)
end
defp apply_operation(ecto_query, {:select, _assocs, [selection]}, assoc_list) do
Select.select(ecto_query, assoc_list, selection)
end
defp apply_operation(ecto_query, {:select_merge, _assocs, [selection]}, assoc_list) do
SelectMerge.select_merge(ecto_query, assoc_list, selection)
end
defp apply_operation(ecto_query, {:distinct, _assocs, [value]}, assoc_list) do
Distinct.distinct(ecto_query, assoc_list, value)
end
defp apply_operation(ecto_query, {:distinct_roots, _assocs, []}, assoc_list) do
DistinctRoots.distinct_roots(ecto_query, assoc_list)
end
defp apply_operation(ecto_query, {:group_by, _assocs, [expr]}, assoc_list) do
GroupBy.group_by(ecto_query, assoc_list, expr)
end
defp apply_operation(ecto_query, {:having, _assocs, [filters, or_filters]}, assoc_list) do
Having.having(ecto_query, assoc_list, filters, or_filters)
end
defp apply_operation(ecto_query, {:order_by, _assocs, [value]}, assoc_list) do
OrderBy.order_by(ecto_query, assoc_list, value)
end
defp apply_operation(ecto_query, {:limit, _assocs, [value]}, assoc_list) do
Limit.limit(ecto_query, assoc_list, value)
end
defp apply_operation(ecto_query, {:offset, _assocs, [value]}, assoc_list) do
Offset.offset(ecto_query, assoc_list, value)
end
defp apply_operation(
ecto_query,
{:where_exists_subquery, _assocs, [assoc_fields, scope, filters, or_filters]},
assoc_list
) do
WhereExistsSubquery.where_exists_subquery(
ecto_query,
assoc_list,
assoc_fields,
scope,
filters,
or_filters
)
end
defp apply_operation(
ecto_query,
{:where_not_exists_subquery, _assocs, [assoc_fields, scope, filters, or_filters]},
assoc_list
) do
WhereNotExistsSubquery.where_not_exists_subquery(
ecto_query,
assoc_list,
assoc_fields,
scope,
filters,
or_filters
)
end
defp apply_operation(ecto_query, {:top_n_per, _assocs, [opts]}, assoc_list) do
TopNPer.top_n_per(ecto_query, assoc_list, opts)
end
defp apply_operation(ecto_query, {:left_join_latest, _assocs, [assoc_field, opts]}, assoc_list) do
LeftJoinLatest.left_join_latest(ecto_query, assoc_list, assoc_field, opts)
end
defp apply_operation(ecto_query, {:left_join_top_n, _assocs, [assoc_field, opts]}, assoc_list) do
LeftJoinTopN.left_join_top_n(ecto_query, assoc_list, assoc_field, opts)
end
defp apply_operation(_ecto_query, {type, _assocs, args}, _assoc_list) do
raise ArgumentError,
"internal error: unknown query operation #{inspect(type)} with args #{inspect(args)}"
end
defp validate_select_operations!(operations) do
select_indexes =
operations
|> Enum.with_index()
|> Enum.flat_map(fn
{{type, _assocs, _args}, index}
when type in [:select, :left_join_latest, :left_join_top_n] ->
[index]
{_op, _index} ->
[]
end)
case select_indexes do
[] ->
:ok
[select_index] ->
if Enum.any?(Enum.take(operations, select_index), fn
{:select_merge, _assocs, _args} -> true
_ -> false
end) do
raise ArgumentError,
"only one select expression is allowed in query; " <>
"calling `select/*` (or `left_join_latest/3` / `left_join_top_n/3`) after `select_merge/*` is not supported (Ecto semantics)"
end
:ok
_many ->
raise ArgumentError,
"only one select expression is allowed in query; " <>
"call `select/*` (or `left_join_latest/3` / `left_join_top_n/3`) at most once and use `select_merge/*` to add fields"
end
end
end