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