-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Fix subqueries in interpolated join wheres #4765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a934a88
c044796
203d049
62b8533
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,11 +27,11 @@ defmodule Ecto.Query.Planner do | |
| in order to keep proper binding order. | ||
| """ | ||
| def query_to_joins(qual, source, %{wheres: wheres, joins: joins}, position) do | ||
| on = %QueryExpr{file: __ENV__.file, line: __ENV__.line, expr: true, params: []} | ||
| on = %BooleanExpr{op: :and, file: __ENV__.file, line: __ENV__.line, expr: true, params: []} | ||
|
|
||
| on = | ||
| Enum.reduce(wheres, on, fn %BooleanExpr{op: op, expr: expr, params: params}, acc -> | ||
| merge_expr_and_params(op, acc, expr, params) | ||
| Enum.reduce(wheres, on, fn %BooleanExpr{op: op} = expr, acc -> | ||
| merge_expr_and_params(op, acc, expr) | ||
| end) | ||
|
|
||
| join = %JoinExpr{qual: qual, source: source, file: __ENV__.file, line: __ENV__.line, on: on} | ||
|
|
@@ -47,14 +47,73 @@ defmodule Ecto.Query.Planner do | |
| end | ||
| end | ||
|
|
||
| defp merge_expr_and_params( | ||
| op, | ||
| %BooleanExpr{expr: left_expr, params: left_params, subqueries: left_subqueries} = struct, | ||
| %{expr: right_expr, params: right_params} = right | ||
| ) do | ||
| # The right side may be a QueryExpr (an explicit join `on`), | ||
| # which holds no subqueries | ||
| right_subqueries = Map.get(right, :subqueries, []) | ||
|
|
||
| merge_expr_and_params( | ||
| op, | ||
| struct, | ||
| left_expr, | ||
| left_params, | ||
| left_subqueries, | ||
| right_expr, | ||
| right_params, | ||
| right_subqueries | ||
| ) | ||
| end | ||
|
|
||
| defp merge_expr_and_params( | ||
| op, | ||
| %QueryExpr{expr: left_expr, params: left_params} = struct, | ||
| %{expr: right_expr, params: right_params} = right | ||
| ) do | ||
| # A QueryExpr cannot hold subqueries, so none may come from the right side | ||
| [] = Map.get(right, :subqueries, []) | ||
| merge_expr_and_params(op, struct, left_expr, left_params, [], right_expr, right_params, []) | ||
| end | ||
|
|
||
| defp merge_expr_and_params( | ||
| op, | ||
| struct, | ||
| left_expr, | ||
| left_params, | ||
| left_subqueries, | ||
| right_expr, | ||
| right_params | ||
| right_params, | ||
| right_subqueries | ||
| ) do | ||
| right_expr = Ecto.Query.Builder.bump_interpolations(right_expr, left_params) | ||
| %{struct | expr: merge_expr(op, left_expr, right_expr), params: left_params ++ right_params} | ||
| right_expr = | ||
| right_expr | ||
| |> Ecto.Query.Builder.bump_interpolations(left_params) | ||
| |> Ecto.Query.Builder.bump_subqueries(left_subqueries) | ||
|
|
||
| right_params = bump_subquery_params(right_params, left_subqueries) | ||
|
|
||
| struct = %{ | ||
| struct | ||
| | expr: merge_expr(op, left_expr, right_expr), | ||
| params: left_params ++ right_params | ||
| } | ||
|
|
||
| case left_subqueries ++ right_subqueries do | ||
| [] -> struct | ||
| subqueries -> %{struct | subqueries: subqueries} | ||
| end | ||
| end | ||
|
|
||
| defp bump_subquery_params(params, subqueries) do | ||
| len = length(subqueries) | ||
|
|
||
| Enum.map(params, fn | ||
| {:subquery, counter} -> {:subquery, len + counter} | ||
| other -> other | ||
| end) | ||
| end | ||
|
|
||
| defp merge_expr(_op, left, true), do: left | ||
|
|
@@ -227,6 +286,7 @@ defmodule Ecto.Query.Planner do | |
|
|
||
| query | ||
| |> plan_assocs() | ||
| |> plan_join_subqueries(plan_subquery) | ||
| |> plan_combinations(adapter, cte_names) | ||
| |> plan_expr_subqueries(:wheres, plan_subquery) | ||
| |> plan_expr_subqueries(:havings, plan_subquery) | ||
|
|
@@ -746,8 +806,8 @@ defmodule Ecto.Query.Planner do | |
| {joins, sources, tail_sources} | ||
| end | ||
|
|
||
| defp attach_on([%{on: on} = h | t], %{expr: expr, params: params}) do | ||
| [%{h | on: merge_expr_and_params(:and, on, expr, params)} | t] | ||
| defp attach_on([%{on: on} = h | t], expr) do | ||
| [%{h | on: merge_expr_and_params(:and, on, expr)} | t] | ||
| end | ||
|
|
||
| defp rewrite_prefix(expr, nil), do: expr | ||
|
|
@@ -879,6 +939,19 @@ defmodule Ecto.Query.Planner do | |
| query | ||
| end | ||
|
|
||
| defp plan_join_subqueries(query, fun) do | ||
| joins = | ||
| Enum.map(query.joins, fn | ||
| %{on: %BooleanExpr{subqueries: [_ | _] = subqueries} = on} = join -> | ||
| %{join | on: %{on | subqueries: Enum.map(subqueries, fun)}} | ||
|
|
||
| join -> | ||
| join | ||
| end) | ||
|
|
||
| %{query | joins: joins} | ||
| end | ||
|
|
||
| defp plan_expr_subquery(query, key, fun) do | ||
| with %{^key => %{subqueries: [_ | _] = subqueries} = expr} <- query do | ||
| %{query | key => %{expr | subqueries: Enum.map(subqueries, fun)}} | ||
|
|
@@ -952,7 +1025,7 @@ defmodule Ecto.Query.Planner do | |
| {params, join_cacheable?} = cast_and_merge_params(:join, query, join, params, adapter) | ||
| {params, on_cacheable?} = cast_and_merge_params(:join, query, on, params, adapter) | ||
|
|
||
| {{qual, key, on.expr, hints}, | ||
| {{qual, key, expr_to_cache(on), hints}, | ||
| {params, cacheable? and join_cacheable? and on_cacheable? and key != :nocache}} | ||
| end) | ||
|
|
||
|
|
@@ -1422,7 +1495,7 @@ defmodule Ecto.Query.Planner do | |
| Enum.map_reduce(exprs, counter, fn join, acc -> | ||
| {source, acc} = prewalk_source(join.source, :join, query, join, acc, adapter) | ||
| {on, acc} = prewalk(:join, query, join.on, acc, adapter) | ||
| {%{join | on: on, source: source, params: nil}, acc} | ||
| {%{join | on: on_to_query_expr(on), source: source, params: nil}, acc} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to rollback? Let's keep it as boolean everywhere... if necessary, we change ecto_sql.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently ecto_sql pattern matches on QueryExpr. Relaxing that is doable. I created a draft PR. The tests pass but I'm not convinced this is the right way. Providers copy the code patterns from ecto and will remain with silent failures (for expressions will filter out %BooleanExpr{}) if ecto_sql is loosly pinned. Besides, after this change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Correct. That's what we should do. We already did this change a while ago to support subqueries in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that what you had in mind? |
||
| end) | ||
| end | ||
|
|
||
|
|
@@ -1453,6 +1526,15 @@ defmodule Ecto.Query.Planner do | |
| {Enum.reverse(combinations), counter} | ||
| end | ||
|
|
||
| # Interpolated join queries carry their `on` as a BooleanExpr during | ||
| # planning, as it may hold subqueries. Once subqueries are inlined by | ||
| # prewalk, convert it back to the QueryExpr adapters expect. | ||
| defp on_to_query_expr(%BooleanExpr{expr: expr, file: file, line: line, params: params}) do | ||
| %QueryExpr{expr: expr, file: file, line: line, params: params} | ||
| end | ||
|
|
||
| defp on_to_query_expr(on), do: on | ||
|
|
||
| defp validate_json_path!([path_field | rest], field, {:parameterized, {Ecto.Embedded, embed}}) | ||
| when is_binary(path_field) or is_integer(path_field) do | ||
| case embed do | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the right-side be a BooleanExpr as well always?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done