Repository navigation
[CALCITE-7882] JOIN ON condition with an uncorrelated sub-query followed by a correlated EXISTS fails with "Unable to implement" - #5356
Conversation
105d10b to
ae4176b
Compare
0f05aee to
7fa9335
Compare
7fa9335 to
2e550ed
Compare
|
I see this is still a draft. Should I wait with the review? |
I'd like more time to consider the most appropriate implementation approach. |
97ede6d to
5ef1f21
Compare
|
The high-level plan makes sense, but this won't be easy to review. |
|
|
||
| !ok | ||
|
|
||
| # [CALCITE-7882] JOIN ON condition with an uncorrelated sub-query followed by |
There was a problem hiding this comment.
Please describe how these results were validated in a comment.
| /** Throws {@link Util.FoundOne} if it finds a non-deterministic call in an | ||
| * expression, in the relational expressions of its sub-queries, or in a | ||
| * relational expression and its inputs. */ | ||
| private static class NonDeterministicFinder extends RexShuttle { |
There was a problem hiding this comment.
Should we also consider non-repeatable Sample inputs? Reading them twice can produce different samples and incorrect results.
There was a problem hiding this comment.
Thx. I'll look into it later.
| } | ||
| // The joins that replace "join" keep its hints. | ||
| builder.push( | ||
| LogicalJoin.create(leftDomain, rightDomain, join.getHints(), |
There was a problem hiding this comment.
Should we use the configured JoinFactory here to support custom join types?
| // both inputs). Not Join#copy either, because that would keep the original, | ||
| // now stale, variablesSet. | ||
| builder.push( | ||
| LogicalJoin.create(left, right, join.getHints(), newCondition, |
There was a problem hiding this comment.
Should we use the configured JoinFactory here as well?
There was a problem hiding this comment.
I'll look into it later.
|
While looking at [CALCITE-7894] I came up with this test which seems to be fixed by this PR: select (select count(*) from (values (0, 1), (1, 2), (2, 3)) as l(k, a)
where exists (select 1 from (values (0), (1), (2)) as s(v) where s.v = l.k)) as c1,
(select count(*) from (values (0, 1), (1, 2), (5, 3)) as r(k, b)
where exists (select 1 from (values (0), (1), (2)) as s(v) where s.v = r.k)) as c2; |
This case was already working before; it is not affected by this change. |
Sorry, I've bundled several fixes into one commit. I'll split them into multiple smaller PRs later so they're easier to review. |
a98740a to
7b5b926
Compare
|
checkstyle is unhappy |
c2997d3 to
f67315d
Compare
…wed by a correlated EXISTS fails with "Unable to implement" SubQueryRemoveRule rewrites one sub-query of a JOIN condition per call. The join that it creates now keeps defining the correlation variables that the remaining sub-queries use, remapped to its new row type, and the rewritten sub-query gets fresh ids for the variables that it shares with them. Add DeduplicateCorrelateVariables.go for a RexNode, which also visits operands and relational expressions of nested sub-queries.
f67315d to
6d29116
Compare
|
|
what is the status of this PR? |



CALCITE-7882
1. Symptom
With
expand=false, aJOIN ... ONcondition containing multiple sub-queries can fail with "Unable to implement":The scalar sub-query is uncorrelated, while the
EXISTSreferences the current join scope. Both remain in the ON condition asRexSubQuery, and the Join declares the correlation variable used by theEXISTS, for examplevariablesSet=[$cor0].2. Root cause: rewriting one sub-query loses the scope of the remaining ones
SubQueryRemoveRule.matchJoinrewrites one sub-query per invocation and relies on repeated rule application. Sub-queries equal to the selected one are replaced together.In the original implementation:
RelBuilder.joinoverload, which supplies an emptyvariablesSet.$cor0, although its ON condition still contains the correlatedEXISTSreferencing$cor0.EMPNO.EXISTSwith the rebuilt Join's now-emptyvariablesSet. The result is empty, so the sub-query is wrongly treated as uncorrelated.The underlying problem is the loss of correlation-variable scope across incremental sub-query rewrites, not an incorrectly computed scalar expression.
3. Fix: preserve the scope and remap correlation references
(a) Keep declaring the variables that the remaining sub-queries use
matchJoinsplits the Join's variables into two sets:subQueryVariables: used by the selected sub-query, bound by its rewrite;remainingVariables: used by the other sub-queries still in the ON condition, which the replacement relational expression must keep declaring.The two single-side branches rebuild the Join through a new helper,
rebuildJoin. WhenremainingVariablesis non-empty, it explicitly creates aLogicalJointhat declares them. It does not use:RelBuilder.joinwith a non-empty variable set, because that may create a Correlate, which cannot represent an ON condition referencing both inputs;Join.copy, because that would keep the original, now stale, variable set.When
remainingVariablesis empty, the ordinaryRelBuilder.joinis used as before. In the cross-product branch (INNER joins whose selected sub-query depends on both inputs), the remaining variables are declared on the replacement Filter.(b) Update both the correlation row type and the field indexes
A correlation variable declared by the Join references the combined input row. Rewriting a sub-query appends auxiliary fields, which changes the shape of that row.
remapCorrelationVariablesbuilds aMappingand appliesRelOptUtil.RexCorrelVariableMapShuttleto update the row type and field-access indexes, including references inside nested sub-queries.The selected sub-query's own correlation references are also adapted to the input it is rewritten against:
-nFieldsLeft.Indexes that a negative shift pushes outside the target row type are left unmapped, so an invalid reference fails instead of silently resolving to the wrong field.
In
rebuildJoin, the correlation row type is built from both inputs withSqlValidatorUtil.createJoinType, not from the Join's output type. This matters for SEMI and ANTI joins, whose ON condition can reference the right input even though their output does not contain it.(c) Separate correlation ids shared by the selected and remaining sub-queries
Preserving
remainingVariablesis not enough when several correlated sub-queries share an id. For example, twoEXISTSsub-queries may both use$cor0. Rewriting one moves its binding to an auxiliary Correlate over a single input, while the other still needs$cor0to be defined by the Join over the joint row. Reusing$cor0would define the same id twice with different row types.renameSharedVariablesgives the selected sub-query fresh ids only for the variables it shares with the remaining sub-queries. Non-shared variables and variables bound by an outer scope are left unchanged.The uncorrelated scalar query in the original example shares no id with the
EXISTS: it demonstrates scope preservation, while shared-id handling covers an additional case.4. Rename correlation variables over the complete expression
renameSharedVariablestakes aRexNodeand returns aPair<RexNode, Set<CorrelationId>>holding the rewritten expression and its updated variable set. It delegates to a newDeduplicateCorrelateVariables.go(RexBuilder, CorrelationId, Iterable<CorrelationId>, RexNode)overload, which reuses the existing traversal and covers:Renaming only
subQuery.relwould be insufficient. If an operand of anINholds a correlated scalar sub-query and keeps the old id while the binder declares only the new one, that reference would be undefined.5. Tests
sub-query.iq: the example above, and a variant that combines a scalar sub-query, a correlatedEXISTSand aNOT INin one ON condition.RelOptUtilTest.testDeduplicateCorrelationsInRexExpression: checks that the newRexNodeoverload renames nested operands and relational expressions, leaves outer-scope ids alone, and is idempotent.6. Scope and limitations
This change preserves correlation scope, binding identity and field geometry across incremental rewrites. It does not change which cases are rewritten:
Logicfor single-side ANTI and LEFT_MARK joins is not changed.