Skip to content

Commit 96f70b5

Browse files
committed
Optimize json_extract_path comparisons in PostgreSQL
1 parent f882429 commit 96f70b5

File tree

2 files changed

+49
-15
lines changed

2 files changed

+49
-15
lines changed

lib/ecto/adapters/postgres/connection.ex

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ if Code.ensure_loaded?(Postgrex) do
7070
case Postgrex.prepare_execute(conn, name, sql, params, opts) do
7171
{:error, %Postgrex.Error{postgres: %{pg_code: "22P02", message: message}} = error} ->
7272
context = """
73-
. If you are trying to query a JSON field, the parameter must be interpolated. Instead of
73+
. If you are trying to query a JSON field, the parameter may need to be interpolated. \
74+
Instead of
7475
7576
p.json["field"] == "value"
7677
@@ -684,16 +685,7 @@ if Code.ensure_loaded?(Postgrex) do
684685
end
685686

686687
defp expr({:json_extract_path, _, [expr, path]}, sources, query) do
687-
path =
688-
intersperse_map(path, ?,, fn
689-
binary when is_binary(binary) ->
690-
[?", escape_json_key(binary), ?"]
691-
692-
integer when is_integer(integer) ->
693-
Integer.to_string(integer)
694-
end)
695-
696-
[?(, expr(expr, sources, query), "#>'{", path, "}')"]
688+
json_extract_path(expr, path, sources, query)
697689
end
698690

699691
defp expr({:filter, _, [agg, filter]}, sources, query) do
@@ -717,6 +709,18 @@ if Code.ensure_loaded?(Postgrex) do
717709

718710
defp expr({:count, _, []}, _sources, _query), do: "count(*)"
719711

712+
defp expr({:==, _, [{:json_extract_path, _, [expr, path]} = left, right]}, sources, query)
713+
when is_binary(right) or is_integer(right) do
714+
case Enum.split(path, -1) do
715+
{path, [last]} when is_binary(last) ->
716+
extracted = json_extract_path(expr, path, sources, query)
717+
[?(, extracted, "@>'{", escape_json(last), ": ", escape_json(right) | "}')"]
718+
719+
_ ->
720+
[maybe_paren(left, sources, query), " = " | maybe_paren(right, sources, query)]
721+
end
722+
end
723+
720724
defp expr({fun, _, args}, sources, query) when is_atom(fun) and is_list(args) do
721725
{modifier, args} =
722726
case args do
@@ -770,6 +774,15 @@ if Code.ensure_loaded?(Postgrex) do
770774
error!(query, "unsupported expression: #{inspect(expr)}")
771775
end
772776

777+
defp json_extract_path(expr, [], sources, query) do
778+
expr(expr, sources, query)
779+
end
780+
781+
defp json_extract_path(expr, path, sources, query) do
782+
path = intersperse_map(path, ?,, &escape_json/1)
783+
[?(, expr(expr, sources, query), "#>'{", path, "}')"]
784+
end
785+
773786
defp type_unless_typed(%Ecto.Query.Tagged{}, _type), do: []
774787
defp type_unless_typed(_, type), do: [?:, ?: | type]
775788

@@ -1328,10 +1341,17 @@ if Code.ensure_loaded?(Postgrex) do
13281341
:binary.replace(value, "'", "''", [:global])
13291342
end
13301343

1331-
defp escape_json_key(value) when is_binary(value) do
1332-
value
1333-
|> escape_string()
1334-
|> :binary.replace("\"", "\\\"", [:global])
1344+
defp escape_json(value) when is_binary(value) do
1345+
escaped =
1346+
value
1347+
|> escape_string()
1348+
|> :binary.replace("\"", "\\\"", [:global])
1349+
1350+
[?", escaped, ?"]
1351+
end
1352+
1353+
defp escape_json(value) when is_integer(value) do
1354+
Integer.to_string(value)
13351355
end
13361356

13371357
defp ecto_to_db({:array, t}), do: [ecto_to_db(t), ?[, ?]]

test/ecto/adapters/postgres_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,20 @@ defmodule Ecto.Adapters.PostgresTest do
605605
assert all(query) == ~s|SELECT (s0.\"meta\"#>'{"\\"a"}') FROM "schema" AS s0|
606606
end
607607

608+
test "optimized json_extract_path" do
609+
query = Schema |> where([s], s.meta["id"] == 123) |> select(true) |> plan()
610+
assert all(query) == ~s|SELECT TRUE FROM \"schema\" AS s0 WHERE ((s0."meta"@>'{"id": 123}'))|
611+
612+
query = Schema |> where([s], s.meta["id"] == "123") |> select(true) |> plan()
613+
assert all(query) == ~s|SELECT TRUE FROM \"schema\" AS s0 WHERE ((s0."meta"@>'{"id": "123"}'))|
614+
615+
query = Schema |> where([s], s.meta["tags"][0]["name"] == "123") |> select(true) |> plan()
616+
assert all(query) == ~s|SELECT TRUE FROM \"schema\" AS s0 WHERE (((s0."meta"#>'{"tags",0}')@>'{"name": "123"}'))|
617+
618+
query = Schema |> where([s], s.meta[0] == "123") |> select(true) |> plan()
619+
assert all(query) == ~s|SELECT TRUE FROM \"schema\" AS s0 WHERE ((s0.\"meta\"#>'{0}') = '123')|
620+
end
621+
608622
test "nested expressions" do
609623
z = 123
610624
query = from(r in Schema, []) |> select([r], r.x > 0 and (r.y > ^(-z)) or true) |> plan()

0 commit comments

Comments
 (0)