Repository: elixir-ecto/ecto
Pull request #4195 (2023-05-27 02:40:18)

Fix late binding with json_extract_path
Closes https://github.com/elixir-ecto/ecto/issues/4194


Patch:
diff --git a/lib/ecto/query/builder.ex b/lib/ecto/query/builder.ex
index b615ad6a..73adc906 100644
--- a/lib/ecto/query/builder.ex
+++ b/lib/ecto/query/builder.ex
@@ -1338,4 +1338,9 @@ defmodule Ecto.Query.Builder do
        when is_atom(var) and is_atom(context) and is_atom(field) do
     {expr, acc}
   end
+
+  defp parse_access_get({{:., _, [{kind, _, [_]}, field]}, _, []} = expr, acc)
+       when kind in [:as, :parent_as] and is_atom(field) do
+    {expr, acc}
+  end
 end
diff --git a/lib/ecto/query/planner.ex b/lib/ecto/query/planner.ex
index b7f5284e..519b8794 100644
--- a/lib/ecto/query/planner.ex
+++ b/lib/ecto/query/planner.ex
@@ -1135,7 +1135,7 @@ defmodule Ecto.Query.Planner do
 
       {:map, _} ->
         :ok
-      
+
       {:parameterized, type, _} ->
         validate_json_path!(path, field, type)
 
@@ -1267,14 +1267,15 @@ defmodule Ecto.Query.Planner do
   end
 
   defp prewalk({:json_extract_path, meta, [json_field, path]}, kind, query, expr, acc, _adapter) do
-    {{:., dot_meta, [{:&, amp_meta, [ix]}, field]}, expr_meta, []} = json_field
+    {{:., dot_meta, [left, field]}, expr_meta, []} = json_field
+    {ix, ix_expr, ix_query} = get_ix!(left, kind, query)
 
-    type = type!(kind, query, expr, ix, field)
+    type = type!(kind, ix_query, expr, ix, field)
     validate_json_path!(path, field, type)
 
-    field_source = kind |> get_source!(query, ix) |> field_source(field)
+    field_source = kind |> get_source!(ix_query, ix) |> field_source(field)
 
-    json_field = {{:., dot_meta, [{:&, amp_meta, [ix]}, field_source]}, expr_meta, []}
+    json_field = {{:., dot_meta, [ix_expr, field_source]}, expr_meta, []}
     {{:json_extract_path, meta, [json_field, path]}, acc}
   end
 


Test patch:
diff --git a/test/ecto/query/builder_test.exs b/test/ecto/query/builder_test.exs
index 52aad030..29187aab 100644
--- a/test/ecto/query/builder_test.exs
+++ b/test/ecto/query/builder_test.exs
@@ -60,6 +60,22 @@ defmodule Ecto.Query.BuilderTest do
     actual = escape(quote do x.y["a"]["b"] end, [x: 0], __ENV__)
     assert actual == expected
 
+    expected = {Macro.escape(quote do: json_extract_path(as(:x).y(), ["a", "b"])), []}
+    actual = escape(quote do json_extract_path(as(:x).y, ["a", "b"]) end, [], __ENV__)
+    assert actual == expected
+
+    expected = {Macro.escape(quote do: json_extract_path(parent_as(:x).y(), ["a", "b"])), []}
+    actual = escape(quote do json_extract_path(parent_as(:x).y, ["a", "b"]) end, [], __ENV__)
+    assert actual == expected
+
+    expected = {Macro.escape(quote do: json_extract_path(as(:x).y(), ["a", "b"])), []}
+    actual = escape(quote do as(:x).y["a"]["b"] end, [], __ENV__)
+    assert actual == expected
+
+    expected = {Macro.escape(quote do: json_extract_path(parent_as(:x).y(), ["a", "b"])), []}
+    actual = escape(quote do parent_as(:x).y["a"]["b"] end, [], __ENV__)
+    assert actual == expected
+
     expected = {Macro.escape(quote do: json_extract_path(&0.y(), ["a", 0])), []}
     actual = escape(quote do x.y["a"][0] end, [x: 0], __ENV__)
     assert actual == expected
diff --git a/test/ecto/query/planner_test.exs b/test/ecto/query/planner_test.exs
index b5fb3257..139e4145 100644
--- a/test/ecto/query/planner_test.exs
+++ b/test/ecto/query/planner_test.exs
@@ -99,9 +99,9 @@ defmodule Ecto.Query.PlannerTest do
     def cast(data, _) do
       {:ok, data}
     end
-    
+
     def dump(data, _, _), do: {:ok, data}
-    
+
     def load(data, _, _), do: {:ok, data}
   end
 
@@ -1273,6 +1273,48 @@ defmodule Ecto.Query.PlannerTest do
              "where: c0.custom_map[\"field\"] == \"value\", select: [c0.custom_map[\"field\"], c0.custom_embed[\"nested_custom_map\"][\"field\"]]>"
   end
 
+  test "normalize: json_extract_path with as/1" do
+    normalized_query =
+      Comment
+      |> join(:inner, [_], p in Post, as: :post, on: true)
+      |> where([p], as(:post).prefs["field"] == "value")
+      |> normalize()
+
+    assert inspect(normalized_query) =~ "where: p1.preferences[\"field\"] == \"value\""
+
+    normalized_query =
+      Comment
+      |> join(:inner, [_], p in Post, as: :post, on: true)
+      |> where([p], json_extract_path(as(:post).prefs, ["field"]) == "value")
+      |> normalize()
+
+    assert inspect(normalized_query) =~ "where: p1.preferences[\"field\"] == \"value\""
+  end
+
+  test "normalize: json_extract_path with parent_as/1" do
+    subquery =
+      Comment
+      |> where([c], c.post_id == parent_as(:post).prefs["field"])
+
+    normalized_query =
+      from(Post, as: :post)
+      |> join(:inner, [_], s in subquery(subquery), on: true)
+      |> normalize()
+
+    assert inspect(normalized_query) =~ "where: c0.post_id == parent_as(:post).preferences[\"field\"]"
+
+    subquery =
+      Comment
+      |> where([c], c.post_id == json_extract_path(parent_as(:post).prefs, ["field"]))
+
+    normalized_query =
+      from(Post, as: :post)
+      |> join(:inner, [_], s in subquery(subquery), on: true)
+      |> normalize()
+
+    assert inspect(normalized_query) =~ "where: c0.post_id == parent_as(:post).preferences[\"field\"]"
+  end
+
   test "normalize: flattens and expands right side of in expressions" do
     {query, cast_params, dump_params, _select} = where(Post, [p], p.id in [1, 2, 3]) |> normalize_with_params()
     assert Macro.to_string(hd(query.wheres).expr) == "&0.id() in [1, 2, 3]"