[SPARK-58165][SQL][CONNECT] Fix SubqueryExpression dropping nested subquery plans in IN values#57308
Closed
LuciferYang wants to merge 4 commits into
Closed
[SPARK-58165][SQL][CONNECT] Fix SubqueryExpression dropping nested subquery plans in IN values#57308LuciferYang wants to merge 4 commits into
LuciferYang wants to merge 4 commits into
Conversation
…bquery plans in IN values SubqueryExpression returned children = Seq.empty even for SubqueryType.IN(values), so ColumnNode.collect/foreach never descended into a nested subquery inside isin(ds). On Spark Connect this left the inner subquery's plan out of WithRelations while the proto converter still referenced its planId, failing with "Missing relation in WithRelations". Expose IN.values as children and override normalize() to mirror sibling nodes.
Contributor
Author
|
cc @cloud-fan |
cloud-fan
reviewed
Jul 18, 2026
cloud-fan
left a comment
Contributor
There was a problem hiding this comment.
0 blocking, 1 non-blocking, 0 nits.
The runtime fix is well-scoped; add direct coverage for the separately introduced normalization contract.
Correctness (1)
- sql/api/src/main/scala/org/apache/spark/sql/internal/columnNodes.scala:669: Add direct coverage for SubqueryExpression normalization -- see inline
| subqueryType: SubqueryType, | ||
| override val origin: Origin = CurrentOrigin.get) | ||
| extends ColumnNode { | ||
| override private[internal] def normalize(): SubqueryExpression = copy( |
Contributor
There was a problem hiding this comment.
Please add this node to the normalization contract coverage in ColumnNodeSuite. The new execution tests exercise children, but they do not verify origin stripping or recursive normalization of IN(values), so this separately introduced equality/hash behavior could regress unnoticed.
…ation coverage Adds direct coverage of the SubqueryExpression normalization contract to ColumnNodeSuite, addressing review feedback: the existing execution tests exercise `children` but not origin stripping or the recursive normalization of IN(values). Covers SCALAR, EXISTS, and IN(values).
uros-b
approved these changes
Jul 20, 2026
uros-b
left a comment
Member
There was a problem hiding this comment.
Thank you @LuciferYang and @cloud-fan!
…case into its own test Address review feedback on PR apache#57308: - Move the SPARK-58165 "scalar subquery nested in IN subquery values" case out of `test("IN predicate subquery")` into a dedicated test in both the Connect and classic `DataFrameSubquerySuite`, so the regression has a self-contained failure signal that can't be masked by an earlier checkAnswer in the same block. - Add a correlated variant where the nested scalar subquery references the outer row via `outer()`, broadening coverage beyond the non-correlated (constant) shape. No production code change; test-only.
…lated test The correlated variant added in the previous commit wrote the nested subquery as a single-line method chain, which fails the scalafmt check that `dev/lint-scala` enforces on the sql/connect modules. Break the chain across lines to satisfy scalafmt. Formatting only; no behavior change.
Contributor
Author
|
friendly ping @cloud-fan |
dongjoon-hyun
pushed a commit
that referenced
this pull request
Jul 22, 2026
…bquery plans in IN values
### What changes were proposed in this pull request?
`SubqueryExpression` (in `sql/api`'s `internal/columnNodes.scala`) was the only `ColumnNode` that returned `children = Seq.empty` even when its `SubqueryType` was `IN(values)`, whose `values: Seq[ColumnNode]` can hold nested column nodes -- including another `SubqueryExpression`. It also did not override `normalize()`, unlike every sibling node.
This PR makes `SubqueryExpression` consistent with the other nodes:
- `children` now exposes `IN.values` (and stays empty for `SCALAR`/`EXISTS`). The subquery plan `ds` is intentionally not a child -- it is a DataFrame plan, not a column node, and the converters handle it separately.
- `normalize()` is overridden to strip `origin` and normalize the `IN.values`, mirroring sibling nodes.
### Why are the changes needed?
A nested subquery is reachable through the public 4.1 API, e.g.
```scala
t1.select(max($"v")).scalar().isin(t2.select($"c"))
// WHERE (SELECT max(v) FROM t1) IN (SELECT c FROM t2)
```
On Spark Connect, `SparkSession.newDataset` collects subquery plan references via `node.collect { case _: SubqueryExpression => ... }`, which recurses through `children`. Because `children` was empty, the inner scalar subquery's plan was never added to `WithRelations`, while the proto converter still emitted a planId reference to it. The server then failed resolving that planId:
```
[CONNECT_INVALID_PLAN.ASSERTION_FAILURE] Missing relation in WithRelations: (N) not in (M)
```
Spark Classic is unaffected: its converter embeds the subquery's logical plan directly and does not rely on `children`. The missing `normalize()` override is a separate, lower-impact inconsistency: two Columns built from the same `ds` at different call sites compared unequal purely due to retained `origin`.
### Does this PR introduce _any_ user-facing change?
Yes. On Spark Connect, a nested subquery inside `isin(ds)` -- e.g. `df1.scalar().isin(df2)` -- previously failed with an internal `Missing relation in WithRelations` error. After this fix it works, matching Spark Classic and the equivalent SQL. This is a bug fix relative to released 4.1.x.
### How was this patch tested?
New cases in `DataFrameSubquerySuite` (`sql/core` Classic + `sql/connect/client/jvm` Connect) exercise a scalar subquery nested inside an IN subquery's values. Verified reproduce-then-fix: the Connect case fails with `Missing relation in WithRelations` on the unfixed tree and passes after the fix; both full suites pass with no regressions. Direct normalization coverage for `SubqueryExpression` (SCALAR / EXISTS / IN(values)) was added to `ColumnNodeSuite`, verifying origin stripping and recursive normalization of `IN(values)`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #57308 from LuciferYang/SPARK-subquery-columnnode.
Authored-by: YangJie <yangjie01@baidu.com>
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
(cherry picked from commit 5994d4f)
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
dongjoon-hyun
pushed a commit
that referenced
this pull request
Jul 22, 2026
…bquery plans in IN values
### What changes were proposed in this pull request?
`SubqueryExpression` (in `sql/api`'s `internal/columnNodes.scala`) was the only `ColumnNode` that returned `children = Seq.empty` even when its `SubqueryType` was `IN(values)`, whose `values: Seq[ColumnNode]` can hold nested column nodes -- including another `SubqueryExpression`. It also did not override `normalize()`, unlike every sibling node.
This PR makes `SubqueryExpression` consistent with the other nodes:
- `children` now exposes `IN.values` (and stays empty for `SCALAR`/`EXISTS`). The subquery plan `ds` is intentionally not a child -- it is a DataFrame plan, not a column node, and the converters handle it separately.
- `normalize()` is overridden to strip `origin` and normalize the `IN.values`, mirroring sibling nodes.
### Why are the changes needed?
A nested subquery is reachable through the public 4.1 API, e.g.
```scala
t1.select(max($"v")).scalar().isin(t2.select($"c"))
// WHERE (SELECT max(v) FROM t1) IN (SELECT c FROM t2)
```
On Spark Connect, `SparkSession.newDataset` collects subquery plan references via `node.collect { case _: SubqueryExpression => ... }`, which recurses through `children`. Because `children` was empty, the inner scalar subquery's plan was never added to `WithRelations`, while the proto converter still emitted a planId reference to it. The server then failed resolving that planId:
```
[CONNECT_INVALID_PLAN.ASSERTION_FAILURE] Missing relation in WithRelations: (N) not in (M)
```
Spark Classic is unaffected: its converter embeds the subquery's logical plan directly and does not rely on `children`. The missing `normalize()` override is a separate, lower-impact inconsistency: two Columns built from the same `ds` at different call sites compared unequal purely due to retained `origin`.
### Does this PR introduce _any_ user-facing change?
Yes. On Spark Connect, a nested subquery inside `isin(ds)` -- e.g. `df1.scalar().isin(df2)` -- previously failed with an internal `Missing relation in WithRelations` error. After this fix it works, matching Spark Classic and the equivalent SQL. This is a bug fix relative to released 4.1.x.
### How was this patch tested?
New cases in `DataFrameSubquerySuite` (`sql/core` Classic + `sql/connect/client/jvm` Connect) exercise a scalar subquery nested inside an IN subquery's values. Verified reproduce-then-fix: the Connect case fails with `Missing relation in WithRelations` on the unfixed tree and passes after the fix; both full suites pass with no regressions. Direct normalization coverage for `SubqueryExpression` (SCALAR / EXISTS / IN(values)) was added to `ColumnNodeSuite`, verifying origin stripping and recursive normalization of `IN(values)`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #57308 from LuciferYang/SPARK-subquery-columnnode.
Authored-by: YangJie <yangjie01@baidu.com>
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
(cherry picked from commit 5994d4f)
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
dongjoon-hyun
pushed a commit
that referenced
this pull request
Jul 22, 2026
…bquery plans in IN values
### What changes were proposed in this pull request?
`SubqueryExpression` (in `sql/api`'s `internal/columnNodes.scala`) was the only `ColumnNode` that returned `children = Seq.empty` even when its `SubqueryType` was `IN(values)`, whose `values: Seq[ColumnNode]` can hold nested column nodes -- including another `SubqueryExpression`. It also did not override `normalize()`, unlike every sibling node.
This PR makes `SubqueryExpression` consistent with the other nodes:
- `children` now exposes `IN.values` (and stays empty for `SCALAR`/`EXISTS`). The subquery plan `ds` is intentionally not a child -- it is a DataFrame plan, not a column node, and the converters handle it separately.
- `normalize()` is overridden to strip `origin` and normalize the `IN.values`, mirroring sibling nodes.
### Why are the changes needed?
A nested subquery is reachable through the public 4.1 API, e.g.
```scala
t1.select(max($"v")).scalar().isin(t2.select($"c"))
// WHERE (SELECT max(v) FROM t1) IN (SELECT c FROM t2)
```
On Spark Connect, `SparkSession.newDataset` collects subquery plan references via `node.collect { case _: SubqueryExpression => ... }`, which recurses through `children`. Because `children` was empty, the inner scalar subquery's plan was never added to `WithRelations`, while the proto converter still emitted a planId reference to it. The server then failed resolving that planId:
```
[CONNECT_INVALID_PLAN.ASSERTION_FAILURE] Missing relation in WithRelations: (N) not in (M)
```
Spark Classic is unaffected: its converter embeds the subquery's logical plan directly and does not rely on `children`. The missing `normalize()` override is a separate, lower-impact inconsistency: two Columns built from the same `ds` at different call sites compared unequal purely due to retained `origin`.
### Does this PR introduce _any_ user-facing change?
Yes. On Spark Connect, a nested subquery inside `isin(ds)` -- e.g. `df1.scalar().isin(df2)` -- previously failed with an internal `Missing relation in WithRelations` error. After this fix it works, matching Spark Classic and the equivalent SQL. This is a bug fix relative to released 4.1.x.
### How was this patch tested?
New cases in `DataFrameSubquerySuite` (`sql/core` Classic + `sql/connect/client/jvm` Connect) exercise a scalar subquery nested inside an IN subquery's values. Verified reproduce-then-fix: the Connect case fails with `Missing relation in WithRelations` on the unfixed tree and passes after the fix; both full suites pass with no regressions. Direct normalization coverage for `SubqueryExpression` (SCALAR / EXISTS / IN(values)) was added to `ColumnNodeSuite`, verifying origin stripping and recursive normalization of `IN(values)`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #57308 from LuciferYang/SPARK-subquery-columnnode.
Authored-by: YangJie <yangjie01@baidu.com>
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
(cherry picked from commit 5994d4f)
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
Member
Contributor
Author
|
Thank you @dongjoon-hyun @uros-b @cloud-fan |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
SubqueryExpression(insql/api'sinternal/columnNodes.scala) was the onlyColumnNodethat returnedchildren = Seq.emptyeven when itsSubqueryTypewasIN(values), whosevalues: Seq[ColumnNode]can hold nested column nodes -- including anotherSubqueryExpression. It also did not overridenormalize(), unlike every sibling node.This PR makes
SubqueryExpressionconsistent with the other nodes:childrennow exposesIN.values(and stays empty forSCALAR/EXISTS). The subquery plandsis intentionally not a child -- it is a DataFrame plan, not a column node, and the converters handle it separately.normalize()is overridden to striporiginand normalize theIN.values, mirroring sibling nodes.Why are the changes needed?
A nested subquery is reachable through the public 4.1 API, e.g.
On Spark Connect,
SparkSession.newDatasetcollects subquery plan references vianode.collect { case _: SubqueryExpression => ... }, which recurses throughchildren. Becausechildrenwas empty, the inner scalar subquery's plan was never added toWithRelations, while the proto converter still emitted a planId reference to it. The server then failed resolving that planId:Spark Classic is unaffected: its converter embeds the subquery's logical plan directly and does not rely on
children. The missingnormalize()override is a separate, lower-impact inconsistency: two Columns built from the samedsat different call sites compared unequal purely due to retainedorigin.Does this PR introduce any user-facing change?
Yes. On Spark Connect, a nested subquery inside
isin(ds)-- e.g.df1.scalar().isin(df2)-- previously failed with an internalMissing relation in WithRelationserror. After this fix it works, matching Spark Classic and the equivalent SQL. This is a bug fix relative to released 4.1.x.How was this patch tested?
New cases in
DataFrameSubquerySuite(sql/coreClassic +sql/connect/client/jvmConnect) exercise a scalar subquery nested inside an IN subquery's values. Verified reproduce-then-fix: the Connect case fails withMissing relation in WithRelationson the unfixed tree and passes after the fix; both full suites pass with no regressions. Direct normalization coverage forSubqueryExpression(SCALAR / EXISTS / IN(values)) was added toColumnNodeSuite, verifying origin stripping and recursive normalization ofIN(values).Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)