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 alias QueryBuilder.Query.DistinctRoots alias QueryBuilder.Query.GroupBy alias QueryBuilder.Query.Having alias QueryBuilder.Query.LeftJoinLatest alias QueryBuilder.Query.LeftJoinTopN alias QueryBuilder.Query.Limit alias QueryBuilder.Query.Offset alias QueryBuilder.Query.OrderBy alias QueryBuilder.Query.Select alias QueryBuilder.Query.SelectMerge alias QueryBuilder.Query.TopNPer alias QueryBuilder.Query.Where alias QueryBuilder.Query.WhereExistsSubquery alias QueryBuilder.Query.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