defmodule QueryBuilder.Query.Select do @moduledoc false require Ecto.Query import QueryBuilder.Utils def select(ecto_query, assoc_list, selection) do select_expr = build_select_expr!(assoc_list, selection) Ecto.Query.select(ecto_query, ^select_expr) end defp build_select_expr!(assoc_list, selection) when is_function(selection, 1) do selection.(&find_field_and_binding_from_token(assoc_list, &1)) end defp build_select_expr!(assoc_list, %QueryBuilder.Aggregate{} = aggregate) do QueryBuilder.Aggregate.to_dynamic(assoc_list, aggregate) end defp build_select_expr!(assoc_list, selection) when is_atom(selection) or is_binary(selection) do {field, binding} = find_field_and_binding_from_token(assoc_list, selection) Ecto.Query.dynamic([{^binding, x}], field(x, ^field)) end defp build_select_expr!(assoc_list, selection) when is_tuple(selection) do build_tuple_dynamic!(assoc_list, selection) end defp build_select_expr!(assoc_list, selection) when is_list(selection) do if Keyword.keyword?(selection) do if length(selection) != length(Enum.uniq_by(selection, &elem(&1, 0))) do raise ArgumentError, "select keyword list contains duplicate keys; got: #{inspect(selection)}" end build_select_expr!(assoc_list, Map.new(selection)) else tokens = Enum.map(selection, fn token -> if is_atom(token) or is_binary(token) do token else raise ArgumentError, "select list expects field tokens (atoms/strings), got: #{inspect(token)}" end end) token_strings = Enum.map(tokens, &to_string/1) if length(token_strings) != length(Enum.uniq(token_strings)) do raise ArgumentError, "select list contains duplicate keys; " <> "keys match the given tokens (and are compared by token string), got: #{inspect(selection)}" end selection_map = Enum.reduce(tokens, %{}, fn token, acc -> Map.put(acc, token, token) end) build_select_map_expr!(selection_map, assoc_list) end end defp build_select_expr!(assoc_list, %{} = selection) do build_select_map_expr!(selection, assoc_list) end defp build_select_expr!(_assoc_list, selection) do raise ArgumentError, "select expects a field token, a list of field tokens, a map, or a 1-arity function; got: #{inspect(selection)}" end defp build_select_map_expr!(selection, assoc_list) do Enum.reduce(selection, %{}, fn {key, value}, acc -> Map.put(acc, key, build_select_value_expr!(assoc_list, value)) end) end defp build_select_value_expr!(_assoc_list, {:literal, value}), do: value defp build_select_value_expr!(assoc_list, %QueryBuilder.Aggregate{} = aggregate) do QueryBuilder.Aggregate.to_dynamic(assoc_list, aggregate) end defp build_select_value_expr!(assoc_list, value) when is_tuple(value) do build_tuple_dynamic!(assoc_list, value) end defp build_select_value_expr!(assoc_list, value) when is_atom(value) or is_binary(value) do {field, binding} = find_field_and_binding_from_token(assoc_list, value) Ecto.Query.dynamic([{^binding, x}], field(x, ^field)) end defp build_select_value_expr!(_assoc_list, value), do: value defp build_tuple_dynamic!(assoc_list, tuple) do element_dynamics = tuple |> Tuple.to_list() |> Enum.map(fn %Ecto.Query.DynamicExpr{} = dynamic -> dynamic %QueryBuilder.Aggregate{} = aggregate -> QueryBuilder.Aggregate.to_dynamic(assoc_list, aggregate) {:literal, value} -> Ecto.Query.dynamic([], ^value) element when is_atom(element) or is_binary(element) -> {field, binding} = find_field_and_binding_from_token(assoc_list, element) Ecto.Query.dynamic([{^binding, x}], field(x, ^field)) element when is_tuple(element) -> build_tuple_dynamic!(assoc_list, element) element -> Ecto.Query.dynamic([], ^element) end) tuple_ast = element_dynamics |> Enum.with_index() |> Enum.map(fn {_dynamic, index} -> {:^, [], [index]} end) |> then(&{:{}, [], &1}) params = Enum.map(element_dynamics, &{&1, :any}) %Ecto.Query.DynamicExpr{ binding: [], file: __ENV__.file, line: __ENV__.line, fun: fn _query -> {tuple_ast, params, [], %{}} end } end end