Skip to content

Commit bec6021

Browse files
authored
Support json_extract_path/2 (elixir-ecto#187)
1 parent 297c166 commit bec6021

File tree

8 files changed

+87
-10
lines changed

8 files changed

+87
-10
lines changed

integration_test/tds/test_helper.exs

+4-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ ExUnit.start(
5353
# and after insert we need to turn it on, must be run manually in transaction
5454
:pk_insert,
5555
# Tds allows nested transactions so this will never raise and SQL query should be "BEGIN TRAN"
56-
:transaction_checkout_raises
56+
:transaction_checkout_raises,
57+
# JSON_VALUE always returns strings (even for e.g. integers) and returns null for
58+
# arrays/objects (JSON_QUERY must be used for these)
59+
:json_extract_path
5760
]
5861
)
5962

lib/ecto/adapters/myxql/connection.ex

+19
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,19 @@ if Code.ensure_loaded?(MyXQL) do
547547

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

550+
defp expr({:json_extract_path, _, [expr, path]}, sources, query) do
551+
path =
552+
Enum.map(path, fn
553+
binary when is_binary(binary) ->
554+
[?., ?", escape_json_key(binary), ?"]
555+
556+
integer when is_integer(integer) ->
557+
"[#{integer}]"
558+
end)
559+
560+
["json_extract(", expr(expr, sources, query), ", '$", path, "')"]
561+
end
562+
550563
defp expr({fun, _, args}, sources, query) when is_atom(fun) and is_list(args) do
551564
{modifier, args} =
552565
case args do
@@ -975,6 +988,12 @@ if Code.ensure_loaded?(MyXQL) do
975988
|> :binary.replace("\\", "\\\\", [:global])
976989
end
977990

991+
defp escape_json_key(value) when is_binary(value) do
992+
value
993+
|> escape_string()
994+
|> :binary.replace("\"", "\\\\\"", [:global])
995+
end
996+
978997
defp ecto_cast_to_db(:id, _query), do: "unsigned"
979998
defp ecto_cast_to_db(:integer, _query), do: "unsigned"
980999
defp ecto_cast_to_db(:string, _query), do: "char"

lib/ecto/adapters/postgres/connection.ex

+19
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,19 @@ if Code.ensure_loaded?(Postgrex) do
570570
interval(count, interval, sources, query) | ")::date"]
571571
end
572572

573+
defp expr({:json_extract_path, _, [expr, path]}, sources, query) do
574+
path =
575+
intersperse_map(path, ?,, fn
576+
binary when is_binary(binary) ->
577+
[?", escape_json_key(binary), ?"]
578+
579+
integer when is_integer(integer) ->
580+
Integer.to_string(integer)
581+
end)
582+
583+
[?(, expr(expr, sources, query), "#>'{", path, "}')"]
584+
end
585+
573586
defp expr({:filter, _, [agg, filter]}, sources, query) do
574587
aggregate = expr(agg, sources, query)
575588
[aggregate, " FILTER (WHERE ", expr(filter, sources, query), ?)]
@@ -1148,6 +1161,12 @@ if Code.ensure_loaded?(Postgrex) do
11481161
:binary.replace(value, "'", "''", [:global])
11491162
end
11501163

1164+
defp escape_json_key(value) when is_binary(value) do
1165+
value
1166+
|> escape_string()
1167+
|> :binary.replace("\"", "\\\"", [:global])
1168+
end
1169+
11511170
defp ecto_to_db({:array, t}), do: [ecto_to_db(t), ?[, ?]]
11521171
defp ecto_to_db(:id), do: "integer"
11531172
defp ecto_to_db(:serial), do: "serial"

lib/ecto/adapters/tds/connection.ex

+15-7
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ if Code.ensure_loaded?(Tds) do
213213
end
214214

215215
defp on_conflict({_, _, _}, _header) do
216-
error!(nil, "MSSQL supports only on_conflict: :raise")
216+
error!(nil, "Tds adapter supports only on_conflict: :raise")
217217
end
218218

219219
defp insert_all(rows, counter) do
@@ -383,7 +383,7 @@ if Code.ensure_loaded?(Tds) do
383383
defp cte_header(%QueryExpr{}, query) do
384384
error!(
385385
query,
386-
"Unfortunately Tds adapter does not support fragment in CTE"
386+
"Tds adapter does not support fragment in CTE"
387387
)
388388
end
389389

@@ -704,6 +704,14 @@ if Code.ensure_loaded?(Tds) do
704704

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

707+
defp expr({:json_extract_path, _, _}, _sources, query) do
708+
error!(
709+
query,
710+
"Tds adapter does not support json_extract_path expression" <>
711+
", use fragment with JSON_VALUE/JSON_QUERY"
712+
)
713+
end
714+
707715
defp expr({fun, _, args}, sources, query) when is_atom(fun) and is_list(args) do
708716
{modifier, args} =
709717
case args do
@@ -997,10 +1005,10 @@ if Code.ensure_loaded?(Tds) do
9971005
end
9981006

9991007
def execute_ddl({:create, %Constraint{check: check}}) when is_binary(check),
1000-
do: error!(nil, "MSSQL adapter does not support check constraints")
1008+
do: error!(nil, "Tds adapter does not support check constraints")
10011009

