Skip to content

Fix OR handling in inToMarkJoin and scalarToSingleJoin (#271) - #322

Merged
zhouqingqing merged 1 commit into
masterfrom
fix-or-in-subquery
Apr 14, 2026
Merged

zhouqingqing merged 1 commit into
masterfrom
fix-or-in-subquery

Conversation

@zhouqingqing

Copy link
Copy Markdown
Owner

Summary

Fixes #271 — OR expressions were only handled in existsToMarkJoin but not in inToMarkJoin or scalarToSingleJoin. When a correlated IN/scalar subquery appeared inside an OR (e.g., a1 IN (select ...) OR a2 > 1), decorrelation did not preserve OR semantics.

  • Apply the same OR-aware filter extraction from existsToMarkJoin to extractCurINExprFromNodeAFilter and djoinOnRightFilter
  • Detect OR conjuncts with extra subqueries via hasAnyExtraSubqueryExprInOR
  • Set canReplace flag so the caller advances innerNode correctly
  • Pass canReplace ref from oneSubqueryToJoin to all three methods

Note: The nested case (OR inside a scalar subquery's filter alongside another correlated subquery — the original #271 example) remains unsupported during unnesting and falls back to nested-loop execution.

Test plan

  • New tests: correlated IN in OR, NOT IN in OR
  • All 72 existing tests pass
  • TPC-H / TPC-DS benchmarks pass

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes decorrelation/unnesting correctness for correlated IN and scalar subqueries that appear under OR in outer filters (issue #271), aligning their behavior with the existing existsToMarkJoin OR-aware handling.

Changes:

  • Extend OR-aware filter extraction/removal to inToMarkJoin and scalarToSingleJoin paths via hasAnyExtraSubqueryExprInOR and a propagated canReplace flag.
  • Update decorrelation plumbing to pass canReplace through oneSubqueryToJoin into the relevant conversion methods.
  • Add unit tests covering correlated IN / NOT IN inside OR, and document/skip the known unsupported nested scalar-subquery-with-OR-inside-subquery-filter case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
qpmodel/subquery.cs Adds OR-aware handling and canReplace propagation for IN and scalar subquery decorrelation.
test/UnitTest.cs Adds regression tests for correlated IN / NOT IN under OR and clarifies the unsupported nested scalar-OR case.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread qpmodel/subquery.cs
Comment on lines +537 to +538
bool unusedCanReplace = false;
var newplan = djoinOnRightFilter(singleJoinNode, scalarExpr, ref unusedCanReplace);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observation. For the aggregation path, the scalar subquery is wrapped in an aggregate (e.g., SELECT max(b.i) FROM b WHERE ...), which guarantees a single row result. The OR-in-outer-filter scenario is handled by scalarToSingleJoin before dispatching to djoinOnRightAggregation, so canReplace is already set correctly at that level. The unusedCanReplace in djoinOnRightAggregation is intentional — the inner call only restructures the aggregation plan.

Comment thread qpmodel/subquery.cs
Comment on lines 1084 to 1102
LogicNode oneSubqueryToJoin(LogicNode planWithSubExpr, SubqueryExpr subexpr, ref bool canRepalce)
{
LogicNode oldplan = planWithSubExpr;
LogicNode newplan = null;

if (!subexpr.IsCorrelated())
return planWithSubExpr;

switch (subexpr)
{
case ExistSubqueryExpr se:
newplan = existsToMarkJoin(planWithSubExpr, se, ref canRepalce);
break;
case ScalarSubqueryExpr ss:
newplan = scalarToSingleJoin(planWithSubExpr, ss);
newplan = scalarToSingleJoin(planWithSubExpr, ss, ref canRepalce);
break;
case InSubqueryExpr si:
newplan = inToMarkJoin(planWithSubExpr, si);
newplan = inToMarkJoin(planWithSubExpr, si, ref canRepalce);
break;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but canRepalce is the existing spelling in the codebase (predates this PR). Renaming it would be a separate cleanup change — keeping it consistent with the existing code for now.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses issue #271 by extending OR-aware correlated-subquery decorrelation beyond existsToMarkJoin to also cover inToMarkJoin and scalarToSingleJoin, aiming to preserve correct semantics when correlated IN/scalar subqueries appear under OR conditions.

Changes:

  • Add OR-aware filter extraction logic to correlated IN and scalar-subquery decorrelation paths (including a canReplace signal to guide iteration state in DecorrelatePass).
  • Introduce/extend detection of OR conjuncts containing additional subqueries via hasAnyExtraSubqueryExprInOR.
  • Add unit tests for correlated IN / NOT IN under OR and annotate an existing unsupported nested scalar case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
test/UnitTest.cs Adds regression tests covering correlated IN/NOT IN in OR and clarifies the unsupported nested scalar-subquery case.
qpmodel/subquery.cs Propagates OR-aware extraction and canReplace signaling into inToMarkJoin and scalarToSingleJoin/djoinOnRightFilter.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread qpmodel/subquery.cs
Comment on lines +537 to +538
bool unusedCanReplace = false;
var newplan = djoinOnRightFilter(singleJoinNode, scalarExpr, ref unusedCanReplace);
Comment thread qpmodel/subquery.cs
Comment on lines +439 to 444
// if there is any (expr with @1 or @2), the root should be replaced
canReplace = andlist.Find(x => (x is LogicOrExpr) && hasAnyExtraSubqueryExprInOR(x, scalarExpr)) != null;

if (andlist.Count == 0 || canReplace)
nodeLeft.NullifyFilter();
else
Previously only existsToMarkJoin handled OR expressions correctly.
When a correlated IN subquery or scalar subquery appeared inside an
OR expression (e.g., "a1 IN (select ...) OR a2 > 1"), the
decorrelation would not properly preserve OR semantics.

Apply the same OR-aware filter extraction pattern from
existsToMarkJoin to both inToMarkJoin (via extractCurINExprFromNodeAFilter)
and scalarToSingleJoin (via djoinOnRightFilter):
- Detect OR conjuncts containing extra subqueries
- Set canReplace flag so the caller advances innerNode correctly
- Preserve OR expressions for further unnesting

Note: the nested case where OR appears *inside* a scalar subquery's
filter alongside another correlated subquery (the original issue #271
example) remains unsupported during unnesting and falls back to
nested-loop execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zhouqingqing
zhouqingqing merged commit 6529451 into master Apr 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OR should be handled in scalarToSingleJoin and inToMarkJoin

2 participants