from.ex (6404B)
1 defmodule Ecto.Query.Builder.From do 2 @moduledoc false 3 4 alias Ecto.Query.Builder 5 6 @doc """ 7 Handles from expressions. 8 9 The expressions may either contain an `in` expression or not. 10 The right side is always expected to Queryable. 11 12 ## Examples 13 14 iex> escape(quote(do: MySchema), __ENV__) 15 {quote(do: MySchema), []} 16 17 iex> escape(quote(do: p in posts), __ENV__) 18 {quote(do: posts), [p: 0]} 19 20 iex> escape(quote(do: p in {"posts", MySchema}), __ENV__) 21 {quote(do: {"posts", MySchema}), [p: 0]} 22 23 iex> escape(quote(do: [p, q] in posts), __ENV__) 24 {quote(do: posts), [p: 0, q: 1]} 25 26 iex> escape(quote(do: [_, _] in abc), __ENV__) 27 {quote(do: abc), [_: 0, _: 1]} 28 29 iex> escape(quote(do: other), __ENV__) 30 {quote(do: other), []} 31 32 iex> escape(quote(do: x() in other), __ENV__) 33 ** (Ecto.Query.CompileError) binding list should contain only variables or `{as, var}` tuples, got: x() 34 35 """ 36 @spec escape(Macro.t(), Macro.Env.t()) :: {Macro.t(), Keyword.t()} 37 def escape({:in, _, [var, query]}, env) do 38 query = escape_source(query, env) 39 Builder.escape_binding(query, List.wrap(var), env) 40 end 41 42 def escape(query, env) do 43 query = escape_source(query, env) 44 {query, []} 45 end 46 47 defp escape_source({:fragment, _, _} = fragment, env) do 48 {fragment, {params, _acc}} = Builder.escape(fragment, :any, {[], %{}}, [], env) 49 {fragment, Builder.escape_params(params)} 50 end 51 52 defp escape_source(query, _env), do: query 53 54 @doc """ 55 Builds a quoted expression. 56 57 The quoted expression should evaluate to a query at runtime. 58 If possible, it does all calculations at compile time to avoid 59 runtime work. 60 """ 61 @spec build(Macro.t(), Macro.Env.t(), atom, String.t | nil, nil | {:ok, String.t | nil} | [String.t]) :: 62 {Macro.t(), Keyword.t(), non_neg_integer | nil} 63 def build(query, env, as, prefix, maybe_hints) do 64 hints = List.wrap(maybe_hints) 65 66 unless Enum.all?(hints, &is_valid_hint/1) do 67 Builder.error!( 68 "`hints` must be a compile time string, list of strings, or a tuple " <> 69 "got: `#{Macro.to_string(maybe_hints)}`" 70 ) 71 end 72 73 case prefix do 74 nil -> :ok 75 {:ok, prefix} when is_binary(prefix) or is_nil(prefix) -> :ok 76 _ -> Builder.error!("`prefix` must be a compile time string, got: `#{Macro.to_string(prefix)}`") 77 end 78 79 as = case as do 80 {:^, _, [as]} -> as 81 as when is_atom(as) -> as 82 as -> Builder.error!("`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}") 83 end 84 85 {query, binds} = escape(query, env) 86 87 case expand_from(query, env) do 88 schema when is_atom(schema) -> 89 # Get the source at runtime so no unnecessary compile time 90 # dependencies between modules are added 91 source = quote(do: unquote(schema).__schema__(:source)) 92 {:ok, prefix} = prefix || {:ok, quote(do: unquote(schema).__schema__(:prefix))} 93 {query(prefix, {source, schema}, [], as, hints, env.file, env.line), binds, 1} 94 95 source when is_binary(source) -> 96 {:ok, prefix} = prefix || {:ok, nil} 97 # When a binary is used, there is no schema 98 {query(prefix, {source, nil}, [], as, hints, env.file, env.line), binds, 1} 99 100 {source, schema} when is_binary(source) and is_atom(schema) -> 101 {:ok, prefix} = prefix || {:ok, quote(do: unquote(schema).__schema__(:prefix))} 102 {query(prefix, {source, schema}, [], as, hints, env.file, env.line), binds, 1} 103 104 {{:{}, _, [:fragment, _, _]} = fragment, params} -> 105 {:ok, prefix} = prefix || {:ok, nil} 106 {query(prefix, fragment, params, as, hints, env.file, env.line), binds, 1} 107 108 _other -> 109 quoted = 110 quote do 111 Ecto.Query.Builder.From.apply(unquote(query), unquote(length(binds)), unquote(as), unquote(prefix), unquote(hints)) 112 end 113 114 {quoted, binds, nil} 115 end 116 end 117 118 defp query(prefix, source, params, as, hints, file, line) do 119 aliases = if as, do: [{as, 0}], else: [] 120 from_fields = [source: source, params: params, as: as, prefix: prefix, hints: hints, file: file, line: line] 121 122 query_fields = [ 123 from: {:%, [], [Ecto.Query.FromExpr, {:%{}, [], from_fields}]}, 124 aliases: {:%{}, [], aliases} 125 ] 126 127 {:%, [], [Ecto.Query, {:%{}, [], query_fields}]} 128 end 129 130 defp expand_from({left, right}, env) do 131 {left, Macro.expand(right, env)} 132 end 133 134 defp expand_from(other, env) do 135 Macro.expand(other, env) 136 end 137 138 @doc """ 139 The callback applied by `build/2` to build the query. 140 """ 141 @spec apply(Ecto.Queryable.t(), non_neg_integer, Macro.t(), {:ok, String.t} | nil, [String.t]) :: Ecto.Query.t() 142 def apply(query, binds, as, prefix, hints) do 143 query = 144 query 145 |> Ecto.Queryable.to_query() 146 |> maybe_apply_as(as) 147 |> maybe_apply_prefix(prefix) 148 |> maybe_apply_hints(hints) 149 150 check_binds(query, binds) 151 query 152 end 153 154 defp maybe_apply_as(query, nil), do: query 155 156 defp maybe_apply_as(%{from: %{as: from_as}}, as) when not is_nil(from_as) do 157 Builder.error!( 158 "can't apply alias `#{inspect(as)}`, binding in `from` is already aliased to `#{inspect(from_as)}`" 159 ) 160 end 161 162 defp maybe_apply_as(%{from: from, aliases: aliases} = query, as) do 163 if Map.has_key?(aliases, as) do 164 Builder.error!("alias `#{inspect(as)}` already exists") 165 else 166 %{query | aliases: Map.put(aliases, as, 0), from: %{from | as: as}} 167 end 168 end 169 170 defp maybe_apply_prefix(query, nil), do: query 171 172 defp maybe_apply_prefix(query, {:ok, prefix}) do 173 update_in query.from.prefix, fn 174 nil -> 175 prefix 176 177 from_prefix -> 178 Builder.error!( 179 "can't apply prefix `#{inspect(prefix)}`, `from` is already prefixed to `#{inspect(from_prefix)}`" 180 ) 181 end 182 end 183 184 defp maybe_apply_hints(query, []), do: query 185 defp maybe_apply_hints(query, hints), do: update_in(query.from.hints, &(&1 ++ hints)) 186 187 defp is_valid_hint(hint) when is_binary(hint), do: true 188 defp is_valid_hint({_key, _val}), do: true 189 defp is_valid_hint(_), do: false 190 191 defp check_binds(query, count) do 192 if count > 1 and count > Builder.count_binds(query) do 193 Builder.error!( 194 "`from` in query expression specified #{count} " <> 195 "binds but query contains #{Builder.count_binds(query)} binds" 196 ) 197 end 198 end 199 end