zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

join.ex (9796B)


      1 import Kernel, except: [apply: 2]
      2 
      3 defmodule Ecto.Query.Builder.Join do
      4   @moduledoc false
      5 
      6   alias Ecto.Query.Builder
      7   alias Ecto.Query.{JoinExpr, QueryExpr}
      8 
      9   @doc """
     10   Escapes a join expression (not including the `on` expression).
     11 
     12   It returns a tuple containing the binds, the on expression (if available)
     13   and the association expression.
     14 
     15   ## Examples
     16 
     17       iex> escape(quote(do: x in "foo"), [], __ENV__)
     18       {:x, {"foo", nil}, nil, []}
     19 
     20       iex> escape(quote(do: "foo"), [], __ENV__)
     21       {:_, {"foo", nil}, nil, []}
     22 
     23       iex> escape(quote(do: x in Sample), [], __ENV__)
     24       {:x, {nil, Sample}, nil, []}
     25 
     26       iex> escape(quote(do: x in __MODULE__), [], __ENV__)
     27       {:x, {nil, __MODULE__}, nil, []}
     28 
     29       iex> escape(quote(do: x in {"foo", :sample}), [], __ENV__)
     30       {:x, {"foo", :sample}, nil, []}
     31 
     32       iex> escape(quote(do: x in {"foo", Sample}), [], __ENV__)
     33       {:x, {"foo", Sample}, nil, []}
     34 
     35       iex> escape(quote(do: x in {"foo", __MODULE__}), [], __ENV__)
     36       {:x, {"foo", __MODULE__}, nil, []}
     37 
     38       iex> escape(quote(do: c in assoc(p, :comments)), [p: 0], __ENV__)
     39       {:c, nil, {0, :comments}, []}
     40 
     41       iex> escape(quote(do: x in fragment("foo")), [], __ENV__)
     42       {:x, {:{}, [], [:fragment, [], [raw: "foo"]]}, nil, []}
     43 
     44   """
     45   @spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {atom, Macro.t | nil, Macro.t | nil, list}
     46   def escape({:in, _, [{var, _, context}, expr]}, vars, env)
     47       when is_atom(var) and is_atom(context) do
     48     {_, expr, assoc, params} = escape(expr, vars, env)
     49     {var, expr, assoc, params}
     50   end
     51 
     52   def escape({:subquery, _, [expr]}, _vars, _env) do
     53     {:_, quote(do: Ecto.Query.subquery(unquote(expr))), nil, []}
     54   end
     55 
     56   def escape({:subquery, _, [expr, opts]}, _vars, _env) do
     57     {:_, quote(do: Ecto.Query.subquery(unquote(expr), unquote(opts))), nil, []}
     58   end
     59 
     60   def escape({:fragment, _, [_ | _]} = expr, vars, env) do
     61     {expr, {params, _acc}} = Builder.escape(expr, :any, {[], %{}}, vars, env)
     62     {:_, expr, nil, params}
     63   end
     64 
     65   def escape({string, schema} = join, _vars, env) when is_binary(string) do
     66     case Macro.expand(schema, env) do
     67       schema when is_atom(schema) ->
     68         {:_, {string, schema}, nil, []}
     69 
     70       _ ->
     71         Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression"
     72     end
     73   end
     74 
     75   def escape({:assoc, _, [{var, _, context}, field]}, vars, _env)
     76       when is_atom(var) and is_atom(context) do
     77     ensure_field!(field)
     78     var   = Builder.find_var!(var, vars)
     79     field = Builder.quoted_atom!(field, "field/2")
     80     {:_, nil, {var, field}, []}
     81   end
     82 
     83   def escape({:^, _, [expr]}, _vars, _env) do
     84     {:_, quote(do: Ecto.Query.Builder.Join.join!(unquote(expr))), nil, []}
     85   end
     86 
     87   def escape(string, _vars, _env) when is_binary(string) do
     88     {:_, {string, nil}, nil, []}
     89   end
     90 
     91   def escape(schema, _vars, _env) when is_atom(schema) do
     92     {:_, {nil, schema}, nil, []}
     93   end
     94 
     95   def escape(join, vars, env) do
     96     case Macro.expand(join, env) do
     97       ^join ->
     98         Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression"
     99       join ->
    100         escape(join, vars, env)
    101     end
    102   end
    103 
    104   @doc """
    105   Called at runtime to check dynamic joins.
    106   """
    107   def join!(expr) when is_atom(expr),
    108     do: {nil, expr}
    109   def join!(expr) when is_binary(expr),
    110     do: {expr, nil}
    111   def join!({source, module}) when is_binary(source) and is_atom(module),
    112     do: {source, module}
    113   def join!(expr),
    114     do: Ecto.Queryable.to_query(expr)
    115 
    116   @doc """
    117   Builds a quoted expression.
    118 
    119   The quoted expression should evaluate to a query at runtime.
    120   If possible, it does all calculations at compile time to avoid
    121   runtime work.
    122   """
    123   @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) ::
    124               {Macro.t, Keyword.t, non_neg_integer | nil}
    125   def build(query, qual, binding, expr, count_bind, on, as, prefix, maybe_hints, env) do
    126     {:ok, prefix} = prefix || {:ok, nil}
    127     hints = List.wrap(maybe_hints)
    128 
    129     unless Enum.all?(hints, &is_binary/1) do
    130       Builder.error!(
    131         "`hints` must be a compile time string or list of strings, " <>
    132           "got: `#{Macro.to_string(maybe_hints)}`"
    133       )
    134     end
    135 
    136     unless is_binary(prefix) or is_nil(prefix) do
    137       Builder.error! "`prefix` must be a compile time string, got: `#{Macro.to_string(prefix)}`"
    138     end
    139     
    140     as = case as do
    141       {:^, _, [as]} -> as
    142       as when is_atom(as) -> as
    143       as -> Builder.error!("`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}")
    144     end
    145 
    146     {query, binding} = Builder.escape_binding(query, binding, env)
    147     {join_bind, join_source, join_assoc, join_params} = escape(expr, binding, env)
    148     join_params = Builder.escape_params(join_params)
    149 
    150     join_qual = validate_qual(qual)
    151     validate_bind(join_bind, binding)
    152 
    153     {count_bind, query} =
    154       if is_nil(count_bind) do
    155         query =
    156           quote do
    157             query = Ecto.Queryable.to_query(unquote(query))
    158             join_count = Builder.count_binds(query)
    159             query
    160           end
    161         {quote(do: join_count), query}
    162       else
    163         {count_bind, query}
    164       end
    165 
    166     binding = binding ++ [{join_bind, count_bind}]
    167 
    168     next_bind =
    169       if is_integer(count_bind) do
    170         count_bind + 1
    171       else
    172         quote(do: unquote(count_bind) + 1)
    173       end
    174 
    175     join = [
    176       as: as,
    177       assoc: join_assoc,
    178       file: env.file,
    179       line: env.line,
    180       params: join_params,
    181       prefix: prefix,
    182       qual: join_qual,
    183       source: join_source,
    184       hints: hints
    185     ]
    186 
    187     query = build_on(on || true, join, as, query, binding, count_bind, env)
    188     {query, binding, next_bind}
    189   end
    190 
    191   def build_on({:^, _, [var]}, join, as, query, _binding, count_bind, env) do
    192     quote do
    193       query = unquote(query)
    194 
    195       Ecto.Query.Builder.Join.join!(
    196         query,
    197         %JoinExpr{unquote_splicing(join), on: %QueryExpr{}},
    198         unquote(var),
    199         unquote(as),
    200         unquote(count_bind),
    201         unquote(env.file),
    202         unquote(env.line)
    203       )
    204     end
    205   end
    206 
    207   def build_on(on, join, as, query, binding, count_bind, env) do
    208     case Ecto.Query.Builder.Filter.escape(:on, on, count_bind, binding, env) do
    209       {_on_expr, {_on_params, %{subqueries: [_ | _]}}} ->
    210         raise ArgumentError, "invalid expression for join `:on`, subqueries aren't supported"
    211 
    212       {on_expr, {on_params, _acc}} ->
    213         on_params = Builder.escape_params(on_params)
    214 
    215         join =
    216           quote do
    217             %JoinExpr{
    218               unquote_splicing(join),
    219               on: %QueryExpr{
    220                 expr: unquote(on_expr),
    221                 params: unquote(on_params),
    222                 line: unquote(env.line),
    223                 file: unquote(env.file)
    224               }
    225             }
    226           end
    227 
    228         Builder.apply_query(query, __MODULE__, [join, as, count_bind], env)
    229     end
    230   end
    231 
    232   @doc """
    233   Applies the join expression to the query.
    234   """
    235   def apply(%Ecto.Query{joins: joins} = query, expr, nil, _count_bind) do
    236     %{query | joins: joins ++ [expr]}
    237   end
    238   def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr, as, count_bind) do
    239     aliases =
    240       case aliases do
    241         %{} -> runtime_aliases(aliases, as, count_bind)
    242         _ -> compile_aliases(aliases, as, count_bind)
    243       end
    244 
    245     %{query | joins: joins ++ [expr], aliases: aliases}
    246   end
    247   def apply(query, expr, as, count_bind) do
    248     apply(Ecto.Queryable.to_query(query), expr, as, count_bind)
    249   end
    250 
    251   @doc """
    252   Called at runtime to build aliases.
    253   """
    254   def runtime_aliases(aliases, nil, _), do: aliases
    255 
    256   def runtime_aliases(aliases, name, join_count) when is_integer(join_count) do
    257     if Map.has_key?(aliases, name) do
    258       Builder.error! "alias `#{inspect name}` already exists"
    259     else
    260       Map.put(aliases, name, join_count)
    261     end
    262   end
    263 
    264   defp compile_aliases({:%{}, meta, aliases}, name, join_count)
    265        when is_atom(name) and is_integer(join_count) do
    266     {:%{}, meta, aliases |> Map.new |> runtime_aliases(name, join_count) |> Map.to_list}
    267   end
    268 
    269   defp compile_aliases(aliases, name, join_count) do
    270     quote do
    271       Ecto.Query.Builder.Join.runtime_aliases(unquote(aliases), unquote(name), unquote(join_count))
    272     end
    273   end
    274 
    275   @doc """
    276   Called at runtime to build a join.
    277   """
    278   def join!(query, join, expr, as, count_bind, file, line) do
    279     # join without expanded :on is built and applied to the query,
    280     # so that expansion of dynamic :on accounts for the new binding
    281     {on_expr, on_params, on_file, on_line} =
    282       Ecto.Query.Builder.Filter.filter!(:on, apply(query, join, as, count_bind), expr, count_bind, file, line)
    283 
    284     join = %{join | on: %QueryExpr{expr: on_expr, params: on_params, line: on_line, file: on_file}}
    285     apply(query, join, as, count_bind)
    286   end
    287 
    288   defp validate_qual(qual) when is_atom(qual) do
    289     qual!(qual)
    290   end
    291 
    292   defp validate_qual(qual) do
    293     quote(do: Ecto.Query.Builder.Join.qual!(unquote(qual)))
    294   end
    295 
    296   defp validate_bind(bind, all) do
    297     if bind != :_ and bind in all do
    298       Builder.error! "variable `#{bind}` is already defined in query"
    299     end
    300   end
    301 
    302   @qualifiers [:inner, :inner_lateral, :left, :left_lateral, :right, :full, :cross]
    303 
    304   @doc """
    305   Called at runtime to check dynamic qualifier.
    306   """
    307   def qual!(qual) when qual in @qualifiers, do: qual
    308   def qual!(qual) do
    309     raise ArgumentError,
    310       "invalid join qualifier `#{inspect qual}`, accepted qualifiers are: " <>
    311       Enum.map_join(@qualifiers, ", ", &"`#{inspect &1}`")
    312   end
    313 
    314   defp ensure_field!({var, _, _}) when var != :^ do
    315     Builder.error! "you passed the variable `#{var}` to `assoc/2`. Did you mean to pass the atom `:#{var}`?"
    316   end
    317   defp ensure_field!(_), do: true
    318 end