Packages
ecto
3.7.2
3.14.1
3.14.0
3.13.6
3.13.5
3.13.4
3.13.3
3.13.2
3.13.1
3.13.0
3.12.6
3.12.5
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.2
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.4
3.8.3
3.8.2
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.1
3.0.0-rc.0
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.2.0-rc.1
2.2.0-rc.0
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.1.0-rc.5
2.1.0-rc.4
2.1.0-rc.3
2.1.0-rc.2
2.1.0-rc.1
2.1.0-rc.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-beta.2
2.0.0-beta.1
2.0.0-beta.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.12.0-rc
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
A toolkit for data mapping and language integrated query for Elixir
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