diff --git a/pkg/sql/plan/flatten_subquery_test.go b/pkg/sql/plan/flatten_subquery_test.go index 1b91be26f73ea..0a0a46293c442 100644 --- a/pkg/sql/plan/flatten_subquery_test.go +++ b/pkg/sql/plan/flatten_subquery_test.go @@ -197,6 +197,447 @@ func TestNestedCorrelatedScalarAggregatePullsUpGroupingKey(t *testing.T) { } } +func TestTransparentCorrelatedDerivedTableChain(t *testing.T) { + for _, tt := range []struct { + name string + sql string + wantKeys int + }{ + { + name: "one projection", + wantKeys: 1, + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY) d1) + FROM NATION n1`, + }, + { + name: "aliases reorder and local filter", + wantKeys: 1, + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT d1.region_alias, d1.nation_alias + FROM (SELECT n2.N_NATIONKEY AS nation_alias, + n2.N_REGIONKEY AS region_alias + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + AND n2.N_NATIONKEY >= 0) d1) d2) + FROM NATION n1`, + }, + { + name: "multiple correlation keys", + wantKeys: 2, + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + AND n1.N_NATIONKEY = n2.N_NATIONKEY) d1) + FROM NATION n1`, + }, + { + name: "reverse correlation operands", + wantKeys: 1, + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n2.N_REGIONKEY = n1.N_REGIONKEY) d1) + FROM NATION n1`, + }, + { + name: "prisma json aggregate shape", + wantKeys: 1, + sql: `SELECT n1.N_NATIONKEY, + (SELECT COALESCE(JSON_ARRAYAGG(__prisma_data__), CONVERT('[]', JSON)) + FROM (SELECT d3.__prisma_data__ + FROM (SELECT JSON_OBJECT('id', d2.N_NATIONKEY, + 'name', d2.N_NAME, + 'regionId', d2.N_REGIONKEY) AS __prisma_data__ + FROM (SELECT n2.* + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY) d2) d3) d4) + FROM NATION n1`, + }, + } { + t.Run(tt.name, func(t *testing.T) { + logicPlan, err := runOneStmt(NewMockOptimizer(true), t, tt.sql) + require.NoError(t, err) + + query := logicPlan.GetQuery() + require.NotNil(t, query) + var scalarJoin *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_JOIN && node.JoinType == plan.Node_LEFT { + scalarJoin = node + break + } + } + require.NotNil(t, scalarJoin) + require.Len(t, scalarJoin.OnList, tt.wantKeys) + assertReachablePlanHasNoCorrelatedExpr(t, query) + }) + } +} + +func TestTransparentCorrelatedDerivedTableRejectsUnsafeShapes(t *testing.T) { + for _, tt := range []struct { + name string + sql string + wantErr string + }{ + { + name: "same FROM scope", + wantErr: "missing FROM-clause entry for table 'n1'", + sql: `SELECT n1.N_NATIONKEY + FROM NATION n1 + JOIN (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY) d ON TRUE`, + }, + { + name: "join inside derived table", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 JOIN REGION r + ON n2.N_REGIONKEY = r.R_REGIONKEY + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY) d) + FROM NATION n1`, + }, + { + name: "correlation in derived projection", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n1.N_REGIONKEY + FROM NATION n2) d) + FROM NATION n1`, + }, + { + name: "group by", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_REGIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + GROUP BY n2.N_REGIONKEY) d) + FROM NATION n1`, + }, + { + name: "having", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_REGIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + GROUP BY n2.N_REGIONKEY + HAVING COUNT(*) > 0) d) + FROM NATION n1`, + }, + { + name: "distinct", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT DISTINCT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY) d) + FROM NATION n1`, + }, + { + name: "window", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT ROW_NUMBER() OVER (ORDER BY n2.N_NATIONKEY) AS rn + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY) d) + FROM NATION n1`, + }, + { + name: "set operation", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + UNION ALL + SELECT r.R_REGIONKEY + FROM REGION r + WHERE n1.N_REGIONKEY = r.R_REGIONKEY) d) + FROM NATION n1`, + }, + { + name: "order by", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + ORDER BY n2.N_NATIONKEY) d) + FROM NATION n1`, + }, + { + name: "limit and offset", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + LIMIT 1 OFFSET 1) d) + FROM NATION n1`, + }, + { + name: "or correlation", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY = n2.N_REGIONKEY + OR n2.N_NATIONKEY = 0) d) + FROM NATION n1`, + }, + { + name: "non equality correlation", + sql: `SELECT n1.N_NATIONKEY, + (SELECT COUNT(*) + FROM (SELECT n2.N_NATIONKEY + FROM NATION n2 + WHERE n1.N_REGIONKEY < n2.N_REGIONKEY) d) + FROM NATION n1`, + }, + } { + t.Run(tt.name, func(t *testing.T) { + _, err := runOneStmt(NewMockOptimizer(true), t, tt.sql) + if tt.wantErr == "" { + tt.wantErr = "correlated subquery in FROM clause is not yet implemented" + } + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +func TestTransparentCorrelatedDerivedTableRejectsDeepAncestor(t *testing.T) { + immediateParent := `SELECT n1.N_NATIONKEY + FROM NATION n1 + WHERE EXISTS ( + SELECT 1 FROM NATION n2 + WHERE n2.N_NATIONKEY = n1.N_NATIONKEY + AND EXISTS ( + SELECT 1 FROM ( + SELECT n3.N_NATIONKEY FROM NATION n3 + WHERE n3.N_REGIONKEY = n2.N_REGIONKEY + ) d + ) + )` + _, err := runOneStmt(NewMockOptimizer(true), t, immediateParent) + require.NoError(t, err) + + for _, tt := range []struct { + name string + sql string + }{ + { + name: "empty intermediate scope", + sql: `SELECT n1.N_NATIONKEY + FROM NATION n1 + WHERE EXISTS ( + SELECT 1 + WHERE EXISTS ( + SELECT 1 FROM ( + SELECT n3.N_NATIONKEY FROM NATION n3 + WHERE n3.N_NATIONKEY = n1.N_NATIONKEY + ) d + ) + )`, + }, + { + name: "empty then non-empty intermediate scopes", + sql: `SELECT n1.N_NATIONKEY + FROM NATION n1 + WHERE EXISTS ( + SELECT 1 + WHERE EXISTS ( + SELECT 1 FROM NATION n2 + WHERE EXISTS ( + SELECT 1 FROM ( + SELECT n3.N_NATIONKEY FROM NATION n3 + WHERE n3.N_NATIONKEY = n1.N_NATIONKEY + ) d + ) + ) + )`, + }, + { + name: "non-empty then empty intermediate scopes", + sql: `SELECT n1.N_NATIONKEY + FROM NATION n1 + WHERE EXISTS ( + SELECT 1 FROM NATION n2 + WHERE EXISTS ( + SELECT 1 + WHERE EXISTS ( + SELECT 1 FROM ( + SELECT n3.N_NATIONKEY FROM NATION n3 + WHERE n3.N_REGIONKEY = n2.N_REGIONKEY + ) d + ) + ) + )`, + }, + { + name: "grandparent only", + sql: `SELECT n1.N_NATIONKEY + FROM NATION n1 + WHERE EXISTS ( + SELECT 1 FROM NATION n2 + WHERE n2.N_NATIONKEY = n1.N_NATIONKEY + AND EXISTS ( + SELECT 1 FROM ( + SELECT n3.N_NATIONKEY FROM NATION n3 + WHERE n3.N_NATIONKEY = n1.N_NATIONKEY + ) d + ) + )`, + }, + { + name: "mixed immediate and grandparent", + sql: `SELECT n1.N_NATIONKEY + FROM NATION n1 + WHERE EXISTS ( + SELECT 1 FROM NATION n2 + WHERE n2.N_NATIONKEY = n1.N_NATIONKEY + AND EXISTS ( + SELECT 1 FROM ( + SELECT n3.N_NATIONKEY FROM NATION n3 + WHERE n3.N_REGIONKEY = n2.N_REGIONKEY + AND n3.N_NATIONKEY = n1.N_NATIONKEY + ) d + ) + )`, + }, + } { + t.Run(tt.name, func(t *testing.T) { + _, err := runOneStmt(NewMockOptimizer(true), t, tt.sql) + require.ErrorContains(t, err, "correlated subquery in FROM clause is not yet implemented") + }) + } +} + +func TestTransparentCorrelatedDerivedTableNormalizationIsAtomic(t *testing.T) { + const outerTag int32 = 41 + + newBuilder := func(nodes []*plan.Node) *QueryBuilder { + builder := NewQueryBuilder(plan.Query_SELECT, NewMockCompilerContext(true), false, true) + builder.qry.Nodes = nodes + return builder + } + newNodes := func(corr *plan.CorrColRef, tail *plan.Node) []*plan.Node { + return []*plan.Node{ + {NodeType: plan.Node_TABLE_SCAN}, + { + NodeType: plan.Node_FILTER, + Children: []int32{2}, + FilterList: []*plan.Expr{newTransparentDerivedEquality(corr, 42)}, + }, + tail, + { + NodeType: plan.Node_PROJECT, + Children: []int32{1}, + ProjectList: []*plan.Expr{newFlattenSubqueryTestColExpr(42)}, + }, + } + } + + t.Run("unsupported tail leaves depth and context unchanged", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 2} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_SORT, Children: []int32{0}}) + ctx := NewBindContext(nil, nil) + err := newBuilder(nodes).normalizeTransparentCorrelatedDerivedTable(3, ctx) + require.ErrorContains(t, err, "correlated subquery in FROM clause is not yet implemented") + require.Equal(t, int32(2), corr.Depth) + require.False(t, ctx.isCorrelated) + }) + + t.Run("scan block filter remains unsupported and atomic", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 2} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_TABLE_SCAN}) + nodes[1].Children[0] = 0 + nodes[0].BlockFilterList = []*plan.Expr{newTransparentDerivedEquality(corr, 42)} + ctx := NewBindContext(nil, nil) + err := newBuilder(nodes).normalizeTransparentCorrelatedDerivedTable(3, ctx) + require.ErrorContains(t, err, "correlated subquery in FROM clause is not yet implemented") + require.Equal(t, int32(2), corr.Depth) + require.False(t, ctx.isCorrelated) + }) + + t.Run("same scope remains non lateral", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 1} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_TABLE_SCAN}) + nodes[1].Children[0] = 0 + ctx := NewBindContext(nil, nil) + ctx.bindingByTag[outerTag] = nil + err := newBuilder(nodes).normalizeTransparentCorrelatedDerivedTable(3, ctx) + require.ErrorContains(t, err, "correlated subquery in FROM clause is not yet implemented") + require.Equal(t, int32(1), corr.Depth) + require.False(t, ctx.isCorrelated) + }) + + t.Run("ancestor correlation propagates through binderless scope", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 1} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_TABLE_SCAN}) + nodes[1].Children[0] = 0 + ctx := NewBindContext(nil, nil) + require.NoError(t, newBuilder(nodes).normalizeTransparentCorrelatedDerivedTable(3, ctx)) + require.Equal(t, int32(1), corr.Depth) + require.True(t, ctx.isCorrelated) + }) + + t.Run("repeated ancestor reference decrements depth exactly once", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 2} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_TABLE_SCAN}) + nodes[1].Children[0] = 0 + nodes[1].FilterList = append(nodes[1].FilterList, newTransparentDerivedEquality(corr, 42)) + ancestor := NewBindContext(nil, nil) + ancestor.bindingByTag[outerTag] = nil + ctx := NewBindContext(nil, ancestor) + require.NoError(t, newBuilder(nodes).normalizeTransparentCorrelatedDerivedTable(3, ctx)) + require.Equal(t, int32(1), corr.Depth) + require.True(t, ctx.isCorrelated) + }) + + t.Run("binder-backed empty ancestor is rejected atomically", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 3} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_TABLE_SCAN}) + nodes[1].Children[0] = 0 + builder := newBuilder(nodes) + owner := NewBindContext(nil, nil) + owner.bindingByTag[outerTag] = nil + emptyQuery := NewBindContext(nil, owner) + emptyQuery.binder = NewWhereBinder(builder, emptyQuery) + ctx := NewBindContext(nil, emptyQuery) + err := builder.normalizeTransparentCorrelatedDerivedTable(3, ctx) + require.ErrorContains(t, err, "correlated subquery in FROM clause is not yet implemented") + require.Equal(t, int32(3), corr.Depth) + require.False(t, ctx.isCorrelated) + }) + + t.Run("deeper ancestor is rejected atomically", func(t *testing.T) { + corr := &plan.CorrColRef{RelPos: outerTag, Depth: 3} + nodes := newNodes(corr, &plan.Node{NodeType: plan.Node_TABLE_SCAN}) + nodes[1].Children[0] = 0 + owner := NewBindContext(nil, nil) + owner.bindingByTag[outerTag] = nil + intermediate := NewBindContext(nil, owner) + intermediate.bindingByTag[42] = nil + ctx := NewBindContext(nil, intermediate) + err := newBuilder(nodes).normalizeTransparentCorrelatedDerivedTable(3, ctx) + require.ErrorContains(t, err, "correlated subquery in FROM clause is not yet implemented") + require.Equal(t, int32(3), corr.Depth) + require.False(t, ctx.isCorrelated) + }) +} + func TestNestedCorrelatedScalarStillRejectsUnsafeShapes(t *testing.T) { for _, sql := range []string{ `SELECT n1.N_NATIONKEY, @@ -1086,3 +1527,17 @@ func newFlattenSubqueryTestColExpr(tag int32) *plan.Expr { }, } } + +func newTransparentDerivedEquality(corr *plan.CorrColRef, localTag int32) *plan.Expr { + return &plan.Expr{ + Expr: &plan.Expr_F{ + F: &plan.Function{ + Func: &plan.ObjectRef{ObjName: "="}, + Args: []*plan.Expr{ + {Expr: &plan.Expr_Corr{Corr: corr}}, + newFlattenSubqueryTestColExpr(localTag), + }, + }, + }, + } +} diff --git a/pkg/sql/plan/numeric_assignment_context_test.go b/pkg/sql/plan/numeric_assignment_context_test.go index d30d3950e384b..f60b86d8457f5 100644 --- a/pkg/sql/plan/numeric_assignment_context_test.go +++ b/pkg/sql/plan/numeric_assignment_context_test.go @@ -802,6 +802,31 @@ func TestPreparedNumericFunctionControlArgUsesOverloadType(t *testing.T) { require.Equal(t, int32(types.T_int64), paramTypes[2].Id) } +func TestPreparedNumericAggregateReachesCorrelatedDerivedTable(t *testing.T) { + optimizer := NewMockOptimizer(true) + // SUM seeds a float64 target for d.x before the correlated derived source is + // bound, exercising the numeric-projection buildTable entry. + stmt, err := mysql.ParseOne( + optimizer.CurrentContext().GetContext(), + "insert into constraint_test.emp (sal) "+ + "select (select sum(d.x) from "+ + "(select ? as x from NATION n2 "+ + "where n2.N_REGIONKEY = n1.N_REGIONKEY) d) from NATION n1", + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(optimizer.CurrentContext(), stmt, true) + require.NoError(t, err) + + paramTypes := collectUniquePlanParamTypes(t, queryPlan) + require.Equal(t, map[int32]planpb.Type{ + 1: {Id: int32(types.T_float64)}, + }, paramTypes) + require.True(t, hasJoinType(queryPlan.GetQuery(), planpb.Node_LEFT)) + assertReachablePlanHasNoCorrelatedExpr(t, queryPlan.GetQuery()) +} + func TestSeedNumericSourceTargetKeepsAmbiguousPositionEmpty(t *testing.T) { decimal72 := planpb.Type{Id: int32(types.T_decimal64), Width: 7, Scale: 2} decimal74 := planpb.Type{Id: int32(types.T_decimal64), Width: 7, Scale: 4} diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index e15f033dc590e..f37f678bacf65 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3407,6 +3407,9 @@ func (builder *QueryBuilder) buildUnionWithResultLen( if err != nil { return 0, err } + if subCtx.isCorrelated { + ctx.isCorrelated = true + } if idx == 0 { projectLength = len(builder.qry.Nodes[nodeID].ProjectList) @@ -8896,6 +8899,173 @@ type tableFunctionInput struct { attachExecutionChild bool } +// normalizeTransparentCorrelatedDerivedTable lets a correlation pass through +// one derived-table query block when that block is only a transparent unary +// PROJECT/FILTER chain over one TABLE_SCAN. It deliberately does not make a +// same-level FROM reference lateral: a correlation owned by the immediate FROM +// context remains rejected. Binder-less derived contexts do not increment +// CorrColRef.Depth, so ownership, rather than depth alone, distinguishes that +// case from a reference to an ancestor outside the derived chain. +// +// Analysis is completed before any CorrColRef is changed so unsupported shapes +// cannot leave a partially normalized plan behind. +func (builder *QueryBuilder) normalizeTransparentCorrelatedDerivedTable( + nodeID int32, + ctx *BindContext, +) error { + corrRefs := make(map[*plan.CorrColRef]struct{}) + if !builder.analyzeTransparentCorrelatedDerivedTable(nodeID, ctx, corrRefs) || len(corrRefs) == 0 { + return moerr.NewNYI(builder.GetContext(), "correlated subquery in FROM clause") + } + + for corr := range corrRefs { + if corr.Depth > 1 { + corr.Depth-- + } + } + ctx.isCorrelated = true + return nil +} + +func (builder *QueryBuilder) analyzeTransparentCorrelatedDerivedTable( + nodeID int32, + ctx *BindContext, + corrRefs map[*plan.CorrColRef]struct{}, +) bool { + if nodeID < 0 || int(nodeID) >= len(builder.qry.Nodes) { + return false + } + + node := builder.qry.Nodes[nodeID] + if node == nil || len(node.OnList) > 0 || len(node.GroupBy) > 0 || len(node.AggList) > 0 || + len(node.WinSpecList) > 0 || len(node.OrderBy) > 0 || len(node.TblFuncExprList) > 0 || + node.Limit != nil || node.Offset != nil || node.RankOption != nil { + return false + } + + switch node.NodeType { + case plan.Node_PROJECT: + if len(node.Children) != 1 || len(node.FilterList) > 0 || len(node.BlockFilterList) > 0 { + return false + } + for _, expr := range node.ProjectList { + if !transparentDerivedLocalExpr(expr) { + return false + } + } + return builder.analyzeTransparentCorrelatedDerivedTable(node.Children[0], ctx, corrRefs) + + case plan.Node_FILTER: + if len(node.Children) != 1 || len(node.ProjectList) > 0 || len(node.BlockFilterList) > 0 { + return false + } + for _, expr := range node.FilterList { + if !analyzeTransparentDerivedFilter(expr, ctx, corrRefs) { + return false + } + } + return builder.analyzeTransparentCorrelatedDerivedTable(node.Children[0], ctx, corrRefs) + + case plan.Node_TABLE_SCAN: + if len(node.Children) != 0 || len(node.ProjectList) > 0 || len(node.BlockFilterList) > 0 { + return false + } + for _, expr := range node.FilterList { + if !analyzeTransparentDerivedFilter(expr, ctx, corrRefs) { + return false + } + } + return true + + default: + return false + } +} + +func transparentDerivedLocalExpr(expr *plan.Expr) bool { + if expr == nil { + return false + } + + switch exprImpl := expr.Expr.(type) { + case *plan.Expr_Corr, *plan.Expr_Sub, *plan.Expr_W: + return false + case *plan.Expr_F: + for _, arg := range exprImpl.F.Args { + if !transparentDerivedLocalExpr(arg) { + return false + } + } + case *plan.Expr_List: + for _, item := range exprImpl.List.List { + if !transparentDerivedLocalExpr(item) { + return false + } + } + } + + return true +} + +func analyzeTransparentDerivedFilter( + expr *plan.Expr, + ctx *BindContext, + corrRefs map[*plan.CorrColRef]struct{}, +) bool { + if !hasCorrCol(expr) { + return transparentDerivedLocalExpr(expr) + } + + fn := expr.GetF() + if fn == nil || fn.Func.GetObjName() != "=" || len(fn.Args) != 2 { + return false + } + + leftCorr, rightCorr := fn.Args[0].GetCorr(), fn.Args[1].GetCorr() + if (leftCorr == nil) == (rightCorr == nil) { + return false + } + + corr, local := leftCorr, fn.Args[1] + if corr == nil { + corr, local = rightCorr, fn.Args[0] + } + if corr.Depth <= 0 || local.GetCol() == nil { + return false + } + if !transparentDerivedCorrelationTargetsNearestAncestor(ctx, corr) { + return false + } + if !transparentDerivedLocalExpr(local) { + return false + } + + corrRefs[corr] = struct{}{} + return true +} + +func transparentDerivedCorrelationTargetsNearestAncestor(ctx *BindContext, corr *plan.CorrColRef) bool { + if _, sameFromScope := ctx.bindingByTag[corr.RelPos]; sameFromScope { + return false + } + if corr.Depth == 1 { + return true + } + + for ancestor := ctx.parent; ancestor != nil; ancestor = ancestor.parent { + if _, ownsCorrelation := ancestor.bindingByTag[corr.RelPos]; ownsCorrelation { + return true + } + // A bound query block is a real correlation level even when its FROM + // clause is empty. Only contexts that have not installed a binder yet + // are transparent while a derived table is being built. + if ancestor.binder != nil || len(ancestor.bindingByTag) > 0 { + return false + } + } + return false +} + func (builder *QueryBuilder) buildTable(stmt tree.TableExpr, ctx *BindContext, tableInput *tableFunctionInput) (nodeID int32, err error) { switch tbl := stmt.(type) { case *tree.Select: @@ -8907,8 +9077,13 @@ func (builder *QueryBuilder) buildTable(stmt tree.TableExpr, ctx *BindContext, t builder.isForUpdate = false nodeID, err = builder.bindSelect(tbl, subCtx, false) builder.isForUpdate = savedIsForUpdate + if err != nil { + return 0, err + } if subCtx.isCorrelated { - return 0, moerr.NewNYI(builder.GetContext(), "correlated subquery in FROM clause") + if err = builder.normalizeTransparentCorrelatedDerivedTable(nodeID, ctx); err != nil { + return 0, err + } } if subCtx.hasSingleRow { @@ -9236,8 +9411,13 @@ func (builder *QueryBuilder) buildTable(stmt tree.TableExpr, ctx *BindContext, t builder.isForUpdate = false nodeID, err = builder.bindSelect(derivedSelect, subCtx, false) builder.isForUpdate = savedIsForUpdate + if err != nil { + return 0, err + } if subCtx.isCorrelated { - return 0, moerr.NewNYI(builder.GetContext(), "correlated subquery in FROM clause") + if err = builder.normalizeTransparentCorrelatedDerivedTable(nodeID, ctx); err != nil { + return 0, err + } } if subCtx.hasSingleRow { ctx.hasSingleRow = true diff --git a/test/distributed/cases/subquery/scalar_correlated_projection.result b/test/distributed/cases/subquery/scalar_correlated_projection.result index fd14e5b3f53fd..4c396c65e7b18 100644 --- a/test/distributed/cases/subquery/scalar_correlated_projection.result +++ b/test/distributed/cases/subquery/scalar_correlated_projection.result @@ -8,6 +8,10 @@ create table parent_agg (id int primary key, corr_key int); create table child_agg (corr_key int, v int); insert into parent_agg values (1, 10), (2, 20), (3, 30), (4, 30); insert into child_agg values (10, 5), (10, 7), (20, null); +create table `Author` (`id` int primary key, `name` varchar(32)); +create table `Post` (`id` int primary key, `title` varchar(32), `authorId` int); +insert into `Author` values (1, 'Zero'), (2, 'One'), (3, 'Many'); +insert into `Post` values (20, 'Only', 2), (30, 'First', 3), (31, null, 3); select t1.*, (select t1.a from t2 where t2.d > t1.a) as x from t1 order by t1.a; ➤ a[4,32,0] ¦ b[4,32,0] ¦ c[4,32,0] ¦ x[4,32,0] 𝄀 1 ¦ 2 ¦ 3 ¦ null 𝄀 @@ -128,4 +132,46 @@ select p.id, (select sum(c.v) from child_agg c where c.corr_key = p.corr_key hav 2 ¦ null 𝄀 3 ¦ null 𝄀 4 ¦ null +SELECT `t0`.`id`, `t0`.`name`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) AS `__prisma_data__` FROM (SELECT `t3`.`__prisma_data__` FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`, 'authorId', `t2`.`authorId`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId` /* root select */) AS `t2` /* inner select */) AS `t3` /* middle select */) AS `t4` /* outer select */) AS `posts` FROM `Author` AS `t0` ORDER BY `t0`.`id` ASC; +➤ id[4,32,0] ¦ name[12,-1,0] ¦ posts[-1,2147483647,0] 𝄀 +1 ¦ Zero ¦ [] 𝄀 +2 ¦ One ¦ [{"authorId": 2, "id": 20, "title": "Only"}] 𝄀 +3 ¦ Many ¦ [{"authorId": 3, "id": 30, "title": "First"}, {"authorId": 3, "id": 31, "title": null}] +SELECT `q`.`id`, JSON_LENGTH(`q`.`posts`) AS `post_count` FROM (SELECT `t0`.`id`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) FROM (SELECT `t3`.`__prisma_data__` FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`, 'authorId', `t2`.`authorId`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId`) AS `t2`) AS `t3`) AS `t4`) AS `posts` FROM `Author` AS `t0`) AS `q` ORDER BY `q`.`id`; +➤ id[4,32,0] ¦ post_count[-5,64,0] 𝄀 +1 ¦ 0 𝄀 +2 ¦ 1 𝄀 +3 ¦ 2 +SELECT `t0`.`id`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId` AND `t1`.`id` >= 30) AS `t2`) AS `t3`) AS `filtered_posts` FROM `Author` AS `t0` ORDER BY `t0`.`id`; +➤ id[4,32,0] ¦ filtered_posts[-1,2147483647,0] 𝄀 +1 ¦ [] 𝄀 +2 ¦ [] 𝄀 +3 ¦ [{"id": 30, "title": "First"}, {"id": 31, "title": null}] +SELECT `q`.`id`, JSON_LENGTH(`q`.`filtered_posts`) AS `filtered_post_count` FROM (SELECT `t0`.`id`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId` AND `t1`.`id` >= 30) AS `t2`) AS `t3`) AS `filtered_posts` FROM `Author` AS `t0`) AS `q` ORDER BY `q`.`id`; +➤ id[4,32,0] ¦ filtered_post_count[-5,64,0] 𝄀 +1 ¦ 0 𝄀 +2 ¦ 0 𝄀 +3 ¦ 2 +SELECT `t0`.`id`, (SELECT COUNT(*) FROM (SELECT `t1`.`id` FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId`) AS `t2`) AS `post_count` FROM `Author` AS `t0` ORDER BY `t0`.`id`; +➤ id[4,32,0] ¦ post_count[-5,64,0] 𝄀 +1 ¦ 0 𝄀 +2 ¦ 1 𝄀 +3 ¦ 2 +SELECT `t1`.`id`, (SELECT `t0`.`name` FROM `Author` AS `t0` WHERE `t0`.`id` = `t1`.`authorId`) AS `author_name` FROM `Post` AS `t1` ORDER BY `t1`.`id`; +➤ id[4,32,0] ¦ author_name[12,-1,0] 𝄀 +20 ¦ One 𝄀 +30 ¦ Many 𝄀 +31 ¦ Many +SELECT JSON_LENGTH(COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON))) AS `post_count` FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`, 'authorId', `t2`.`authorId`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t1`.`authorId` = 3) AS `t2`) AS `t3`; +➤ post_count[-5,64,0] 𝄀 +2 +SELECT `a`.`id` FROM `Author` AS `a` WHERE EXISTS (SELECT 1 WHERE EXISTS (SELECT 1 FROM (SELECT `p`.`id` FROM `Post` AS `p` WHERE `p`.`authorId` = `a`.`id`) AS `d`)); +-- @regex("correlated subquery in FROM clause is not yet implemented", true) +correlated subquery in FROM clause is not yet implemented +SELECT `a`.`id` FROM `Author` AS `a` WHERE EXISTS (SELECT 1 WHERE EXISTS (SELECT 1 FROM `Post` AS `p` WHERE EXISTS (SELECT 1 FROM (SELECT `a2`.`id` FROM `Author` AS `a2` WHERE `a2`.`id` = `a`.`id`) AS `d`))); +-- @regex("correlated subquery in FROM clause is not yet implemented", true) +correlated subquery in FROM clause is not yet implemented +SELECT `a`.`id` FROM `Author` AS `a` WHERE EXISTS (SELECT 1 FROM `Post` AS `p` WHERE EXISTS (SELECT 1 WHERE EXISTS (SELECT 1 FROM (SELECT `a2`.`id` FROM `Author` AS `a2` WHERE `a2`.`id` = `p`.`authorId`) AS `d`))); +-- @regex("correlated subquery in FROM clause is not yet implemented", true) +correlated subquery in FROM clause is not yet implemented drop database test_subq_corr_project; diff --git a/test/distributed/cases/subquery/scalar_correlated_projection.sql b/test/distributed/cases/subquery/scalar_correlated_projection.sql index 3d9fdc35892b5..51779165227cd 100644 --- a/test/distributed/cases/subquery/scalar_correlated_projection.sql +++ b/test/distributed/cases/subquery/scalar_correlated_projection.sql @@ -10,6 +10,10 @@ create table parent_agg (id int primary key, corr_key int); create table child_agg (corr_key int, v int); insert into parent_agg values (1, 10), (2, 20), (3, 30), (4, 30); insert into child_agg values (10, 5), (10, 7), (20, null); +create table `Author` (`id` int primary key, `name` varchar(32)); +create table `Post` (`id` int primary key, `title` varchar(32), `authorId` int); +insert into `Author` values (1, 'Zero'), (2, 'One'), (3, 'Many'); +insert into `Post` values (20, 'Only', 2), (30, 'First', 3), (31, null, 3); -- @case -- @desc:direct outer column projected by a correlated scalar subquery @@ -49,5 +53,27 @@ select p.id, (with correlated_input as (select c.v from child_agg c where c.corr select p.id, (select sum(c.v) from child_agg c where c.corr_key = p.corr_key group by c.corr_key) as grouped_sum from parent_agg p order by p.id; select p.id, (select sum(c.v) from child_agg c where c.corr_key = p.corr_key having sum(c.v) > 100) as having_sum from parent_agg p order by p.id; +-- @case +-- @desc:issue #24737 - Prisma 7.9.1 to-many relation join through transparent derived tables +-- @label:bvt +-- @ignore:2 +SELECT `t0`.`id`, `t0`.`name`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) AS `__prisma_data__` FROM (SELECT `t3`.`__prisma_data__` FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`, 'authorId', `t2`.`authorId`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId` /* root select */) AS `t2` /* inner select */) AS `t3` /* middle select */) AS `t4` /* outer select */) AS `posts` FROM `Author` AS `t0` ORDER BY `t0`.`id` ASC; +SELECT `q`.`id`, JSON_LENGTH(`q`.`posts`) AS `post_count` FROM (SELECT `t0`.`id`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) FROM (SELECT `t3`.`__prisma_data__` FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`, 'authorId', `t2`.`authorId`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId`) AS `t2`) AS `t3`) AS `t4`) AS `posts` FROM `Author` AS `t0`) AS `q` ORDER BY `q`.`id`; +-- @ignore:1 +SELECT `t0`.`id`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId` AND `t1`.`id` >= 30) AS `t2`) AS `t3`) AS `filtered_posts` FROM `Author` AS `t0` ORDER BY `t0`.`id`; +SELECT `q`.`id`, JSON_LENGTH(`q`.`filtered_posts`) AS `filtered_post_count` FROM (SELECT `t0`.`id`, (SELECT COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON)) FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId` AND `t1`.`id` >= 30) AS `t2`) AS `t3`) AS `filtered_posts` FROM `Author` AS `t0`) AS `q` ORDER BY `q`.`id`; +SELECT `t0`.`id`, (SELECT COUNT(*) FROM (SELECT `t1`.`id` FROM `Post` AS `t1` WHERE `t0`.`id` = `t1`.`authorId`) AS `t2`) AS `post_count` FROM `Author` AS `t0` ORDER BY `t0`.`id`; +SELECT `t1`.`id`, (SELECT `t0`.`name` FROM `Author` AS `t0` WHERE `t0`.`id` = `t1`.`authorId`) AS `author_name` FROM `Post` AS `t1` ORDER BY `t1`.`id`; +SELECT JSON_LENGTH(COALESCE(JSON_ARRAYAGG(`__prisma_data__`), CONVERT('[]', JSON))) AS `post_count` FROM (SELECT JSON_OBJECT('id', `t2`.`id`, 'title', `t2`.`title`, 'authorId', `t2`.`authorId`) AS `__prisma_data__` FROM (SELECT `t1`.* FROM `Post` AS `t1` WHERE `t1`.`authorId` = 3) AS `t2`) AS `t3`; + +-- Empty query blocks are real correlation levels even without FROM bindings. +-- @regex("correlated subquery in FROM clause is not yet implemented",true) +SELECT `a`.`id` FROM `Author` AS `a` WHERE EXISTS (SELECT 1 WHERE EXISTS (SELECT 1 FROM (SELECT `p`.`id` FROM `Post` AS `p` WHERE `p`.`authorId` = `a`.`id`) AS `d`)); +-- Mixed empty/non-empty ancestor orderings must both stay fail-closed. +-- @regex("correlated subquery in FROM clause is not yet implemented",true) +SELECT `a`.`id` FROM `Author` AS `a` WHERE EXISTS (SELECT 1 WHERE EXISTS (SELECT 1 FROM `Post` AS `p` WHERE EXISTS (SELECT 1 FROM (SELECT `a2`.`id` FROM `Author` AS `a2` WHERE `a2`.`id` = `a`.`id`) AS `d`))); +-- @regex("correlated subquery in FROM clause is not yet implemented",true) +SELECT `a`.`id` FROM `Author` AS `a` WHERE EXISTS (SELECT 1 FROM `Post` AS `p` WHERE EXISTS (SELECT 1 WHERE EXISTS (SELECT 1 FROM (SELECT `a2`.`id` FROM `Author` AS `a2` WHERE `a2`.`id` = `p`.`authorId`) AS `d`))); + -- @teardown drop database test_subq_corr_project;