Repository navigation
[SPARK-58383][SQL] Bind the right filter by expression id in ReplaceExceptWithFilter - #58046
Vivek1106-04 wants to merge 2 commits into
Conversation
…xceptWithFilter ReplaceExceptWithFilter rewrites `left EXCEPT right` into `Distinct(Filter(NOT coalesce(cond, false), left))` when the right side is the same relation and projection as the left plus an extra filter. The filter condition references the base columns below the right projection, but it was remapped onto the left output by attribute name. When a left alias reuses the name of an unrelated base column, the injected predicate binds to the wrong column and the query returns wrong results. Map the condition through the base relations instead: the two bases are known to be sameResult, so their outputs correspond positionally, and the resulting left base attributes are translated into the left output through the left project list by expression id. Fall back to the anti-join when a reference cannot be traced.
…ss guard The guard added by the original SPARK-22181 commit required the left output names to be distinct, because the name-keyed condition map silently collapsed duplicate names. Now that the condition is translated by expression id, the collapse cannot happen and the guard only prevents the rewrite. Removing it lets the rule apply to a left side with duplicate output names, e.g. `SELECT id AS x, val AS x FROM t`, which previously always fell back to the anti-join. Results are unchanged.
|
+1, LGTM. |
|
I hit the same bug from a different direction and verified this PR covers it, so +1 to landing it.
Since this is a wrong-results fix that has been approved for a while, could a committer take a look? |
What changes were proposed in this pull request?
ReplaceExceptWithFilterrewritesleft EXCEPT rightintoDistinct(Filter(NOT coalesce(cond, false), left))when the right side is the same relation and projection as the left plus an additional filter.The filter condition references the base columns below the right projection, but
transformConditionremapped it ontoleft.outputby attribute name:When a left alias reuses the name of an unrelated base column, the injected predicate binds to the wrong column.
This PR maps the condition through the base relations instead:
sameResult(checked byverifyConditions), so their outputs correspond positionally — that gives right-base attribute -> left-base attribute.left.outputthrough the left project list by expression id, either directly or through an alias.This replaces the name-presence check added by SPARK-23274, and also lets the left-output-name-distinctness guard from the original SPARK-22181 commit go away: it existed because the name-keyed map silently collapsed duplicate names, which expression-id matching cannot do.
Why are the changes needed?
It is a correctness bug: the query returns wrong results. Present since Spark 2.3.0, when the rule was introduced.
The right side contains no alias named
id, so itsWHERE id = 1unambiguously references the base column. But the condition is ported onto the left output by name and lands on the left's alias ofval:[10],[20][20]SPARK-23274added a check that every reference in the condition is present among the left output names, but that does not catch this case: here the name is present, it just belongs to an unrelated column. The collision is between a left alias and a right base column. Note that alias names are erased during canonicalization (QueryPlan#doCanonicalizerewritesAlias(child, name)toAlias(normalizedChild, "")), which is whyval AS idandval AS vcompare assameResultand the rule fires in the first place.The existing workaround is
SET spark.sql.optimizer.replaceExceptWithFilter=false.Does this PR introduce any user-facing change?
Yes, it fixes wrong results for the queries described above. Affected queries now return the correct answer.
There is also a secondary plan change. Because references are now traced by expression id rather than by name, the rule can apply in a case where it previously bailed out — when the right condition references a base column that the left side exposes under a different name, e.g.
Previously
valwas not found among the left output names (v) and the plan fell back to the anti-join; it is now rewritten to the filter form.Dropping the name-distinctness guard widens the rule the same way for a left side with duplicate output names, e.g.
SELECT id AS x, val AS x FROM t, which previously always fell back to the anti-join.In both cases the results are unchanged — these are plan/performance changes only.
How was this patch tested?
New tests, both of which fail without the rule change and pass with it:
ReplaceOperatorSuite:SPARK-58383: ReplaceExceptWithFilter should not bind the right condition by name— asserts the rewrite is skipped and the anti-join is kept.SQLQuerySuite:SPARK-58383: EXCEPT when a left alias reuses the name of a base column— end-to-end version of the reported repro, plus a duplicate-left-output-name query covering the path opened up by removing the distinctness guard. Both run withspark.sql.optimizer.replaceExceptWithFilterenabled and disabled.Verified the end-to-end test catches the bug by reverting only the rule change and re-running it: it returned 2 rows instead of 1, with
DataFilters: [NOT coalesce((val#87 = 1), false)]on the scan.Existing suites, all passing:
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)