zf

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

inspect.ex (11827B)


      1 import Inspect.Algebra
      2 import Kernel, except: [to_string: 1]
      3 
      4 alias Ecto.Query.{DynamicExpr, JoinExpr, QueryExpr, WithExpr}
      5 
      6 defimpl Inspect, for: Ecto.Query.DynamicExpr do
      7   def inspect(%DynamicExpr{binding: binding} = dynamic, opts) do
      8     joins =
      9       binding
     10       |> Enum.drop(1)
     11       |> Enum.with_index()
     12       |> Enum.map(&%JoinExpr{ix: &1})
     13 
     14     aliases =
     15       for({as, _} when is_atom(as) <- binding, do: as)
     16       |> Enum.with_index()
     17       |> Map.new
     18 
     19     query = %Ecto.Query{joins: joins, aliases: aliases}
     20 
     21     {expr, binding, params, subqueries, _, _} =
     22       Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)
     23 
     24     names =
     25       Enum.map(binding, fn
     26         {_, {name, _, _}} -> name
     27         {name, _, _} -> name
     28       end)
     29 
     30     query_expr = %{expr: expr, params: params, subqueries: subqueries}
     31     inspected = Inspect.Ecto.Query.expr(expr, List.to_tuple(names), query_expr)
     32 
     33     container_doc("dynamic(", [Macro.to_string(binding), inspected], ")", opts, fn str, _ ->
     34       str
     35     end)
     36   end
     37 end
     38 
     39 defimpl Inspect, for: Ecto.Query do
     40   @doc false
     41   def inspect(query, opts) do
     42     list =
     43       Enum.map(to_list(query), fn
     44         {key, string} ->
     45           concat(Atom.to_string(key) <> ": ", string)
     46 
     47         string ->
     48           string
     49       end)
     50 
     51     result = container_doc("#Ecto.Query<", list, ">", opts, fn str, _ -> str end)
     52 
     53     case query.with_ctes do
     54       %WithExpr{recursive: recursive, queries: [_ | _] = queries} ->
     55         with_ctes =
     56           Enum.map(queries, fn {name, query} ->
     57             cte = case query do
     58               %Ecto.Query{} -> __MODULE__.inspect(query, opts)
     59               %Ecto.Query.QueryExpr{} -> expr(query, {})
     60             end
     61 
     62             concat(["|> with_cte(\"" <> name <> "\", as: ", cte, ")"])
     63           end)
     64 
     65         result = if recursive, do: glue(result, "\n", "|> recursive_ctes(true)"), else: result
     66         [result | with_ctes] |> Enum.intersperse(break("\n")) |> concat()
     67 
     68       _ ->
     69         result
     70     end
     71   end
     72 
     73   @doc false
     74   def to_string(query) do
     75     Enum.map_join(to_list(query), ",\n  ", fn
     76       {key, string} ->
     77         Atom.to_string(key) <> ": " <> string
     78 
     79       string ->
     80         string
     81     end)
     82   end
     83 
     84   defp to_list(query) do
     85     names =
     86       query
     87       |> collect_sources()
     88       |> generate_letters()
     89       |> generate_names()
     90       |> List.to_tuple()
     91 
     92     from = bound_from(query.from, elem(names, 0), names)
     93     joins = joins(query.joins, names)
     94     preloads = preloads(query.preloads)
     95     assocs = assocs(query.assocs, names)
     96     windows = windows(query.windows, names)
     97     combinations = combinations(query.combinations)
     98 
     99     wheres = bool_exprs(%{and: :where, or: :or_where}, query.wheres, names)
    100     group_bys = kw_exprs(:group_by, query.group_bys, names)
    101     havings = bool_exprs(%{and: :having, or: :or_having}, query.havings, names)
    102     order_bys = kw_exprs(:order_by, query.order_bys, names)
    103     updates = kw_exprs(:update, query.updates, names)
    104 
    105     lock = kw_inspect(:lock, query.lock)
    106     limit = kw_expr(:limit, query.limit, names)
    107     offset = kw_expr(:offset, query.offset, names)
    108     select = kw_expr(:select, query.select, names)
    109     distinct = kw_expr(:distinct, query.distinct, names)
    110 
    111     Enum.concat([
    112       from,
    113       joins,
    114       wheres,
    115       group_bys,
    116       havings,
    117       windows,
    118       combinations,
    119       order_bys,
    120       limit,
    121       offset,
    122       lock,
    123       distinct,
    124       updates,
    125       select,
    126       preloads,
    127       assocs
    128     ])
    129   end
    130 
    131   defp bound_from(nil, name, _names), do: ["from #{name} in query"]
    132 
    133   defp bound_from(from, name, names) do
    134     ["from #{name} in #{inspect_source(from, names)}"] ++ kw_as_and_prefix(from)
    135   end
    136 
    137   defp inspect_source(%{source: %Ecto.Query{} = query}, _names), do: "^" <> inspect(query)
    138   defp inspect_source(%{source: %Ecto.SubQuery{query: query}}, _names), do: "subquery(#{to_string(query)})"
    139   defp inspect_source(%{source: {source, nil}}, _names), do: inspect(source)
    140   defp inspect_source(%{source: {nil, schema}}, _names), do: inspect(schema)
    141   defp inspect_source(%{source: {:fragment, _, _} = source} = part, names), do: "#{expr(source, names, part)}"
    142 
    143   defp inspect_source(%{source: {source, schema}}, _names) do
    144     inspect(if source == schema.__schema__(:source), do: schema, else: {source, schema})
    145   end
    146 
    147   defp joins(joins, names) do
    148     joins
    149     |> Enum.with_index()
    150     |> Enum.flat_map(fn {expr, ix} -> join(expr, elem(names, expr.ix || ix + 1), names) end)
    151   end
    152 
    153   defp join(%JoinExpr{qual: qual, assoc: {ix, right}, on: on} = join, name, names) do
    154     string = "#{name} in assoc(#{elem(names, ix)}, #{inspect(right)})"
    155     [{join_qual(qual), string}] ++ kw_as_and_prefix(join) ++ maybe_on(on, names)
    156   end
    157 
    158   defp join(%JoinExpr{qual: qual, on: on} = join, name, names) do
    159     string = "#{name} in #{inspect_source(join, names)}"
    160     [{join_qual(qual), string}] ++ kw_as_and_prefix(join) ++ [on: expr(on, names)]
    161   end
    162 
    163   defp maybe_on(%QueryExpr{expr: true}, _names), do: []
    164   defp maybe_on(%QueryExpr{} = on, names), do: [on: expr(on, names)]
    165 
    166   defp preloads([]), do: []
    167   defp preloads(preloads), do: [preload: inspect(preloads)]
    168 
    169   defp assocs([], _names), do: []
    170   defp assocs(assocs, names), do: [preload: expr(assocs(assocs), names, %{})]
    171 
    172   defp assocs(assocs) do
    173     Enum.map(assocs, fn
    174       {field, {idx, []}} ->
    175         {field, {:&, [], [idx]}}
    176 
    177       {field, {idx, children}} ->
    178         {field, {{:&, [], [idx]}, assocs(children)}}
    179     end)
    180   end
    181 
    182   defp windows(windows, names) do
    183     Enum.map(windows, &window(&1, names))
    184   end
    185 
    186   defp window({name, %{expr: definition} = part}, names) do
    187     {:windows, "[#{name}: " <> expr(definition, names, part) <> "]"}
    188   end
    189 
    190   defp combinations(combinations) do
    191     Enum.map(combinations, fn {key, val} -> {key, "(" <> to_string(val) <> ")"} end)
    192   end
    193 
    194   defp bool_exprs(keys, exprs, names) do
    195     Enum.map(exprs, fn %{expr: expr, op: op} = part ->
    196       {Map.fetch!(keys, op), expr(expr, names, part)}
    197     end)
    198   end
    199 
    200   defp kw_exprs(key, exprs, names) do
    201     Enum.map(exprs, &{key, expr(&1, names)})
    202   end
    203 
    204   defp kw_expr(_key, nil, _names), do: []
    205   defp kw_expr(key, expr, names), do: [{key, expr(expr, names)}]
    206 
    207   defp kw_inspect(_key, nil), do: []
    208   defp kw_inspect(key, val), do: [{key, inspect(val)}]
    209 
    210   defp kw_as_and_prefix(%{as: as, prefix: prefix}) do
    211     kw_inspect(:as, as) ++ kw_inspect(:prefix, prefix)
    212   end
    213 
    214   defp expr(%{expr: expr} = part, names) do
    215     expr(expr, names, part)
    216   end
    217 
    218   @doc false
    219   def expr(expr, names, part) do
    220     expr
    221     |> Macro.traverse(:ok, &{prewalk(&1), &2}, &{postwalk(&1, names, part), &2})
    222     |> elem(0)
    223     |> macro_to_string()
    224   end
    225 
    226   if Version.match?(System.version(), ">= 1.11.0") do
    227     defp macro_to_string(expr), do: Macro.to_string(expr)
    228   else
    229     defp macro_to_string(expr) do
    230       Macro.to_string(expr, fn
    231         {{:., _, [_, _]}, _, []}, string -> String.replace_suffix(string, "()", "")
    232         _other, string -> string
    233       end)
    234     end
    235   end
    236 
    237   # Tagged values
    238   defp prewalk(%Ecto.Query.Tagged{value: value, tag: nil}) do
    239     value
    240   end
    241 
    242   defp prewalk(%Ecto.Query.Tagged{value: value, tag: {:parameterized, type, opts}}) do
    243     {:type, [], [value, {:{}, [], [:parameterized, type, opts]}]}
    244   end
    245 
    246   defp prewalk(%Ecto.Query.Tagged{value: value, tag: tag}) do
    247     {:type, [], [value, tag]}
    248   end
    249 
    250   defp prewalk({:type, _, [value, {:parameterized, type, opts}]}) do
    251     {:type, [], [value, {:{}, [], [:parameterized, type, opts]}]}
    252   end
    253 
    254   defp prewalk(node) do
    255     node
    256   end
    257 
    258   # Convert variables to proper names
    259   defp postwalk({:&, _, [ix]}, names, part) do
    260     binding_to_expr(ix, names, part)
    261   end
    262 
    263   # Remove parens from field calls
    264   defp postwalk({{:., _, [_, _]} = dot, meta, []}, _names, _part) do
    265     {dot, [no_parens: true] ++ meta, []}
    266   end
    267 
    268   # Interpolated unknown value
    269   defp postwalk({:^, _, [_ix, _len]}, _names, _part) do
    270     {:^, [], [{:..., [], nil}]}
    271   end
    272 
    273   # Interpolated known value
    274   defp postwalk({:^, _, [ix]}, _, %{params: params}) do
    275     value =
    276       case Enum.at(params || [], ix) do
    277         # Wrap the head in a block so it is not treated as a charlist
    278         {[head | tail], _type} -> [{:__block__, [], [head]} | tail]
    279         {value, _type} -> value
    280         _ -> {:..., [], nil}
    281       end
    282 
    283     {:^, [], [value]}
    284   end
    285 
    286   # Types need to be converted back to AST for fields
    287   defp postwalk({:type, meta, [expr, type]}, names, part) do
    288     {:type, meta, [expr, type_to_expr(type, names, part)]}
    289   end
    290 
    291   # For keyword and interpolated fragments use normal escaping
    292   defp postwalk({:fragment, _, [{_, _} | _] = parts}, _names, _part) do
    293     {:fragment, [], unmerge_fragments(parts, "", [])}
    294   end
    295 
    296   # Subqueries
    297   defp postwalk({:subquery, i}, _names, %{subqueries: subqueries}) do
    298     {:subquery, [], [Enum.fetch!(subqueries, i).query]}
    299   end
    300 
    301   # Jason
    302   defp postwalk({:json_extract_path, _, [expr, path]}, _names, _part) do
    303     Enum.reduce(path, expr, fn element, acc ->
    304       {{:., [], [Access, :get]}, [], [acc, element]}
    305     end)
    306   end
    307 
    308   defp postwalk(node, _names, _part) do
    309     node
    310   end
    311 
    312   defp binding_to_expr(ix, names, part) do
    313     case part do
    314       %{take: %{^ix => {:any, fields}}} when ix == 0 ->
    315         fields
    316 
    317       %{take: %{^ix => {tag, fields}}} ->
    318         {tag, [], [binding(names, ix), fields]}
    319 
    320       _ ->
    321         binding(names, ix)
    322     end
    323   end
    324 
    325   defp type_to_expr({:parameterized, type, opts}, _names, _part) do
    326     {:{}, [], [:parameterized, type, opts]}
    327   end
    328 
    329   defp type_to_expr({ix, type}, names, part) when is_integer(ix) do
    330     {{:., [], [binding_to_expr(ix, names, part), type]}, [no_parens: true], []}
    331   end
    332 
    333   defp type_to_expr({composite, type}, names, part) when is_atom(composite) do
    334     {composite, type_to_expr(type, names, part)}
    335   end
    336 
    337   defp type_to_expr(type, _names, _part) do
    338     type
    339   end
    340 
    341   defp unmerge_fragments([{:raw, s}, {:expr, v} | t], frag, args) do
    342     unmerge_fragments(t, frag <> s <> "?", [v | args])
    343   end
    344 
    345   defp unmerge_fragments([{:raw, s}], frag, args) do
    346     [frag <> s | Enum.reverse(args)]
    347   end
    348 
    349   defp join_qual(:inner), do: :join
    350   defp join_qual(:inner_lateral), do: :join_lateral
    351   defp join_qual(:left), do: :left_join
    352   defp join_qual(:left_lateral), do: :left_join_lateral
    353   defp join_qual(:right), do: :right_join
    354   defp join_qual(:full), do: :full_join
    355   defp join_qual(:cross), do: :cross_join
    356 
    357   defp collect_sources(%{from: nil, joins: joins}) do
    358     ["query" | join_sources(joins)]
    359   end
    360 
    361   defp collect_sources(%{from: %{source: source}, joins: joins}) do
    362     [from_sources(source) | join_sources(joins)]
    363   end
    364 
    365   defp from_sources(%Ecto.SubQuery{query: query}), do: from_sources(query.from.source)
    366   defp from_sources({source, schema}), do: schema || source
    367   defp from_sources(nil), do: "query"
    368   defp from_sources({:fragment, _, _}), do: "fragment"
    369 
    370   defp join_sources(joins) do
    371     joins
    372     |> Enum.sort_by(& &1.ix)
    373     |> Enum.map(fn
    374       %JoinExpr{assoc: {_var, assoc}} ->
    375         assoc
    376 
    377       %JoinExpr{source: {:fragment, _, _}} ->
    378         "fragment"
    379 
    380       %JoinExpr{source: %Ecto.Query{from: from}} ->
    381         from_sources(from.source)
    382 
    383       %JoinExpr{source: source} ->
    384         from_sources(source)
    385     end)
    386   end
    387 
    388   defp generate_letters(sources) do
    389     Enum.map(sources, fn source ->
    390       source
    391       |> Kernel.to_string()
    392       |> normalize_source()
    393       |> String.first()
    394       |> String.downcase()
    395     end)
    396   end
    397 
    398   defp generate_names(letters) do
    399     {names, _} = Enum.map_reduce(letters, 0, &{:"#{&1}#{&2}", &2 + 1})
    400     names
    401   end
    402 
    403   defp binding(names, pos) do
    404     try do
    405       {elem(names, pos), [], nil}
    406     rescue
    407       ArgumentError -> {:"unknown_binding_#{pos}!", [], nil}
    408     end
    409   end
    410 
    411   defp normalize_source("Elixir." <> _ = source),
    412     do: source |> Module.split() |> List.last()
    413 
    414   defp normalize_source(source),
    415     do: source
    416 end