Skip to content

Commit 2d2a11b

Browse files
aunjgrXuPeng-SHmergify[bot]
authored
fix(plan): handle correlated scalar outer projections (#25920)
## What type of PR is this? - [ ] API-change - [x] BUG - [ ] Improvement - [ ] Documentation - [ ] Feature - [ ] Test and CI - [ ] Code Refactoring ## Which issue(s) this PR fixes: issue #23062 ## What this PR does / why we need it: Direct outer-column references projected by correlated scalar subqueries could remain in the executable right subtree, causing internal errors and incorrect behavior through DISTINCT and ORDER BY/LIMIT wrappers. This change: - normalizes the direct correlated projection before predicate pull-up; - preserves SINGLE JOIN cardinality checks for direct and ORDER BY-only cases; - uses MARK JOIN existence semantics for DISTINCT and LIMIT 1; - seeds predicate-free scalar MARK joins with TRUE so empty and nonempty inner inputs preserve all outer rows correctly; - adds planner and distributed regressions for empty, one-row, and multi-row inner inputs. ## Testing - make build - go vet ./pkg/sql/plan - .agents/skills/mo-dev/scripts/mo-cgo-test -v -count=1 -timeout=180s ./pkg/sql/plan - focused mo-tester BVT: 21/21 passed --------- Co-authored-by: XuPeng-SH <xupeng3112@163.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 1ce49be commit 2d2a11b

4 files changed

Lines changed: 477 additions & 7 deletions

File tree

pkg/sql/plan/flatten_subquery.go

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func (builder *QueryBuilder) flattenSubquery(nodeID int32, subquery *plan.Subque
7878

7979
subID := subquery.NodeId
8080
subCtx := builder.ctxByNode[subID]
81+
var scalarMatch *plan.Expr
82+
var scalarOuterResult *plan.Expr
83+
var scalarExistential bool
8184

8285
// Strip unnecessary subqueries which have no FROM clause
8386
subNode := builder.qry.Nodes[subID]
@@ -122,6 +125,11 @@ func (builder *QueryBuilder) flattenSubquery(nodeID int32, subquery *plan.Subque
122125
}
123126
}
124127

128+
if subquery.Typ == plan.SubqueryRef_SCALAR {
129+
subID, scalarMatch, scalarOuterResult, scalarExistential =
130+
builder.normalizeDirectCorrelatedScalarProjection(subID, subCtx)
131+
}
132+
125133
subID, preds, err := builder.pullupCorrelatedPredicates(subID, subCtx)
126134
if err != nil {
127135
return 0, nil, err
@@ -149,11 +157,36 @@ func (builder *QueryBuilder) flattenSubquery(nodeID int32, subquery *plan.Subque
149157
switch subquery.Typ {
150158
case plan.SubqueryRef_SCALAR:
151159
var rewrite bool
160+
152161
// Uncorrelated subquery
153162
if len(joinPreds) > 0 && builder.findAggrCount(subCtx.aggregates) {
154163
rewrite = true
155164
}
156165

166+
if scalarExistential {
167+
if len(joinPreds) == 0 {
168+
joinPreds = append(joinPreds, constTrue)
169+
}
170+
var retExpr *plan.Expr
171+
nodeID, retExpr, err = builder.insertMarkJoin(nodeID, subID, joinPreds, nil, false, ctx)
172+
if err != nil {
173+
return 0, nil, err
174+
}
175+
if len(filterPreds) > 0 {
176+
nodeID = builder.appendNode(&plan.Node{
177+
NodeType: plan.Node_FILTER,
178+
Children: []int32{nodeID},
179+
FilterList: filterPreds,
180+
}, ctx)
181+
}
182+
retExpr, err = BindFuncExprImplByPlanExpr(builder.GetContext(), "case", []*plan.Expr{
183+
retExpr,
184+
scalarOuterResult,
185+
makePlan2NullConstExprWithType(),
186+
})
187+
return nodeID, retExpr, err
188+
}
189+
157190
joinType := plan.Node_SINGLE
158191
if subCtx.hasSingleRow {
159192
joinType = plan.Node_LEFT
@@ -175,14 +208,27 @@ func (builder *QueryBuilder) flattenSubquery(nodeID int32, subquery *plan.Subque
175208
}, ctx)
176209
}
177210

178-
retExpr := &plan.Expr{
179-
Typ: subCtx.results[0].Typ,
180-
Expr: &plan.Expr_Col{
181-
Col: &plan.ColRef{
182-
RelPos: subCtx.topTag(),
183-
ColPos: 0,
211+
retExpr := scalarMatch
212+
if retExpr == nil {
213+
retExpr = &plan.Expr{
214+
Typ: subCtx.results[0].Typ,
215+
Expr: &plan.Expr_Col{
216+
Col: &plan.ColRef{
217+
RelPos: subCtx.topTag(),
218+
ColPos: 0,
219+
},
184220
},
185-
},
221+
}
222+
}
223+
if scalarOuterResult != nil {
224+
retExpr, err = BindFuncExprImplByPlanExpr(builder.GetContext(), "case", []*plan.Expr{
225+
retExpr,
226+
scalarOuterResult,
227+
makePlan2NullConstExprWithType(),
228+
})
229+
if err != nil {
230+
return 0, nil, err
231+
}
186232
}
187233
if rewrite {
188234
argsType := make([]types.Type, 1)
@@ -317,6 +363,85 @@ func (builder *QueryBuilder) flattenSubquery(nodeID int32, subquery *plan.Subque
317363
}
318364
}
319365

366+
// normalizeDirectCorrelatedScalarProjection handles a scalar subquery whose
367+
// only result is a direct reference to the outer row. Rim projections, sorting,
368+
// DISTINCT, and literal positive LIMITs do not change that value. Removing them
369+
// before pulling up predicates keeps join-key references on the real projection
370+
// instead of leaving correlated expressions in executable wrapper nodes.
371+
func (builder *QueryBuilder) normalizeDirectCorrelatedScalarProjection(
372+
subID int32,
373+
ctx *BindContext,
374+
) (int32, *plan.Expr, *plan.Expr, bool) {
375+
if len(ctx.results) != 1 || len(ctx.projects) == 0 {
376+
return subID, nil, nil, false
377+
}
378+
379+
projectCorr := ctx.projects[0].GetCorr()
380+
if projectCorr == nil || projectCorr.Depth != 1 {
381+
return subID, nil, nil, false
382+
}
383+
if !builder.casePreservesType(ctx.projects[0]) {
384+
return subID, nil, nil, false
385+
}
386+
387+
nodeID := subID
388+
existential := false
389+
for {
390+
node := builder.qry.Nodes[nodeID]
391+
if node.Offset != nil || node.RankOption != nil {
392+
return subID, nil, nil, false
393+
}
394+
if node.Limit != nil {
395+
limit, ok := getLiteralUint64(node.Limit)
396+
if !ok || limit == 0 {
397+
return subID, nil, nil, false
398+
}
399+
if limit == 1 {
400+
existential = true
401+
}
402+
}
403+
404+
if node.NodeType == plan.Node_PROJECT && len(node.BindingTags) > 0 && node.BindingTags[0] == ctx.projectTag {
405+
if len(node.ProjectList) == 0 {
406+
return subID, nil, nil, false
407+
}
408+
corr := node.ProjectList[0].GetCorr()
409+
if corr == nil || corr.RelPos != projectCorr.RelPos || corr.ColPos != projectCorr.ColPos || corr.Depth != projectCorr.Depth {
410+
return subID, nil, nil, false
411+
}
412+
413+
outerResult, _ := decreaseDepth(DeepCopyExpr(ctx.projects[0]))
414+
marker := DeepCopyExpr(constTrue)
415+
node.ProjectList = []*plan.Expr{marker}
416+
ctx.projects = []*plan.Expr{marker}
417+
node.Limit = nil
418+
return nodeID, GetColExpr(marker.Typ, ctx.projectTag, 0), outerResult, existential
419+
}
420+
421+
if len(node.Children) != 1 {
422+
return subID, nil, nil, false
423+
}
424+
switch node.NodeType {
425+
case plan.Node_PROJECT, plan.Node_SORT:
426+
case plan.Node_DISTINCT:
427+
existential = true
428+
default:
429+
return subID, nil, nil, false
430+
}
431+
nodeID = node.Children[0]
432+
}
433+
}
434+
435+
func (builder *QueryBuilder) casePreservesType(expr *plan.Expr) bool {
436+
sourceType := makeTypeByPlan2Expr(expr)
437+
caseFn, err := function.GetFunctionByName(builder.GetContext(), "case", []types.Type{
438+
types.T_bool.ToType(),
439+
sourceType,
440+
types.T_any.ToType(),
441+
})
442+
return err == nil && caseFn.GetReturnType().Eq(sourceType)
443+
}
444+
320445
func (builder *QueryBuilder) insertMarkJoin(left, right int32, joinPreds []*plan.Expr, outerPred *plan.Expr, negate bool, ctx *BindContext) (nodeID int32, markExpr *plan.Expr, err error) {
321446
markTag := builder.genNewBindTag()
322447

0 commit comments

Comments
 (0)