10021010
def execute_ddl({:create, %Constraint{exclude: exclude}}) when is_binary(exclude),
1003-
do: error!(nil, "MSSQL adapter does not support exclusion constraints")
1011+
do: error!(nil, "Tds adapter does not support exclusion constraints")
10041012

10051013
def execute_ddl({command, %Index{} = index}) when command in [:drop, :drop_if_exists] do
10061014
prefix = index.prefix
@@ -1023,7 +1031,7 @@ if Code.ensure_loaded?(Tds) do
10231031
end
10241032

10251033
def execute_ddl({:drop, %Constraint{}}),
1026-
do: error!(nil, "MSSQL adapter does not support constraints")
1034+
do: error!(nil, "Tds adapter does not support constraints")
10271035

10281036
def execute_ddl({:rename, %Table{} = current_table, %Table{} = new_table}) do
10291037
[
@@ -1052,7 +1060,7 @@ if Code.ensure_loaded?(Tds) do
10521060
def execute_ddl(string) when is_binary(string), do: [string]
10531061

10541062
def execute_ddl(keyword) when is_list(keyword),
1055-
do: error!(nil, "MSSQL adapter does not support keyword lists in execute")
1063+
do: error!(nil, "Tds adapter does not support keyword lists in execute")
10561064

10571065
@impl true
10581066
def ddl_logs(_), do: []
@@ -1244,7 +1252,7 @@ if Code.ensure_loaded?(Tds) do
12441252
defp options_expr(nil), do: []
12451253

12461254
defp options_expr(keyword) when is_list(keyword),
1247-
do: error!(nil, "MSSQL adapter does not support keyword lists in :options")
1255+
do: error!(nil, "Tds adapter does not support keyword lists in :options")
12481256

12491257
defp options_expr(options), do: [" ", to_string(options)]
12501258

mix.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
77
"deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm", "e3bf435a54ed27b0ba3a01eb117ae017988804e136edcbe8a6a14c310daa966e"},
88
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
9-
"ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "e5908c0ed7b988c8c230502d31e278a85f3d2a68", []},
9+
"ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "cb2f03964dac324a9771f84f5088e395326e6de8", []},
1010
"ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
1111
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
1212
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},

test/ecto/adapters/myxql_test.exs

+14
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,20 @@ defmodule Ecto.Adapters.MyXQLTest do
492492
assert all(query) == ~s{SELECT CAST(? AS char) FROM `schema` AS s0}
493493
end
494494

495+
test "json_extract_path" do
496+
query = Schema |> select([r], json_extract_path(r, [0, 1])) |> plan()
497+
assert all(query) == ~s{SELECT json_extract(s0, '$[0][1]') FROM `schema` AS s0}
498+
499+
query = Schema |> select([r], json_extract_path(r, ["a", "b"])) |> plan()
500+
assert all(query) == ~s{SELECT json_extract(s0, '$."a"."b"') FROM `schema` AS s0}
501+
502+
query = Schema |> select([r], json_extract_path(r, ["'a"])) |> plan()
503+
assert all(query) == ~s{SELECT json_extract(s0, '$."''a"') FROM `schema` AS s0}
504+
505+
query = Schema |> select([r], json_extract_path(r, ["\"a"])) |> plan()
506+
assert all(query) == ~s{SELECT json_extract(s0, '$."\\\\"a"') FROM `schema` AS s0}
507+
end
508+
495509
test "nested expressions" do
496510
z = 123
497511
query = from(r in Schema, []) |> select([r], r.x > 0 and (r.y > ^(-z)) or true) |> plan()

test/ecto/adapters/postgres_test.exs

+14
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,20 @@ defmodule Ecto.Adapters.PostgresTest do
532532
assert all(query) == ~s{SELECT $1::uuid[] FROM "schema" AS s0}
533533
end
534534

535+
test "json_extract_path" do
536+
query = Schema |> select([r], json_extract_path(r, [0, 1])) |> plan()
537+
assert all(query) == ~s|SELECT (s0#>'{0,1}') FROM "schema" AS s0|
538+
539+
query = Schema |> select([r], json_extract_path(r, ["a", "b"])) |> plan()
540+
assert all(query) == ~s|SELECT (s0#>'{"a","b"}') FROM "schema" AS s0|
541+
542+
query = Schema |> select([r], json_extract_path(r, ["'a"])) |> plan()
543+
assert all(query) == ~s|SELECT (s0#>'{"''a"}') FROM "schema" AS s0|
544+
545+
query = Schema |> select([r], json_extract_path(r, ["\"a"])) |> plan()
546+
assert all(query) == ~s|SELECT (s0#>'{"\\"a"}') FROM "schema" AS s0|
547+
end
548+
535549
test "nested expressions" do
536550
z = 123
537551
query = from(r in Schema, []) |> select([r], r.x > 0 and (r.y > ^(-z)) or true) |> plan()

test/ecto/adapters/tds_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ defmodule Ecto.Adapters.TdsTest do
204204
|> select([r], r.x)
205205
|> plan()
206206

207-
assert_raise Ecto.QueryError, ~r"Unfortunately Tds adapter does not support fragment in CTE", fn ->
207+
assert_raise Ecto.QueryError, ~r"Tds adapter does not support fragment in CTE", fn ->
208208
all(query)
209209
end
210210
end

0 commit comments

Comments
 (0)