Current section
Files
Jump to
Current section
Files
lib/ecto/query/builder/join.ex
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Join do
@moduledoc false
alias Ecto.Query.Builder
alias Ecto.Query.{JoinExpr, QueryExpr}
@doc """
Escapes a join expression (not including the `on` expression).
It returns a tuple containing the binds, the on expression (if available)
and the association expression.
## Examples
iex> escape(quote(do: x in "foo"), [], __ENV__)
{:x, {"foo", nil}, nil, []}
iex> escape(quote(do: "foo"), [], __ENV__)
{:_, {"foo", nil}, nil, []}
iex> escape(quote(do: x in Sample), [], __ENV__)
{:x, {nil, Sample}, nil, []}
iex> escape(quote(do: x in __MODULE__), [], __ENV__)
{:x, {nil, __MODULE__}, nil, []}
iex> escape(quote(do: x in {"foo", :sample}), [], __ENV__)
{:x, {"foo", :sample}, nil, []}
iex> escape(quote(do: x in {"foo", Sample}), [], __ENV__)
{:x, {"foo", Sample}, nil, []}
iex> escape(quote(do: x in {"foo", __MODULE__}), [], __ENV__)
{:x, {"foo", __MODULE__}, nil, []}
iex> escape(quote(do: c in assoc(p, :comments)), [p: 0], __ENV__)
{:c, nil, {0, :comments}, []}
iex> escape(quote(do: x in fragment("foo")), [], __ENV__)
{:x, {:{}, [], [:fragment, [], [raw: "foo"]]}, nil, []}
"""
@spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {atom, Macro.t | nil, Macro.t | nil, list}
def escape({:in, _, [{var, _, context}, expr]}, vars, env)
when is_atom(var) and is_atom(context) do
{_, expr, assoc, params} = escape(expr, vars, env)
{var, expr, assoc, params}
end
def escape({:subquery, _, [expr]}, _vars, _env) do
{:_, quote(do: Ecto.Query.subquery(unquote(expr))), nil, []}
end
def escape({:subquery, _, [expr, opts]}, _vars, _env) do
{:_, quote(do: Ecto.Query.subquery(unquote(expr), unquote(opts))), nil, []}
end
def escape({:fragment, _, [_ | _]} = expr, vars, env) do
{expr, {params, :acc}} = Builder.escape(expr, :any, {[], :acc}, vars, env)
{:_, expr, nil, params}
end
def escape({string, schema} = join, _vars, env) when is_binary(string) do
case Macro.expand(schema, env) do
schema when is_atom(schema) ->
{:_, {string, schema}, nil, []}
_ ->
Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression"
end
end
def escape({:assoc, _, [{var, _, context}, field]}, vars, _env)
when is_atom(var) and is_atom(context) do
ensure_field!(field)
var = Builder.find_var!(var, vars)
field = Builder.quoted_atom!(field, "field/2")
{:_, nil, {var, field}, []}
end
def escape({:^, _, [expr]}, _vars, _env) do
{:_, quote(do: Ecto.Query.Builder.Join.join!(unquote(expr))), nil, []}
end
def escape(string, _vars, _env) when is_binary(string) do
{:_, {string, nil}, nil, []}
end
def escape(schema, _vars, _env) when is_atom(schema) do
{:_, {nil, schema}, nil, []}
end
def escape(join, vars, env) do
case Macro.expand(join, env) do
^join ->
Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression"
join ->
escape(join, vars, env)
end
end
@doc """
Called at runtime to check dynamic joins.
"""
def join!(expr) when is_atom(expr),
do: {nil, expr}
def join!(expr) when is_binary(expr),
do: {expr, nil}
def join!({source, module}) when is_binary(source) and is_atom(module),
do: {source, module}
def join!(expr),
do: Ecto.Queryable.to_query(expr)
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, atom, nil | {:ok, String.t | nil}, nil | String.t | [String.t], Macro.Env.t) ::
{Macro.t, Keyword.t, non_neg_integer | nil}
def build(query, qual, binding, expr, count_bind, on, as, prefix, maybe_hints, env) do
{:ok, prefix} = prefix || {:ok, nil}
hints = List.wrap(maybe_hints)
unless Enum.all?(hints, &is_binary/1) do
Builder.error!(
"`hints` must be a compile time string or list of strings, " <>
"got: `#{Macro.to_string(maybe_hints)}`"
)
end
unless is_atom(as) do
Builder.error! "`as` must be a compile time atom, got: `#{Macro.to_string(as)}`"
end
unless is_binary(prefix) or is_nil(prefix) do
Builder.error! "`prefix` must be a compile time string, got: `#{Macro.to_string(prefix)}`"
end
{query, binding} = Builder.escape_binding(query, binding, env)
{join_bind, join_source, join_assoc, join_params} = escape(expr, binding, env)
join_params = Builder.escape_params(join_params)
join_qual = validate_qual(qual)
validate_bind(join_bind, binding)
{count_bind, query} =
if is_nil(count_bind) do
query =
quote do
query = Ecto.Queryable.to_query(unquote(query))
join_count = Builder.count_binds(query)
query
end
{quote(do: join_count), query}
else
{count_bind, query}
end
binding = binding ++ [{join_bind, count_bind}]
next_bind =
if is_integer(count_bind) do
count_bind + 1
else
quote(do: unquote(count_bind) + 1)
end
join = [
as: as,
assoc: join_assoc,
file: env.file,
line: env.line,
params: join_params,
prefix: prefix,
qual: join_qual,
source: join_source,
hints: hints
]
query = build_on(on || true, join, as, query, binding, count_bind, env)
{query, binding, next_bind}
end
def build_on({:^, _, [var]}, join, as, query, _binding, count_bind, env) do
quote do
query = unquote(query)
Ecto.Query.Builder.Join.join!(
query,
%JoinExpr{unquote_splicing(join), on: %QueryExpr{}},
unquote(var),
unquote(as),
unquote(count_bind),
unquote(env.file),
unquote(env.line)
)
end
end
def build_on(on, join, as, query, binding, count_bind, env) do
case Ecto.Query.Builder.Filter.escape(:on, on, count_bind, binding, env) do
{on_expr, {on_params, []}} ->
on_params = Builder.escape_params(on_params)
join =
quote do
%JoinExpr{
unquote_splicing(join),
on: %QueryExpr{
expr: unquote(on_expr),
params: unquote(on_params),
line: unquote(env.line),
file: unquote(env.file)
}
}
end
Builder.apply_query(query, __MODULE__, [join, as, count_bind], env)
_pattern ->
raise ArgumentError, "invalid expression for join `:on`, subqueries aren't supported"
end
end
@doc """
Applies the join expression to the query.
"""
def apply(%Ecto.Query{joins: joins} = query, expr, nil, _count_bind) do
%{query | joins: joins ++ [expr]}
end
def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr, as, count_bind) do
aliases =
case aliases do
%{} -> runtime_aliases(aliases, as, count_bind)
_ -> compile_aliases(aliases, as, count_bind)
end
%{query | joins: joins ++ [expr], aliases: aliases}
end
def apply(query, expr, as, count_bind) do
apply(Ecto.Queryable.to_query(query), expr, as, count_bind)
end
@doc """
Called at runtime to build aliases.
"""
def runtime_aliases(aliases, nil, _), do: aliases
def runtime_aliases(aliases, name, join_count) when is_atom(name) and is_integer(join_count) do
if Map.has_key?(aliases, name) do
Builder.error! "alias `#{inspect name}` already exists"
else
Map.put(aliases, name, join_count)
end
end
defp compile_aliases({:%{}, meta, aliases}, name, join_count)
when is_atom(name) and is_integer(join_count) do
{:%{}, meta, aliases |> Map.new |> runtime_aliases(name, join_count) |> Map.to_list}
end
defp compile_aliases(aliases, name, join_count) do
quote do
Ecto.Query.Builder.Join.runtime_aliases(unquote(aliases), unquote(name), unquote(join_count))
end
end
@doc """
Called at runtime to build a join.
"""
def join!(query, join, expr, as, count_bind, file, line) do
# join without expanded :on is built and applied to the query,
# so that expansion of dynamic :on accounts for the new binding
{on_expr, on_params, on_file, on_line} =
Ecto.Query.Builder.Filter.filter!(:on, apply(query, join, as, count_bind), expr, count_bind, file, line)
join = %{join | on: %QueryExpr{expr: on_expr, params: on_params, line: on_line, file: on_file}}
apply(query, join, as, count_bind)
end
defp validate_qual(qual) when is_atom(qual) do
qual!(qual)
end
defp validate_qual(qual) do
quote(do: Ecto.Query.Builder.Join.qual!(unquote(qual)))
end
defp validate_bind(bind, all) do
if bind != :_ and bind in all do
Builder.error! "variable `#{bind}` is already defined in query"
end
end
@qualifiers [:inner, :inner_lateral, :left, :left_lateral, :right, :full, :cross]
@doc """
Called at runtime to check dynamic qualifier.
"""
def qual!(qual) when qual in @qualifiers, do: qual
def qual!(qual) do
raise ArgumentError,
"invalid join qualifier `#{inspect qual}`, accepted qualifiers are: " <>
Enum.map_join(@qualifiers, ", ", &"`#{inspect &1}`")
end
defp ensure_field!({var, _, _}) when var != :^ do
Builder.error! "you passed the variable `#{var}` to `assoc/2`. Did you mean to pass the atom `:#{var}`?"
end
defp ensure_field!(_), do: true
end