zf

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

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