zf

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

preload.ex (5259B)


      1 import Kernel, except: [apply: 3]
      2 
      3 defmodule Ecto.Query.Builder.Preload do
      4   @moduledoc false
      5   alias Ecto.Query.Builder
      6 
      7   @doc """
      8   Escapes a preload.
      9 
     10   A preload may be an atom, a list of atoms or a keyword list
     11   nested as a rose tree.
     12 
     13       iex> escape(:foo, [])
     14       {[:foo], []}
     15 
     16       iex> escape([foo: :bar], [])
     17       {[foo: [:bar]], []}
     18 
     19       iex> escape([:foo, :bar], [])
     20       {[:foo, :bar], []}
     21 
     22       iex> escape([foo: [:bar, bar: :bat]], [])
     23       {[foo: [:bar, bar: [:bat]]], []}
     24 
     25       iex> escape([foo: {:^, [], ["external"]}], [])
     26       {[foo: "external"], []}
     27 
     28       iex> escape([foo: [:bar, {:^, [], ["external"]}], baz: :bat], [])
     29       {[foo: [:bar, "external"], baz: [:bat]], []}
     30 
     31       iex> escape([foo: {:c, [], nil}], [c: 1])
     32       {[], [foo: {1, []}]}
     33 
     34       iex> escape([foo: {{:c, [], nil}, bar: {:l, [], nil}}], [c: 1, l: 2])
     35       {[], [foo: {1, [bar: {2, []}]}]}
     36 
     37       iex> escape([foo: {:c, [], nil}, bar: {:l, [], nil}], [c: 1, l: 2])
     38       {[], [foo: {1, []}, bar: {2, []}]}
     39 
     40       iex> escape([foo: {{:c, [], nil}, :bar}], [c: 1])
     41       {[foo: [:bar]], [foo: {1, []}]}
     42 
     43       iex> escape([foo: [bar: {:c, [], nil}]], [c: 1])
     44       ** (Ecto.Query.CompileError) cannot preload join association `:bar` with binding `c` because parent preload is not a join association
     45 
     46   """
     47   @spec escape(Macro.t, Keyword.t) :: {[Macro.t], [Macro.t]}
     48   def escape(preloads, vars) do
     49     {preloads, assocs} = escape(preloads, :both, [], [], vars)
     50     {Enum.reverse(preloads), Enum.reverse(assocs)}
     51   end
     52 
     53   defp escape(atom, _mode, preloads, assocs, _vars) when is_atom(atom) do
     54     {[atom|preloads], assocs}
     55   end
     56 
     57   defp escape(list, mode, preloads, assocs, vars) when is_list(list) do
     58     Enum.reduce list, {preloads, assocs}, fn item, acc ->
     59       escape_each(item, mode, acc, vars)
     60     end
     61   end
     62 
     63   defp escape({:^, _, [inner]}, _mode, preloads, assocs, _vars) do
     64     {[inner|preloads], assocs}
     65   end
     66 
     67   defp escape(other, _mode, _preloads, _assocs, _vars) do
     68     Builder.error! "`#{Macro.to_string other}` is not a valid preload expression. " <>
     69                    "preload expects an atom, a list of atoms or a keyword list with " <>
     70                    "more preloads as values. Use ^ on the outermost preload to interpolate a value"
     71   end
     72 
     73   defp escape_each({key, {:^, _, [inner]}}, _mode, {preloads, assocs}, _vars) do
     74     key = escape_key(key)
     75     {[{key, inner}|preloads], assocs}
     76   end
     77 
     78   defp escape_each({key, {var, _, context}}, mode, {preloads, assocs}, vars) when is_atom(context) do
     79     assert_assoc!(mode, key, var)
     80     key = escape_key(key)
     81     idx = Builder.find_var!(var, vars)
     82     {preloads, [{key, {idx, []}}|assocs]}
     83   end
     84 
     85   defp escape_each({key, {{var, _, context}, list}}, mode, {preloads, assocs}, vars) when is_atom(context) do
     86     assert_assoc!(mode, key, var)
     87     key = escape_key(key)
     88     idx = Builder.find_var!(var, vars)
     89     {inner_preloads, inner_assocs} = escape(list, :assoc, [], [], vars)
     90     assocs = [{key, {idx, Enum.reverse(inner_assocs)}}|assocs]
     91     case inner_preloads do
     92       [] -> {preloads, assocs}
     93       _  -> {[{key, Enum.reverse(inner_preloads)}|preloads], assocs}
     94     end
     95   end
     96 
     97   defp escape_each({key, list}, _mode, {preloads, assocs}, vars) do
     98     key = escape_key(key)
     99     {inner_preloads, []} = escape(list, :preload, [], [], vars)
    100     {[{key, Enum.reverse(inner_preloads)}|preloads], assocs}
    101   end
    102 
    103   defp escape_each(other, mode, {preloads, assocs}, vars) do
    104     escape(other, mode, preloads, assocs, vars)
    105   end
    106 
    107   defp escape_key(atom) when is_atom(atom) do
    108     atom
    109   end
    110 
    111   defp escape_key({:^, _, [expr]}) do
    112     quote(do: Ecto.Query.Builder.Preload.key!(unquote(expr)))
    113   end
    114 
    115   defp escape_key(other) do
    116     Builder.error! "malformed key in preload `#{Macro.to_string(other)}` in query expression"
    117   end
    118 
    119   defp assert_assoc!(mode, _atom, _var) when mode in [:both, :assoc], do: :ok
    120   defp assert_assoc!(_mode, atom, var) do
    121     Builder.error! "cannot preload join association `#{Macro.to_string atom}` with binding `#{var}` " <>
    122                    "because parent preload is not a join association"
    123   end
    124 
    125   @doc """
    126   Called at runtime to check dynamic preload keys.
    127   """
    128   def key!(key) when is_atom(key),
    129     do: key
    130   def key!(key) do
    131     raise ArgumentError,
    132       "expected key in preload to be an atom, got: `#{inspect key}`"
    133   end
    134 
    135   @doc """
    136   Applies the preloaded value into the query.
    137 
    138   The quoted expression should evaluate to a query at runtime.
    139   If possible, it does all calculations at compile time to avoid
    140   runtime work.
    141   """
    142   @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
    143   def build(query, binding, expr, env) do
    144     {query, binding} = Builder.escape_binding(query, binding, env)
    145     {preloads, assocs} = escape(expr, binding)
    146     Builder.apply_query(query, __MODULE__, [Enum.reverse(preloads), Enum.reverse(assocs)], env)
    147   end
    148 
    149   @doc """
    150   The callback applied by `build/4` to build the query.
    151   """
    152   @spec apply(Ecto.Queryable.t, term, term) :: Ecto.Query.t
    153   def apply(%Ecto.Query{preloads: p, assocs: a} = query, preloads, assocs) do
    154     %{query | preloads: p ++ preloads, assocs: a ++ assocs}
    155   end
    156   def apply(query, preloads, assocs) do
    157     apply(Ecto.Queryable.to_query(query), preloads, assocs)
    158   end
    159 end