From 124206c92749da7b2ed3bf79d935d9443afdf6b2 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Wed, 1 Jul 2026 18:06:08 +0800 Subject: [PATCH 01/15] fix(plan): resolve grouping in rollup order by --- pkg/sql/plan/query_builder.go | 22 +++++++ pkg/sql/plan/query_builder_test.go | 66 +++++++++++++++++++++ test/distributed/cases/window/rollup.result | 35 +++++++++++ test/distributed/cases/window/rollup.sql | 30 +++++++++- 4 files changed, 152 insertions(+), 1 deletion(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 219aa0ff08710..af5b451b244da 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3094,6 +3094,7 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } leftClause = &tree.UnionClause{Type: tree.UNION, Left: leftClause, Right: stmt, All: true} } + rewriteGroupingSetOrderBy(astOrderBy, selectClause.Exprs) return builder.buildUnion(leftClause, astOrderBy, astLimit, astRankOption, ctx, isRoot) } } @@ -3798,6 +3799,27 @@ func (builder *QueryBuilder) bindTimeWindow( return } +func rewriteGroupingSetOrderBy(astOrderBy tree.OrderBy, selectList tree.SelectExprs) { + projectPosByAst := make(map[string]int64, len(selectList)) + for i, selectExpr := range selectList { + astKey := tree.String(selectExpr.Expr, dialect.MYSQL) + if _, exists := projectPosByAst[astKey]; !exists { + projectPosByAst[astKey] = int64(i + 1) + } + } + + for _, order := range astOrderBy { + if _, isOrdinal := order.Expr.(*tree.NumVal); isOrdinal { + continue + } + + if projectPos, ok := projectPosByAst[tree.String(order.Expr, dialect.MYSQL)]; ok { + projectPosText := strconv.FormatInt(projectPos, 10) + order.Expr = tree.NewNumVal(projectPos, projectPosText, false, tree.P_int64) + } + } +} + func (builder *QueryBuilder) bindOrderBy( ctx *BindContext, astOrderBy tree.OrderBy, diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 4c24375c475ce..f1cac090e0834 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -412,6 +412,72 @@ func TestQueryBuilder_bindGroupByOrdinalPosition(t *testing.T) { require.Equal(t, int64(1), funcExpr.F.Args[1].Expr.(*plan.Expr_Lit).Lit.Value.(*plan.Literal_I64Val).I64Val) } +func TestQueryBuilderBuildRollupOrderByGroupingExpression(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) as grouping_a, grouping(b) as grouping_b, count(*) + from select_test.bind_select + group by a, b with rollup + order by grouping(a), grouping(b)`, + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + query := queryPlan.GetQuery() + require.NotEmpty(t, query.Steps) + rootNode := query.Nodes[query.Steps[len(query.Steps)-1]] + require.Equal(t, plan.Node_PROJECT, rootNode.NodeType) + require.Len(t, rootNode.ProjectList, 3) + + var sortNode *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_SORT { + sortNode = node + break + } + } + require.NotNil(t, sortNode) + require.Len(t, sortNode.OrderBy, 2) + require.Equal(t, int32(0), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Equal(t, int32(1), sortNode.OrderBy[1].Expr.GetCol().ColPos) +} + +func TestRewriteGroupingSetOrderBy(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a, b) as grouping_ab, count(*) as row_count + from select_test.bind_select + group by a, b with rollup + order by grouping(a, b) desc, 2, grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs) + + rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(1), pos) + require.Equal(t, tree.Descending, selectStmt.OrderBy[0].Direction) + + existingPos, ok := selectStmt.OrderBy[1].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok = existingPos.Int64() + require.True(t, ok) + require.Equal(t, int64(2), pos) + + require.Equal(t, "grouping(a)", tree.String(selectStmt.OrderBy[2].Expr, dialect.MYSQL)) +} + func TestQueryBuilder_bindHaving(t *testing.T) { builder, bindCtx := genBuilderAndCtx() diff --git a/test/distributed/cases/window/rollup.result b/test/distributed/cases/window/rollup.result index bc23b58dd4c44..b793f0a935f8d 100644 --- a/test/distributed/cases/window/rollup.result +++ b/test/distributed/cases/window/rollup.result @@ -893,4 +893,39 @@ Year: 2021 70000.00 Year: 2022 150000.00 Year: Total 220000.00 drop table sales_mixed_types; +drop table if exists grouping_order; +create table grouping_order ( +region varchar(8), +product varchar(8), +amount int +); +insert into grouping_order values +(null, 'tea', 10), +(null, 'tea', 20), +('east', null, 5), +('east', 'coffee', 7), +('east', 'tea', null), +('west', 'tea', 3); +select +if(grouping(region), 'ROLLUP', if(region is null, 'NULL', region)) as region_label, +grouping(region) as grp_region, +if(grouping(product), 'ROLLUP', if(product is null, 'NULL', product)) as product_label, +grouping(product) as grp_product, +count(*) as count_all, +count(amount) as count_amount, +sum(amount) as sum_amount +from grouping_order +group by region, product with rollup +order by grouping(region), region_label, grouping(product), product_label; +region_label grp_region product_label grp_product count_all count_amount sum_amount +NULL 0 tea 0 2 2 30 +NULL 0 ROLLUP 1 2 2 30 +east 0 NULL 0 1 1 5 +east 0 coffee 0 1 1 7 +east 0 tea 0 1 0 null +east 0 ROLLUP 1 3 2 12 +west 0 tea 0 1 1 3 +west 0 ROLLUP 1 1 1 3 +ROLLUP 1 ROLLUP 1 6 5 45 +drop table grouping_order; drop database rollup_test; diff --git a/test/distributed/cases/window/rollup.sql b/test/distributed/cases/window/rollup.sql index e58bc146cec9f..10680c07475ad 100644 --- a/test/distributed/cases/window/rollup.sql +++ b/test/distributed/cases/window/rollup.sql @@ -592,4 +592,32 @@ group by order by year_label; drop table sales_mixed_types; -drop database rollup_test; \ No newline at end of file + +drop table if exists grouping_order; +create table grouping_order ( + region varchar(8), + product varchar(8), + amount int +); +insert into grouping_order values + (null, 'tea', 10), + (null, 'tea', 20), + ('east', null, 5), + ('east', 'coffee', 7), + ('east', 'tea', null), + ('west', 'tea', 3); + +select + if(grouping(region), 'ROLLUP', if(region is null, 'NULL', region)) as region_label, + grouping(region) as grp_region, + if(grouping(product), 'ROLLUP', if(product is null, 'NULL', product)) as product_label, + grouping(product) as grp_product, + count(*) as count_all, + count(amount) as count_amount, + sum(amount) as sum_amount +from grouping_order +group by region, product with rollup +order by grouping(region), region_label, grouping(product), product_label; + +drop table grouping_order; +drop database rollup_test; From 6a0dcf66f249e1d9cc36aef2bdde73b267d31b45 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Wed, 1 Jul 2026 22:27:47 +0800 Subject: [PATCH 02/15] fix(plan): normalize grouping order matching --- pkg/sql/plan/query_builder.go | 58 +++++++++++++++++++++++++++--- pkg/sql/plan/query_builder_test.go | 23 +++++++++++- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index af5b451b244da..d200337991aa4 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3800,11 +3800,21 @@ func (builder *QueryBuilder) bindTimeWindow( } func rewriteGroupingSetOrderBy(astOrderBy tree.OrderBy, selectList tree.SelectExprs) { - projectPosByAst := make(map[string]int64, len(selectList)) + selectAliases := make(map[string]struct{}, len(selectList)) + for _, selectExpr := range selectList { + if selectExpr.As != nil && !selectExpr.As.Empty() { + selectAliases[selectExpr.As.Compare()] = struct{}{} + } + } + + projectPosByKey := make(map[string]int64, len(selectList)) for i, selectExpr := range selectList { - astKey := tree.String(selectExpr.Expr, dialect.MYSQL) - if _, exists := projectPosByAst[astKey]; !exists { - projectPosByAst[astKey] = int64(i + 1) + key, ok := groupingSetOrderByKey(selectExpr.Expr, nil) + if !ok { + continue + } + if _, exists := projectPosByKey[key]; !exists { + projectPosByKey[key] = int64(i + 1) } } @@ -3813,13 +3823,51 @@ func rewriteGroupingSetOrderBy(astOrderBy tree.OrderBy, selectList tree.SelectEx continue } - if projectPos, ok := projectPosByAst[tree.String(order.Expr, dialect.MYSQL)]; ok { + key, ok := groupingSetOrderByKey(order.Expr, selectAliases) + if !ok { + continue + } + if projectPos, ok := projectPosByKey[key]; ok { projectPosText := strconv.FormatInt(projectPos, 10) order.Expr = tree.NewNumVal(projectPos, projectPosText, false, tree.P_int64) } } } +func groupingSetOrderByKey(astExpr tree.Expr, blockedAliases map[string]struct{}) (string, bool) { + funcExpr, ok := unwrapParenExpr(astExpr).(*tree.FuncExpr) + if !ok || funcExpr.FuncName == nil || funcExpr.FuncName.Compare() != "grouping" || funcExpr.WindowSpec != nil { + return "", false + } + + var key strings.Builder + key.WriteString(funcExpr.FuncName.Compare()) + key.WriteByte(':') + key.WriteString(strconv.Itoa(int(funcExpr.Type))) + + for _, arg := range funcExpr.Exprs { + col, ok := unwrapParenExpr(arg).(*tree.UnresolvedName) + if !ok || col.Star || col.NumParts == 0 { + return "", false + } + if col.NumParts == 1 { + if _, blocked := blockedAliases[col.ColName()]; blocked { + return "", false + } + } + + key.WriteByte('|') + for i := col.NumParts - 1; i >= 0; i-- { + if i < col.NumParts-1 { + key.WriteByte('.') + } + key.WriteString(col.CStrParts[i].Compare()) + } + } + + return key.String(), true +} + func (builder *QueryBuilder) bindOrderBy( ctx *BindContext, astOrderBy tree.OrderBy, diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index f1cac090e0834..e6852e597a8f0 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -419,7 +419,7 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpression(t *testing.T) { `select grouping(a) as grouping_a, grouping(b) as grouping_b, count(*) from select_test.bind_select group by a, b with rollup - order by grouping(a), grouping(b)`, + order by GROUPING(A), GROUPING(B)`, 1, ) require.NoError(t, err) @@ -478,6 +478,27 @@ func TestRewriteGroupingSetOrderBy(t *testing.T) { require.Equal(t, "grouping(a)", tree.String(selectStmt.OrderBy[2].Expr, dialect.MYSQL)) } +func TestRewriteGroupingSetOrderBySkipsAliasShadowing(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) as b, grouping(b) as grouping_b, count(*) + from select_test.bind_select + group by a, b with rollup + order by grouping(b)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs) + + _, rewritten := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.False(t, rewritten) + require.Equal(t, "grouping(b)", tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) +} + func TestQueryBuilder_bindHaving(t *testing.T) { builder, bindCtx := genBuilderAndCtx() From 40f6cef5c65754c513d2fc4f4446676616cf72eb Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Thu, 2 Jul 2026 11:13:51 +0800 Subject: [PATCH 03/15] fix(plan): resolve grouping order columns before aliases --- pkg/sql/plan/query_builder.go | 92 ++++++++++++++++++------ pkg/sql/plan/query_builder_test.go | 110 +++++++++++++++++++++++++++-- 2 files changed, 176 insertions(+), 26 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index d200337991aa4..72bd1dd601cc6 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3094,7 +3094,7 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } leftClause = &tree.UnionClause{Type: tree.UNION, Left: leftClause, Right: stmt, All: true} } - rewriteGroupingSetOrderBy(astOrderBy, selectClause.Exprs) + rewriteGroupingSetOrderBy(astOrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) return builder.buildUnion(leftClause, astOrderBy, astLimit, astRankOption, ctx, isRoot) } } @@ -3799,17 +3799,15 @@ func (builder *QueryBuilder) bindTimeWindow( return } -func rewriteGroupingSetOrderBy(astOrderBy tree.OrderBy, selectList tree.SelectExprs) { - selectAliases := make(map[string]struct{}, len(selectList)) - for _, selectExpr := range selectList { - if selectExpr.As != nil && !selectExpr.As.Empty() { - selectAliases[selectExpr.As.Compare()] = struct{}{} - } - } - +func rewriteGroupingSetOrderBy( + astOrderBy tree.OrderBy, + selectList tree.SelectExprs, + groupByExprsList []tree.Exprs, +) { + columnResolver := newGroupingSetColumnResolver(groupByExprsList) projectPosByKey := make(map[string]int64, len(selectList)) for i, selectExpr := range selectList { - key, ok := groupingSetOrderByKey(selectExpr.Expr, nil) + key, ok := groupingSetOrderByKey(selectExpr.Expr, columnResolver) if !ok { continue } @@ -3823,7 +3821,7 @@ func rewriteGroupingSetOrderBy(astOrderBy tree.OrderBy, selectList tree.SelectEx continue } - key, ok := groupingSetOrderByKey(order.Expr, selectAliases) + key, ok := groupingSetOrderByKey(order.Expr, columnResolver) if !ok { continue } @@ -3834,7 +3832,64 @@ func rewriteGroupingSetOrderBy(astOrderBy tree.OrderBy, selectList tree.SelectEx } } -func groupingSetOrderByKey(astExpr tree.Expr, blockedAliases map[string]struct{}) (string, bool) { +type groupingSetColumnResolver map[string]map[string]struct{} + +func newGroupingSetColumnResolver(groupByExprsList []tree.Exprs) groupingSetColumnResolver { + resolver := make(groupingSetColumnResolver) + for _, groupByExprs := range groupByExprsList { + for _, groupByExpr := range groupByExprs { + col, ok := unwrapParenExpr(groupByExpr).(*tree.UnresolvedName) + if !ok || col.Star || col.NumParts == 0 { + continue + } + + columnName := col.CStrParts[0].Compare() + if resolver[columnName] == nil { + resolver[columnName] = make(map[string]struct{}) + } + resolver[columnName][groupingSetColumnKey(col)] = struct{}{} + } + } + return resolver +} + +func (resolver groupingSetColumnResolver) resolve(col *tree.UnresolvedName) (string, bool) { + candidates := resolver[col.CStrParts[0].Compare()] + if len(candidates) == 0 { + return "", false + } + + exactKey := groupingSetColumnKey(col) + if col.NumParts > 1 { + if _, ok := candidates[exactKey]; ok { + return exactKey, true + } + } + if len(candidates) != 1 { + return "", false + } + for candidate := range candidates { + return candidate, true + } + return "", false +} + +func (resolver groupingSetColumnResolver) isAmbiguous(col *tree.UnresolvedName) bool { + return len(resolver[col.CStrParts[0].Compare()]) > 1 +} + +func groupingSetColumnKey(col *tree.UnresolvedName) string { + var key strings.Builder + for i := col.NumParts - 1; i >= 0; i-- { + if i < col.NumParts-1 { + key.WriteByte('.') + } + key.WriteString(col.CStrParts[i].Compare()) + } + return key.String() +} + +func groupingSetOrderByKey(astExpr tree.Expr, columnResolver groupingSetColumnResolver) (string, bool) { funcExpr, ok := unwrapParenExpr(astExpr).(*tree.FuncExpr) if !ok || funcExpr.FuncName == nil || funcExpr.FuncName.Compare() != "grouping" || funcExpr.WindowSpec != nil { return "", false @@ -3850,19 +3905,16 @@ func groupingSetOrderByKey(astExpr tree.Expr, blockedAliases map[string]struct{} if !ok || col.Star || col.NumParts == 0 { return "", false } - if col.NumParts == 1 { - if _, blocked := blockedAliases[col.ColName()]; blocked { + columnKey, ok := columnResolver.resolve(col) + if !ok { + if columnResolver.isAmbiguous(col) { return "", false } + columnKey = groupingSetColumnKey(col) } key.WriteByte('|') - for i := col.NumParts - 1; i >= 0; i-- { - if i < col.NumParts-1 { - key.WriteByte('.') - } - key.WriteString(col.CStrParts[i].Compare()) - } + key.WriteString(columnKey) } return key.String(), true diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index e6852e597a8f0..4063b50e7cdb5 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -446,6 +446,60 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpression(t *testing.T) { require.Equal(t, int32(1), sortNode.OrderBy[1].Expr.GetCol().ColPos) } +func TestRewriteGroupingSetOrderByMatchesQualifiedColumn(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) as grouping_a, count(*) + from select_test.bind_select as t + group by a with rollup + order by grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) + orderFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + + rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(1), pos) +} + +func TestQueryBuilderBuildRollupOrderByGroupingExpressionIgnoresAliasShadowing(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) as b, grouping(b) as grouping_b, count(*) + from select_test.bind_select + group by a, b with rollup + order by grouping(b)`, + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + query := queryPlan.GetQuery() + var sortNode *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_SORT { + sortNode = node + break + } + } + require.NotNil(t, sortNode) + require.Len(t, sortNode.OrderBy, 1) + require.NotNil(t, sortNode.OrderBy[0].Expr.GetCol()) + require.Equal(t, int32(1), sortNode.OrderBy[0].Expr.GetCol().ColPos) +} + func TestRewriteGroupingSetOrderBy(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), @@ -460,7 +514,7 @@ func TestRewriteGroupingSetOrderBy(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) @@ -478,7 +532,7 @@ func TestRewriteGroupingSetOrderBy(t *testing.T) { require.Equal(t, "grouping(a)", tree.String(selectStmt.OrderBy[2].Expr, dialect.MYSQL)) } -func TestRewriteGroupingSetOrderBySkipsAliasShadowing(t *testing.T) { +func TestRewriteGroupingSetOrderByIgnoresAliasShadowing(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -492,11 +546,55 @@ func TestRewriteGroupingSetOrderBySkipsAliasShadowing(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + + rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(2), pos) +} + +func TestRewriteGroupingSetOrderBySkipsAmbiguousColumn(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) as grouping_a, count(*) + from select_test.bind_select as t1, select_test.bind_select as t2 + group by t1.a, t2.a with rollup + order by grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + + require.Equal(t, "grouping(a)", tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) +} - _, rewritten := selectStmt.OrderBy[0].Expr.(*tree.NumVal) - require.False(t, rewritten) - require.Equal(t, "grouping(b)", tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) +func TestRewriteGroupingSetOrderByPreservesRawMatchForGroupAlias(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select a as group_a, grouping(a) as grouping_a, count(*) + from select_test.bind_select + group by group_a with rollup + order by grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + + rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(2), pos) } func TestQueryBuilder_bindHaving(t *testing.T) { From 1af56165f38fb9c09adaadd739f27c4e2a12ec0e Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Fri, 3 Jul 2026 14:47:56 +0800 Subject: [PATCH 04/15] fix(plan): rewrite nested grouping order expressions --- pkg/sql/plan/query_builder.go | 160 +++++++++++++++++++++++++---- pkg/sql/plan/query_builder_test.go | 149 +++++++++++++++++++++++++++ 2 files changed, 291 insertions(+), 18 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 5d831f5bbac29..76cee56101cb7 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "maps" + "reflect" "slices" "strconv" "strings" @@ -3889,35 +3890,158 @@ func groupingSetColumnKey(col *tree.UnresolvedName) string { return key.String() } +type groupingSetCStrRestore struct { + target **tree.CStr + value *tree.CStr +} + +type groupingSetFuncArgsRestore struct { + function *tree.FuncExpr + args tree.Exprs +} + +type groupingSetOrderByKeyNormalizer struct { + columnResolver groupingSetColumnResolver + containsGrouping bool + valid bool + cstrRestores []groupingSetCStrRestore + funcArgsRestores []groupingSetFuncArgsRestore +} + func groupingSetOrderByKey(astExpr tree.Expr, columnResolver groupingSetColumnResolver) (string, bool) { - funcExpr, ok := unwrapParenExpr(astExpr).(*tree.FuncExpr) - if !ok || funcExpr.FuncName == nil || funcExpr.FuncName.Compare() != "grouping" || funcExpr.WindowSpec != nil { + normalizer := &groupingSetOrderByKeyNormalizer{ + columnResolver: columnResolver, + valid: true, + } + defer normalizer.restore() + + walkGroupingSetOrderByExpr(unwrapParenExpr(astExpr), normalizer.visit) + if !normalizer.valid || !normalizer.containsGrouping { return "", false } + return tree.String(unwrapParenExpr(astExpr), dialect.MYSQL), true +} - var key strings.Builder - key.WriteString(funcExpr.FuncName.Compare()) - key.WriteByte(':') - key.WriteString(strconv.Itoa(int(funcExpr.Type))) +func (normalizer *groupingSetOrderByKeyNormalizer) visit(astExpr tree.Expr) bool { + switch expr := astExpr.(type) { + case *tree.FuncExpr: + if expr.FuncName == nil { + return true + } + normalizer.normalizeCStr(&expr.FuncName) + if expr.FuncName.Compare() != "grouping" { + return true + } - for _, arg := range funcExpr.Exprs { - col, ok := unwrapParenExpr(arg).(*tree.UnresolvedName) - if !ok || col.Star || col.NumParts == 0 { - return "", false + normalizer.containsGrouping = true + if expr.WindowSpec != nil { + normalizer.valid = false + return false } - columnKey, ok := columnResolver.resolve(col) - if !ok { - if columnResolver.isAmbiguous(col) { - return "", false + + originalArgs := append(tree.Exprs(nil), expr.Exprs...) + canonicalArgs := make(tree.Exprs, len(expr.Exprs)) + for i, arg := range expr.Exprs { + col, ok := unwrapParenExpr(arg).(*tree.UnresolvedName) + if !ok || col.Star || col.NumParts == 0 { + normalizer.valid = false + return false + } + columnKey, ok := normalizer.columnResolver.resolve(col) + if !ok { + if normalizer.columnResolver.isAmbiguous(col) { + normalizer.valid = false + return false + } + columnKey = groupingSetColumnKey(col) } - columnKey = groupingSetColumnKey(col) + canonicalArgs[i] = tree.NewUnresolvedColName(columnKey) } + normalizer.funcArgsRestores = append(normalizer.funcArgsRestores, groupingSetFuncArgsRestore{ + function: expr, + args: originalArgs, + }) + expr.Exprs = canonicalArgs + return false + case *tree.UnresolvedName: + for i := 0; i < expr.NumParts; i++ { + normalizer.normalizeCStr(&expr.CStrParts[i]) + } + case *tree.Subquery: + return false + } + return true +} + +func (normalizer *groupingSetOrderByKeyNormalizer) normalizeCStr(target **tree.CStr) { + if *target == nil { + return + } + normalizer.cstrRestores = append(normalizer.cstrRestores, groupingSetCStrRestore{ + target: target, + value: *target, + }) + *target = tree.NewCStr((*target).Compare(), 0) +} - key.WriteByte('|') - key.WriteString(columnKey) +func (normalizer *groupingSetOrderByKeyNormalizer) restore() { + for i := len(normalizer.funcArgsRestores) - 1; i >= 0; i-- { + restore := normalizer.funcArgsRestores[i] + restore.function.Exprs = restore.args + } + for i := len(normalizer.cstrRestores) - 1; i >= 0; i-- { + restore := normalizer.cstrRestores[i] + *restore.target = restore.value } +} + +func walkGroupingSetOrderByExpr(astExpr tree.Expr, visit func(tree.Expr) bool) { + visited := make(map[uintptr]struct{}) + var walk func(reflect.Value) + walk = func(value reflect.Value) { + if !value.IsValid() { + return + } + if value.Kind() == reflect.Interface { + if value.IsNil() { + return + } + walk(value.Elem()) + return + } + if value.Kind() == reflect.Pointer { + if value.IsNil() { + return + } + pointer := value.Pointer() + if _, ok := visited[pointer]; ok { + return + } + visited[pointer] = struct{}{} + if value.CanInterface() { + if expr, ok := value.Interface().(tree.Expr); ok && !visit(expr) { + return + } + } + walk(value.Elem()) + return + } - return key.String(), true + switch value.Kind() { + case reflect.Struct: + valueType := value.Type() + for i := 0; i < value.NumField(); i++ { + if valueType.Field(i).PkgPath == "" { + walk(value.Field(i)) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < value.Len(); i++ { + walk(value.Index(i)) + } + } + } + walk(reflect.ValueOf(astExpr)) } func (builder *QueryBuilder) bindOrderBy( diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 4063b50e7cdb5..a7c3825a59f36 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -500,6 +500,64 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpressionIgnoresAliasShadowing(t require.Equal(t, int32(1), sortNode.OrderBy[0].Expr.GetCol().ColPos) } +func TestQueryBuilderBuildRollupOrderByGroupingBinaryExpression(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) + 0 as ga, count(*) + from select_test.bind_select + group by a with rollup + order by grouping(a) + 0`, + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + query := queryPlan.GetQuery() + var sortNode *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_SORT { + sortNode = node + break + } + } + require.NotNil(t, sortNode) + require.Len(t, sortNode.OrderBy, 1) + require.NotNil(t, sortNode.OrderBy[0].Expr.GetCol()) + require.Equal(t, int32(0), sortNode.OrderBy[0].Expr.GetCol().ColPos) +} + +func TestQueryBuilderBuildRollupOrderByNestedGroupingExpression(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select if(grouping(a), 1, 0) as ga, count(*) + from select_test.bind_select + group by a with rollup + order by if(grouping(a), 1, 0)`, + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + query := queryPlan.GetQuery() + var sortNode *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_SORT { + sortNode = node + break + } + } + require.NotNil(t, sortNode) + require.Len(t, sortNode.OrderBy, 1) + require.NotNil(t, sortNode.OrderBy[0].Expr.GetCol()) + require.Equal(t, int32(0), sortNode.OrderBy[0].Expr.GetCol().ColPos) +} + func TestRewriteGroupingSetOrderBy(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), @@ -597,6 +655,97 @@ func TestRewriteGroupingSetOrderByPreservesRawMatchForGroupAlias(t *testing.T) { require.Equal(t, int64(2), pos) } +func TestRewriteGroupingSetOrderByNestedExpressionCanonicalization(t *testing.T) { + testCases := []struct { + name string + sql string + expectRewritten bool + }{ + { + name: "mixed case grouping", + sql: `select if(GROUPING(A), 'X', 'x') as grouping_a, count(*) + from select_test.bind_select + group by a with rollup + order by IF(grouping(a), 'X', 'x')`, + expectRewritten: true, + }, + { + name: "different literal case", + sql: `select if(grouping(a), 'X', 'same') as grouping_a, count(*) + from select_test.bind_select + group by a with rollup + order by if(grouping(a), 'x', 'same')`, + expectRewritten: false, + }, + { + name: "ordinary expression", + sql: `select a + 0 as adjusted_a, count(*) + from select_test.bind_select + group by a with rollup + order by a + 0`, + expectRewritten: false, + }, + { + name: "ambiguous grouping column", + sql: `select if(grouping(a), 1, 0) as grouping_a, count(*) + from select_test.bind_select as t1, select_test.bind_select as t2 + group by t1.a, t2.a with rollup + order by if(grouping(a), 1, 0)`, + expectRewritten: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + stmts, err := parsers.Parse(context.TODO(), dialect.MYSQL, testCase.sql, 1) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + selectExprBefore := tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL) + orderExprBefore := tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL) + rewriteGroupingSetOrderBy( + selectStmt.OrderBy, + selectClause.Exprs, + selectClause.GroupBy.GroupByExprsList, + ) + + _, rewritten := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.Equal(t, testCase.expectRewritten, rewritten) + require.Equal(t, selectExprBefore, tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL)) + if !testCase.expectRewritten { + require.Equal(t, orderExprBefore, tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) + } + }) + } +} + +func TestRewriteGroupingSetOrderByNestedExpressionMatchesQualifiedGroupingColumn(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select if(grouping(a), 1, 0) as grouping_a, count(*) + from select_test.bind_select as t + group by a with rollup + order by if(grouping(a), 1, 0)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) + groupingFunc := orderFunc.Exprs[0].(*tree.FuncExpr) + groupingFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) + rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + + rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(1), pos) +} + func TestQueryBuilder_bindHaving(t *testing.T) { builder, bindCtx := genBuilderAndCtx() From ca7653d388046a6ac2bc988876bb0e93a963b539 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Sat, 4 Jul 2026 00:56:34 +0800 Subject: [PATCH 05/15] fix(plan): bind grouping order keys in branches --- pkg/sql/plan/query_builder.go | 241 +++++++---------------------- pkg/sql/plan/query_builder_test.go | 116 +++++++++----- 2 files changed, 136 insertions(+), 221 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 76cee56101cb7..ef418671b61cf 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -2230,6 +2230,18 @@ func (builder *QueryBuilder) createQuery() (*Query, error) { } func (builder *QueryBuilder) buildUnion(stmt *tree.UnionClause, astOrderBy tree.OrderBy, astLimit *tree.Limit, astRankOption *tree.RankOption, ctx *BindContext, isRoot bool) (int32, error) { + return builder.buildUnionWithResultLen(stmt, astOrderBy, astLimit, astRankOption, ctx, isRoot, -1) +} + +func (builder *QueryBuilder) buildUnionWithResultLen( + stmt *tree.UnionClause, + astOrderBy tree.OrderBy, + astLimit *tree.Limit, + astRankOption *tree.RankOption, + ctx *BindContext, + isRoot bool, + visibleResultLen int, +) (int32, error) { if builder.isForUpdate { return 0, moerr.NewInternalError(builder.GetContext(), "not support select union for update") } @@ -2471,6 +2483,12 @@ func (builder *QueryBuilder) buildUnion(stmt *tree.UnionClause, astOrderBy tree. // Track the original number of columns before ORDER BY binding // ORDER BY may add new expressions to ctx.projects, but these should not be in the final output resultLen := len(ctx.projects) + if visibleResultLen >= 0 { + if visibleResultLen > resultLen { + return 0, moerr.NewInternalError(builder.GetContext(), "visible UNION result length exceeds project length") + } + resultLen = visibleResultLen + } // bind orderBy BEFORE creating PROJECT node, so that any new expressions // added to ctx.projects by ORDER BY are included in the PROJECT node @@ -2596,6 +2614,8 @@ func (builder *QueryBuilder) buildUnion(stmt *tree.UnionClause, astOrderBy tree. ctx.results = ctx.projects[:resultLen] } + ctx.headings = ctx.headings[:resultLen] + // set heading if isRoot { builder.qry.Headings = append(builder.qry.Headings, ctx.headings...) @@ -3064,6 +3084,8 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } } if len(selectClause.GroupBy.GroupByExprsList) > 1 && !selectClause.GroupBy.Apart { + visibleResultLen := len(selectClause.Exprs) + branchExprs := appendGroupingSetOrderByProjects(astOrderBy, selectClause.Exprs) groupingCount := len(selectClause.GroupBy.GroupByExprsList) selectStmts := make([]*tree.SelectClause, groupingCount) if groupingCount > 1 { @@ -3073,7 +3095,7 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } selectStmts[i] = &tree.SelectClause{ Distinct: selectClause.Distinct, - Exprs: selectClause.Exprs, + Exprs: branchExprs, From: selectClause.From, Where: selectClause.Where, GroupBy: &tree.GroupByClause{ @@ -3095,8 +3117,21 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } leftClause = &tree.UnionClause{Type: tree.UNION, Left: leftClause, Right: stmt, All: true} } - rewriteGroupingSetOrderBy(astOrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) - return builder.buildUnion(leftClause, astOrderBy, astLimit, astRankOption, ctx, isRoot) + if nodeID, err = builder.buildUnionWithResultLen( + leftClause, + astOrderBy, + astLimit, + astRankOption, + ctx, + false, + visibleResultLen, + ); err != nil { + return + } + if isRoot { + builder.qry.Headings = append(builder.qry.Headings, ctx.headings...) + } + return } } @@ -3800,199 +3835,35 @@ func (builder *QueryBuilder) bindTimeWindow( return } -func rewriteGroupingSetOrderBy( - astOrderBy tree.OrderBy, - selectList tree.SelectExprs, - groupByExprsList []tree.Exprs, -) { - columnResolver := newGroupingSetColumnResolver(groupByExprsList) - projectPosByKey := make(map[string]int64, len(selectList)) - for i, selectExpr := range selectList { - key, ok := groupingSetOrderByKey(selectExpr.Expr, columnResolver) - if !ok { - continue - } - if _, exists := projectPosByKey[key]; !exists { - projectPosByKey[key] = int64(i + 1) - } - } - +func appendGroupingSetOrderByProjects(astOrderBy tree.OrderBy, selectList tree.SelectExprs) tree.SelectExprs { + branchSelectList := append(tree.SelectExprs(nil), selectList...) for _, order := range astOrderBy { - if _, isOrdinal := order.Expr.(*tree.NumVal); isOrdinal { + if _, isOrdinal := order.Expr.(*tree.NumVal); isOrdinal || !containsGroupingFunction(order.Expr) { continue } - key, ok := groupingSetOrderByKey(order.Expr, columnResolver) - if !ok { - continue - } - if projectPos, ok := projectPosByKey[key]; ok { - projectPosText := strconv.FormatInt(projectPos, 10) - order.Expr = tree.NewNumVal(projectPos, projectPosText, false, tree.P_int64) - } - } -} - -type groupingSetColumnResolver map[string]map[string]struct{} - -func newGroupingSetColumnResolver(groupByExprsList []tree.Exprs) groupingSetColumnResolver { - resolver := make(groupingSetColumnResolver) - for _, groupByExprs := range groupByExprsList { - for _, groupByExpr := range groupByExprs { - col, ok := unwrapParenExpr(groupByExpr).(*tree.UnresolvedName) - if !ok || col.Star || col.NumParts == 0 { - continue - } - - columnName := col.CStrParts[0].Compare() - if resolver[columnName] == nil { - resolver[columnName] = make(map[string]struct{}) - } - resolver[columnName][groupingSetColumnKey(col)] = struct{}{} - } + branchSelectList = append(branchSelectList, tree.SelectExpr{Expr: order.Expr}) + projectPos := int64(len(branchSelectList)) + order.Expr = tree.NewNumVal(projectPos, strconv.FormatInt(projectPos, 10), false, tree.P_int64) } - return resolver + return branchSelectList } -func (resolver groupingSetColumnResolver) resolve(col *tree.UnresolvedName) (string, bool) { - candidates := resolver[col.CStrParts[0].Compare()] - if len(candidates) == 0 { - return "", false - } - - exactKey := groupingSetColumnKey(col) - if col.NumParts > 1 { - if _, ok := candidates[exactKey]; ok { - return exactKey, true - } - } - if len(candidates) != 1 { - return "", false - } - for candidate := range candidates { - return candidate, true - } - return "", false -} - -func (resolver groupingSetColumnResolver) isAmbiguous(col *tree.UnresolvedName) bool { - return len(resolver[col.CStrParts[0].Compare()]) > 1 -} - -func groupingSetColumnKey(col *tree.UnresolvedName) string { - var key strings.Builder - for i := col.NumParts - 1; i >= 0; i-- { - if i < col.NumParts-1 { - key.WriteByte('.') - } - key.WriteString(col.CStrParts[i].Compare()) - } - return key.String() -} - -type groupingSetCStrRestore struct { - target **tree.CStr - value *tree.CStr -} - -type groupingSetFuncArgsRestore struct { - function *tree.FuncExpr - args tree.Exprs -} - -type groupingSetOrderByKeyNormalizer struct { - columnResolver groupingSetColumnResolver - containsGrouping bool - valid bool - cstrRestores []groupingSetCStrRestore - funcArgsRestores []groupingSetFuncArgsRestore -} - -func groupingSetOrderByKey(astExpr tree.Expr, columnResolver groupingSetColumnResolver) (string, bool) { - normalizer := &groupingSetOrderByKeyNormalizer{ - columnResolver: columnResolver, - valid: true, - } - defer normalizer.restore() - - walkGroupingSetOrderByExpr(unwrapParenExpr(astExpr), normalizer.visit) - if !normalizer.valid || !normalizer.containsGrouping { - return "", false - } - return tree.String(unwrapParenExpr(astExpr), dialect.MYSQL), true -} - -func (normalizer *groupingSetOrderByKeyNormalizer) visit(astExpr tree.Expr) bool { - switch expr := astExpr.(type) { - case *tree.FuncExpr: - if expr.FuncName == nil { - return true - } - normalizer.normalizeCStr(&expr.FuncName) - if expr.FuncName.Compare() != "grouping" { - return true - } - - normalizer.containsGrouping = true - if expr.WindowSpec != nil { - normalizer.valid = false - return false - } - - originalArgs := append(tree.Exprs(nil), expr.Exprs...) - canonicalArgs := make(tree.Exprs, len(expr.Exprs)) - for i, arg := range expr.Exprs { - col, ok := unwrapParenExpr(arg).(*tree.UnresolvedName) - if !ok || col.Star || col.NumParts == 0 { - normalizer.valid = false +func containsGroupingFunction(astExpr tree.Expr) bool { + found := false + walkGroupingSetOrderByExpr(unwrapParenExpr(astExpr), func(expr tree.Expr) bool { + switch typedExpr := expr.(type) { + case *tree.FuncExpr: + if typedExpr.FuncName != nil && typedExpr.FuncName.Compare() == "grouping" { + found = true return false } - columnKey, ok := normalizer.columnResolver.resolve(col) - if !ok { - if normalizer.columnResolver.isAmbiguous(col) { - normalizer.valid = false - return false - } - columnKey = groupingSetColumnKey(col) - } - canonicalArgs[i] = tree.NewUnresolvedColName(columnKey) - } - normalizer.funcArgsRestores = append(normalizer.funcArgsRestores, groupingSetFuncArgsRestore{ - function: expr, - args: originalArgs, - }) - expr.Exprs = canonicalArgs - return false - case *tree.UnresolvedName: - for i := 0; i < expr.NumParts; i++ { - normalizer.normalizeCStr(&expr.CStrParts[i]) + case *tree.Subquery: + return false } - case *tree.Subquery: - return false - } - return true -} - -func (normalizer *groupingSetOrderByKeyNormalizer) normalizeCStr(target **tree.CStr) { - if *target == nil { - return - } - normalizer.cstrRestores = append(normalizer.cstrRestores, groupingSetCStrRestore{ - target: target, - value: *target, + return !found }) - *target = tree.NewCStr((*target).Compare(), 0) -} - -func (normalizer *groupingSetOrderByKeyNormalizer) restore() { - for i := len(normalizer.funcArgsRestores) - 1; i >= 0; i-- { - restore := normalizer.funcArgsRestores[i] - restore.function.Exprs = restore.args - } - for i := len(normalizer.cstrRestores) - 1; i >= 0; i-- { - restore := normalizer.cstrRestores[i] - *restore.target = restore.value - } + return found } func walkGroupingSetOrderByExpr(astExpr tree.Expr, visit func(tree.Expr) bool) { diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index a7c3825a59f36..8e3c7d2a0f0ba 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -442,11 +442,12 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpression(t *testing.T) { } require.NotNil(t, sortNode) require.Len(t, sortNode.OrderBy, 2) - require.Equal(t, int32(0), sortNode.OrderBy[0].Expr.GetCol().ColPos) - require.Equal(t, int32(1), sortNode.OrderBy[1].Expr.GetCol().ColPos) + require.Equal(t, int32(3), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Equal(t, int32(4), sortNode.OrderBy[1].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 5) } -func TestRewriteGroupingSetOrderByMatchesQualifiedColumn(t *testing.T) { +func TestQueryBuilderBuildRollupOrderByQualifiedGroupingColumn(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -462,13 +463,27 @@ func TestRewriteGroupingSetOrderByMatchesQualifiedColumn(t *testing.T) { selectClause := selectStmt.Select.(*tree.SelectClause) orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) orderFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) - rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok := rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(1), pos) + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + query := queryPlan.GetQuery() + rootNode := query.Nodes[query.Steps[len(query.Steps)-1]] + require.Equal(t, plan.Node_PROJECT, rootNode.NodeType) + require.Len(t, rootNode.ProjectList, len(selectClause.Exprs)) + require.Len(t, query.Headings, len(selectClause.Exprs)) + + var sortNode *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_SORT { + sortNode = node + break + } + } + require.NotNil(t, sortNode) + require.Len(t, sortNode.OrderBy, 1) + require.Equal(t, int32(2), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 3) } func TestQueryBuilderBuildRollupOrderByGroupingExpressionIgnoresAliasShadowing(t *testing.T) { @@ -497,7 +512,8 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpressionIgnoresAliasShadowing(t require.NotNil(t, sortNode) require.Len(t, sortNode.OrderBy, 1) require.NotNil(t, sortNode.OrderBy[0].Expr.GetCol()) - require.Equal(t, int32(1), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Equal(t, int32(3), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 4) } func TestQueryBuilderBuildRollupOrderByGroupingBinaryExpression(t *testing.T) { @@ -526,7 +542,8 @@ func TestQueryBuilderBuildRollupOrderByGroupingBinaryExpression(t *testing.T) { require.NotNil(t, sortNode) require.Len(t, sortNode.OrderBy, 1) require.NotNil(t, sortNode.OrderBy[0].Expr.GetCol()) - require.Equal(t, int32(0), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Equal(t, int32(2), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 3) } func TestQueryBuilderBuildRollupOrderByNestedGroupingExpression(t *testing.T) { @@ -555,10 +572,27 @@ func TestQueryBuilderBuildRollupOrderByNestedGroupingExpression(t *testing.T) { require.NotNil(t, sortNode) require.Len(t, sortNode.OrderBy, 1) require.NotNil(t, sortNode.OrderBy[0].Expr.GetCol()) - require.Equal(t, int32(0), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Equal(t, int32(2), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 3) +} + +func TestQueryBuilderBuildRollupOrderByAmbiguousGroupingColumn(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select count(*) + from select_test.bind_select as t1, select_test.bind_select as t2 + group by t1.a, t2.a with rollup + order by grouping(a)`, + 1, + ) + require.NoError(t, err) + + _, err = BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.Error(t, err) } -func TestRewriteGroupingSetOrderBy(t *testing.T) { +func TestAppendGroupingSetOrderByProjects(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -572,13 +606,15 @@ func TestRewriteGroupingSetOrderBy(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + require.Len(t, branchExprs, 4) + require.Len(t, selectClause.Exprs, 2) rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) - require.Equal(t, int64(1), pos) + require.Equal(t, int64(3), pos) require.Equal(t, tree.Descending, selectStmt.OrderBy[0].Direction) existingPos, ok := selectStmt.OrderBy[1].Expr.(*tree.NumVal) @@ -587,10 +623,14 @@ func TestRewriteGroupingSetOrderBy(t *testing.T) { require.True(t, ok) require.Equal(t, int64(2), pos) - require.Equal(t, "grouping(a)", tree.String(selectStmt.OrderBy[2].Expr, dialect.MYSQL)) + rewrittenPos, ok = selectStmt.OrderBy[2].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok = rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(4), pos) } -func TestRewriteGroupingSetOrderByIgnoresAliasShadowing(t *testing.T) { +func TestAppendGroupingSetOrderByProjectsIgnoresAliasShadowing(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -604,16 +644,17 @@ func TestRewriteGroupingSetOrderByIgnoresAliasShadowing(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + require.Len(t, branchExprs, 4) rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) - require.Equal(t, int64(2), pos) + require.Equal(t, int64(4), pos) } -func TestRewriteGroupingSetOrderBySkipsAmbiguousColumn(t *testing.T) { +func TestAppendGroupingSetOrderByProjectsDefersAmbiguousColumnToBinder(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -627,12 +668,17 @@ func TestRewriteGroupingSetOrderBySkipsAmbiguousColumn(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + require.Len(t, branchExprs, 3) - require.Equal(t, "grouping(a)", tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) + rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(3), pos) } -func TestRewriteGroupingSetOrderByPreservesRawMatchForGroupAlias(t *testing.T) { +func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -646,16 +692,17 @@ func TestRewriteGroupingSetOrderByPreservesRawMatchForGroupAlias(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + require.Len(t, branchExprs, 4) rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) - require.Equal(t, int64(2), pos) + require.Equal(t, int64(4), pos) } -func TestRewriteGroupingSetOrderByNestedExpressionCanonicalization(t *testing.T) { +func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { testCases := []struct { name string sql string @@ -675,7 +722,7 @@ func TestRewriteGroupingSetOrderByNestedExpressionCanonicalization(t *testing.T) from select_test.bind_select group by a with rollup order by if(grouping(a), 'x', 'same')`, - expectRewritten: false, + expectRewritten: true, }, { name: "ordinary expression", @@ -691,7 +738,7 @@ func TestRewriteGroupingSetOrderByNestedExpressionCanonicalization(t *testing.T) from select_test.bind_select as t1, select_test.bind_select as t2 group by t1.a, t2.a with rollup order by if(grouping(a), 1, 0)`, - expectRewritten: false, + expectRewritten: true, }, } @@ -704,11 +751,7 @@ func TestRewriteGroupingSetOrderByNestedExpressionCanonicalization(t *testing.T) selectClause := selectStmt.Select.(*tree.SelectClause) selectExprBefore := tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL) orderExprBefore := tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL) - rewriteGroupingSetOrderBy( - selectStmt.OrderBy, - selectClause.Exprs, - selectClause.GroupBy.GroupByExprsList, - ) + _ = appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) _, rewritten := selectStmt.OrderBy[0].Expr.(*tree.NumVal) require.Equal(t, testCase.expectRewritten, rewritten) @@ -720,7 +763,7 @@ func TestRewriteGroupingSetOrderByNestedExpressionCanonicalization(t *testing.T) } } -func TestRewriteGroupingSetOrderByNestedExpressionMatchesQualifiedGroupingColumn(t *testing.T) { +func TestAppendGroupingSetOrderByNestedQualifiedProject(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), dialect.MYSQL, @@ -737,13 +780,14 @@ func TestRewriteGroupingSetOrderByNestedExpressionMatchesQualifiedGroupingColumn orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) groupingFunc := orderFunc.Exprs[0].(*tree.FuncExpr) groupingFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) - rewriteGroupingSetOrderBy(selectStmt.OrderBy, selectClause.Exprs, selectClause.GroupBy.GroupByExprsList) + branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + require.Len(t, branchExprs, 3) rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) - require.Equal(t, int64(1), pos) + require.Equal(t, int64(3), pos) } func TestQueryBuilder_bindHaving(t *testing.T) { From 2b14bd5d6a44b813d31f158be722312917978e73 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Sat, 4 Jul 2026 20:39:49 +0800 Subject: [PATCH 06/15] fix: preserve rollup order by projections --- pkg/sql/plan/opt_misc.go | 2 +- pkg/sql/plan/query_builder.go | 191 +++++++++++++++++++++++++---- pkg/sql/plan/query_builder_test.go | 156 ++++++++++++++++++++--- 3 files changed, 305 insertions(+), 44 deletions(-) diff --git a/pkg/sql/plan/opt_misc.go b/pkg/sql/plan/opt_misc.go index dfc9533e9a9f6..6c06083dd7ec5 100644 --- a/pkg/sql/plan/opt_misc.go +++ b/pkg/sql/plan/opt_misc.go @@ -845,7 +845,7 @@ func (builder *QueryBuilder) rewriteEffectlessAggToProject(nodeID int32) { return } scan := builder.qry.Nodes[node.Children[0]] - if scan.NodeType != plan.Node_TABLE_SCAN { + if scan.NodeType != plan.Node_TABLE_SCAN || scan.TableDef == nil || scan.TableDef.Pkey == nil { return } groupCol := make([]int32, 0) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index ef418671b61cf..55ddbb2270437 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -2230,7 +2230,7 @@ func (builder *QueryBuilder) createQuery() (*Query, error) { } func (builder *QueryBuilder) buildUnion(stmt *tree.UnionClause, astOrderBy tree.OrderBy, astLimit *tree.Limit, astRankOption *tree.RankOption, ctx *BindContext, isRoot bool) (int32, error) { - return builder.buildUnionWithResultLen(stmt, astOrderBy, astLimit, astRankOption, ctx, isRoot, -1) + return builder.buildUnionWithResultLen(stmt, astOrderBy, astLimit, astRankOption, ctx, isRoot, 0) } func (builder *QueryBuilder) buildUnionWithResultLen( @@ -2240,7 +2240,7 @@ func (builder *QueryBuilder) buildUnionWithResultLen( astRankOption *tree.RankOption, ctx *BindContext, isRoot bool, - visibleResultLen int, + hiddenResultLen int, ) (int32, error) { if builder.isForUpdate { return 0, moerr.NewInternalError(builder.GetContext(), "not support select union for update") @@ -2483,12 +2483,10 @@ func (builder *QueryBuilder) buildUnionWithResultLen( // Track the original number of columns before ORDER BY binding // ORDER BY may add new expressions to ctx.projects, but these should not be in the final output resultLen := len(ctx.projects) - if visibleResultLen >= 0 { - if visibleResultLen > resultLen { - return 0, moerr.NewInternalError(builder.GetContext(), "visible UNION result length exceeds project length") - } - resultLen = visibleResultLen + if hiddenResultLen > resultLen { + return 0, moerr.NewInternalError(builder.GetContext(), "hidden UNION result length exceeds project length") } + resultLen -= hiddenResultLen // bind orderBy BEFORE creating PROJECT node, so that any new expressions // added to ctx.projects by ORDER BY are included in the PROJECT node @@ -3056,11 +3054,13 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR switch selectClause := stmt.Select.(type) { case *tree.SelectClause: if selectClause.GroupBy != nil { + groupByExprsList := selectClause.GroupBy.GroupByExprsList if selectClause.GroupBy.Rollup { - for i := len(selectClause.GroupBy.GroupByExprsList[0]) - 1; i > 0; i-- { - selectClause.GroupBy.GroupByExprsList = append(selectClause.GroupBy.GroupByExprsList, selectClause.GroupBy.GroupByExprsList[0][0:i]) + groupByExprsList = append([]tree.Exprs(nil), groupByExprsList...) + for i := len(groupByExprsList[0]) - 1; i > 0; i-- { + groupByExprsList = append(groupByExprsList, groupByExprsList[0][0:i]) } - selectClause.GroupBy.GroupByExprsList = append(selectClause.GroupBy.GroupByExprsList, nil) + groupByExprsList = append(groupByExprsList, nil) } if selectClause.GroupBy.Cube { subsets := func(Exprs []tree.Expr) [][]tree.Expr { @@ -3077,19 +3077,24 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR backtrack(0, []tree.Expr{}) return result } - Exprs := selectClause.GroupBy.GroupByExprsList[0] - selectClause.GroupBy.GroupByExprsList = nil + Exprs := groupByExprsList[0] + groupByExprsList = nil for _, subset := range subsets(Exprs) { - selectClause.GroupBy.GroupByExprsList = append(selectClause.GroupBy.GroupByExprsList, subset) + groupByExprsList = append(groupByExprsList, subset) } } - if len(selectClause.GroupBy.GroupByExprsList) > 1 && !selectClause.GroupBy.Apart { - visibleResultLen := len(selectClause.Exprs) - branchExprs := appendGroupingSetOrderByProjects(astOrderBy, selectClause.Exprs) - groupingCount := len(selectClause.GroupBy.GroupByExprsList) + if len(groupByExprsList) > 1 && !selectClause.GroupBy.Apart { + var branchExprs tree.SelectExprs + var unionOrderBy tree.OrderBy + branchExprs, unionOrderBy, err = prepareGroupingSetOrderByProjects(builder, astOrderBy, selectClause.Exprs) + if err != nil { + return 0, err + } + hiddenResultLen := len(branchExprs) - len(selectClause.Exprs) + groupingCount := len(groupByExprsList) selectStmts := make([]*tree.SelectClause, groupingCount) if groupingCount > 1 { - for i, list := range selectClause.GroupBy.GroupByExprsList { + for i, list := range groupByExprsList { if selectClause.Having != nil { selectClause.Having.RollupHaving = true } @@ -3099,7 +3104,7 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR From: selectClause.From, Where: selectClause.Where, GroupBy: &tree.GroupByClause{ - GroupByExprsList: selectClause.GroupBy.GroupByExprsList, + GroupByExprsList: groupByExprsList, GroupingSet: list, Apart: true, Cube: false, @@ -3119,12 +3124,12 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } if nodeID, err = builder.buildUnionWithResultLen( leftClause, - astOrderBy, + unionOrderBy, astLimit, astRankOption, ctx, false, - visibleResultLen, + hiddenResultLen, ); err != nil { return } @@ -3835,18 +3840,152 @@ func (builder *QueryBuilder) bindTimeWindow( return } -func appendGroupingSetOrderByProjects(astOrderBy tree.OrderBy, selectList tree.SelectExprs) tree.SelectExprs { +func prepareGroupingSetOrderByProjects( + builder *QueryBuilder, + astOrderBy tree.OrderBy, + selectList tree.SelectExprs, +) (tree.SelectExprs, tree.OrderBy, error) { branchSelectList := append(tree.SelectExprs(nil), selectList...) - for _, order := range astOrderBy { + unionOrderBy := make(tree.OrderBy, len(astOrderBy)) + for i, order := range astOrderBy { + orderCopy := *order + orderCopy.Expr = cloneTreeExpr(order.Expr) + unionOrderBy[i] = &orderCopy + if _, isOrdinal := order.Expr.(*tree.NumVal); isOrdinal || !containsGroupingFunction(order.Expr) { continue } - branchSelectList = append(branchSelectList, tree.SelectExpr{Expr: order.Expr}) + hiddenExpr := cloneTreeExpr(order.Expr) + protectedNames := protectGroupingFunctionArguments(hiddenExpr) + aliasCtx := NewBindContext(builder, nil) + for selectIdx := range selectList { + selectExpr := selectList[selectIdx] + if selectExpr.As == nil || selectExpr.As.Empty() { + continue + } + alias := selectExpr.As.Compare() + aliasCtx.aliasMap[alias] = &aliasItem{ + idx: int32(selectIdx), + astExpr: cloneTreeExpr(selectExpr.Expr), + } + } + var err error + hiddenExpr, err = aliasCtx.qualifyColumnNames(hiddenExpr, AliasBeforeColumn) + restoreProtectedGroupingFunctionArguments(protectedNames) + if err != nil { + return nil, nil, err + } + + branchSelectList = append(branchSelectList, tree.SelectExpr{Expr: hiddenExpr}) projectPos := int64(len(branchSelectList)) - order.Expr = tree.NewNumVal(projectPos, strconv.FormatInt(projectPos, 10), false, tree.P_int64) + orderCopy.Expr = tree.NewNumVal(projectPos, strconv.FormatInt(projectPos, 10), false, tree.P_int64) + } + return branchSelectList, unionOrderBy, nil +} + +func cloneTreeExpr(astExpr tree.Expr) tree.Expr { + if astExpr == nil { + return nil + } + cloned := cloneTreeValue(reflect.ValueOf(astExpr), make(map[treeClonePointer]reflect.Value)) + return cloned.Interface().(tree.Expr) +} + +type treeClonePointer struct { + typ reflect.Type + ptr uintptr +} + +func cloneTreeValue(value reflect.Value, visited map[treeClonePointer]reflect.Value) reflect.Value { + if !value.IsValid() { + return value + } + + switch value.Kind() { + case reflect.Interface: + if value.IsNil() { + return reflect.Zero(value.Type()) + } + cloned := cloneTreeValue(value.Elem(), visited) + result := reflect.New(value.Type()).Elem() + result.Set(cloned) + return result + case reflect.Pointer: + if value.IsNil() { + return reflect.Zero(value.Type()) + } + key := treeClonePointer{typ: value.Type(), ptr: value.Pointer()} + if cloned, ok := visited[key]; ok { + return cloned + } + result := reflect.New(value.Type().Elem()) + visited[key] = result + result.Elem().Set(value.Elem()) + cloneTreeStructFields(result.Elem(), value.Elem(), visited) + return result + case reflect.Struct: + result := reflect.New(value.Type()).Elem() + result.Set(value) + cloneTreeStructFields(result, value, visited) + return result + case reflect.Slice: + if value.IsNil() { + return reflect.Zero(value.Type()) + } + result := reflect.MakeSlice(value.Type(), value.Len(), value.Len()) + for i := 0; i < value.Len(); i++ { + result.Index(i).Set(cloneTreeValue(value.Index(i), visited)) + } + return result + case reflect.Array: + result := reflect.New(value.Type()).Elem() + for i := 0; i < value.Len(); i++ { + result.Index(i).Set(cloneTreeValue(value.Index(i), visited)) + } + return result + default: + return value + } +} + +func cloneTreeStructFields(dst, src reflect.Value, visited map[treeClonePointer]reflect.Value) { + for i := 0; i < src.NumField(); i++ { + if src.Type().Field(i).PkgPath != "" { + continue + } + dst.Field(i).Set(cloneTreeValue(src.Field(i), visited)) + } +} + +func protectGroupingFunctionArguments(astExpr tree.Expr) []*tree.UnresolvedName { + var protectedNames []*tree.UnresolvedName + walkGroupingSetOrderByExpr(astExpr, func(expr tree.Expr) bool { + function, ok := expr.(*tree.FuncExpr) + if !ok || function.FuncName == nil || function.FuncName.Compare() != "grouping" { + return true + } + for _, arg := range function.Exprs { + walkGroupingSetOrderByExpr(arg, func(argExpr tree.Expr) bool { + name, ok := argExpr.(*tree.UnresolvedName) + if ok && !name.Star && name.NumParts == 1 { + name.NumParts = 2 + name.CStrParts[1] = tree.NewCStr("__mo_grouping_argument__", 0) + protectedNames = append(protectedNames, name) + } + return true + }) + } + return false + }) + return protectedNames +} + +func restoreProtectedGroupingFunctionArguments(protectedNames []*tree.UnresolvedName) { + for _, name := range protectedNames { + name.NumParts = 1 + name.CStrParts[1] = nil } - return branchSelectList } func containsGroupingFunction(astExpr tree.Expr) bool { diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 8e3c7d2a0f0ba..e06a7fee3f3ac 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -17,6 +17,7 @@ package plan import ( "context" "encoding/json" + "fmt" "math" "strings" "testing" @@ -447,6 +448,123 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpression(t *testing.T) { require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 5) } +func TestQueryBuilderBuildRollupOrderByGroupingExpressionWithStar(t *testing.T) { + testCases := []struct { + name string + selectList string + }{ + {name: "star", selectList: "*"}, + {name: "qualified star", selectList: "t.*"}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + fmt.Sprintf(`select %s from select_test.bind_select as t + group by a, b, c with rollup order by grouping(a)`, testCase.selectList), + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + query := queryPlan.GetQuery() + rootNode := query.Nodes[query.Steps[len(query.Steps)-1]] + require.Equal(t, plan.Node_PROJECT, rootNode.NodeType) + require.Len(t, rootNode.ProjectList, 3) + require.Len(t, query.Headings, 3) + + var sortNode *plan.Node + for _, node := range query.Nodes { + if node.NodeType == plan.Node_SORT { + sortNode = node + break + } + } + require.NotNil(t, sortNode) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 4) + }) + } +} + +func TestQueryBuilderBuildRollupOrderByGroupingExpressionUsesSelectAlias(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select 100 as b, grouping(a) as grouping_a, count(*) + from select_test.bind_select + group by a, b with rollup + order by grouping(a) + b`, + 1, + ) + require.NoError(t, err) + + queryPlan, err := BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + + foundHiddenProject := false + for _, node := range queryPlan.GetQuery().Nodes { + if node.NodeType != plan.Node_PROJECT || len(node.ProjectList) != 4 { + continue + } + hiddenFunc := node.ProjectList[3].GetF() + if hiddenFunc == nil || hiddenFunc.Func.ObjName != "+" { + continue + } + foundHiddenProject = true + require.Len(t, hiddenFunc.Args, 2) + require.True(t, planExprContainsInt64Literal(hiddenFunc.Args[1], 100)) + } + require.True(t, foundHiddenProject) +} + +func planExprContainsInt64Literal(expr *plan.Expr, expected int64) bool { + if literal := expr.GetLit(); literal != nil && literal.GetI64Val() == expected { + return true + } + if function := expr.GetF(); function != nil { + for _, arg := range function.Args { + if planExprContainsInt64Literal(arg, expected) { + return true + } + } + } + return false +} + +func TestQueryBuilderBuildRollupOrderByGroupingExpressionIsReentrant(t *testing.T) { + for _, isPrepare := range []bool{false, true} { + t.Run(fmt.Sprintf("prepare=%t", isPrepare), func(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a), count(*) from select_test.bind_select + group by a with rollup order by grouping(a) + 1`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + orderExpr := tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL) + firstNodeCount := 0 + for buildIndex := range 2 { + queryPlan, buildErr := BuildPlan(NewMockCompilerContext(true), stmts[0], isPrepare) + require.NoError(t, buildErr) + require.NotNil(t, queryPlan.GetQuery()) + require.Equal(t, orderExpr, tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) + if buildIndex == 0 { + firstNodeCount = len(queryPlan.GetQuery().Nodes) + } else { + require.Len(t, queryPlan.GetQuery().Nodes, firstNodeCount) + } + } + }) + } +} + func TestQueryBuilderBuildRollupOrderByQualifiedGroupingColumn(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), @@ -606,24 +724,25 @@ func TestAppendGroupingSetOrderByProjects(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + require.NoError(t, err) require.Len(t, branchExprs, 4) require.Len(t, selectClause.Exprs, 2) - rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) require.Equal(t, int64(3), pos) require.Equal(t, tree.Descending, selectStmt.OrderBy[0].Direction) - existingPos, ok := selectStmt.OrderBy[1].Expr.(*tree.NumVal) + existingPos, ok := rewrittenOrderBy[1].Expr.(*tree.NumVal) require.True(t, ok) pos, ok = existingPos.Int64() require.True(t, ok) require.Equal(t, int64(2), pos) - rewrittenPos, ok = selectStmt.OrderBy[2].Expr.(*tree.NumVal) + rewrittenPos, ok = rewrittenOrderBy[2].Expr.(*tree.NumVal) require.True(t, ok) pos, ok = rewrittenPos.Int64() require.True(t, ok) @@ -644,10 +763,11 @@ func TestAppendGroupingSetOrderByProjectsIgnoresAliasShadowing(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + require.NoError(t, err) require.Len(t, branchExprs, 4) - rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) @@ -668,10 +788,11 @@ func TestAppendGroupingSetOrderByProjectsDefersAmbiguousColumnToBinder(t *testin selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + require.NoError(t, err) require.Len(t, branchExprs, 3) - rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) @@ -692,10 +813,11 @@ func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + require.NoError(t, err) require.Len(t, branchExprs, 4) - rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) @@ -751,14 +873,13 @@ func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { selectClause := selectStmt.Select.(*tree.SelectClause) selectExprBefore := tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL) orderExprBefore := tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL) - _ = appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + _, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + require.NoError(t, err) - _, rewritten := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + _, rewritten := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.Equal(t, testCase.expectRewritten, rewritten) require.Equal(t, selectExprBefore, tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL)) - if !testCase.expectRewritten { - require.Equal(t, orderExprBefore, tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) - } + require.Equal(t, orderExprBefore, tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) }) } } @@ -780,10 +901,11 @@ func TestAppendGroupingSetOrderByNestedQualifiedProject(t *testing.T) { orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) groupingFunc := orderFunc.Exprs[0].(*tree.FuncExpr) groupingFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) - branchExprs := appendGroupingSetOrderByProjects(selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + require.NoError(t, err) require.Len(t, branchExprs, 3) - rewrittenPos, ok := selectStmt.OrderBy[0].Expr.(*tree.NumVal) + rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := rewrittenPos.Int64() require.True(t, ok) From dd96c30160f1b3faf750d0accff9c4dccbdc57f7 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Sun, 5 Jul 2026 15:59:16 +0800 Subject: [PATCH 07/15] fix: update hint_cte rollup result after dedup fix The rollup projection-order fix removed duplicate subtotal/grand-total rows, so the CTE+ROLLUP case in hint_cte now returns 10 rows instead of the stale 15. Update the expected result accordingly. Co-Authored-By: Claude Opus 4.8 --- test/distributed/cases/hint/hint_cte.result | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/distributed/cases/hint/hint_cte.result b/test/distributed/cases/hint/hint_cte.result index cf7cd310ce637..d35e954fb0974 100644 --- a/test/distributed/cases/hint/hint_cte.result +++ b/test/distributed/cases/hint/hint_cte.result @@ -626,19 +626,14 @@ SELECT * FROM sales_rollup ORDER BY region, product; ➤ region[12,-1,0] ¦ product[12,-1,0] ¦ total_amount[3,38,2] 𝄀 null ¦ null ¦ 1750.00 𝄀 -null ¦ null ¦ 1750.00 𝄀 -East ¦ null ¦ 650.00 𝄀 East ¦ null ¦ 650.00 𝄀 East ¦ Product C ¦ 650.00 𝄀 North ¦ null ¦ 470.00 𝄀 -North ¦ null ¦ 470.00 𝄀 North ¦ Product A ¦ 250.00 𝄀 North ¦ Product B ¦ 220.00 𝄀 South ¦ null ¦ 450.00 𝄀 -South ¦ null ¦ 450.00 𝄀 South ¦ Product B ¦ 450.00 𝄀 West ¦ null ¦ 180.00 𝄀 -West ¦ null ¦ 180.00 𝄀 West ¦ Product A ¦ 180.00 /*+ { "rewrites" : { From 08c146594bcd81325731ca5aec8a1578d64bf9e7 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Mon, 6 Jul 2026 10:57:46 +0800 Subject: [PATCH 08/15] fix(plan): preserve SELECT DISTINCT contract for grouping-set order keys For SELECT DISTINCT over ROLLUP/CUBE, prepareGroupingSetOrderByProjects injected every GROUPING() ORDER BY expression as a hidden branch projection. That hidden key participated in branch-level DISTINCT and was then trimmed, bypassing the OrderBinder DISTINCT guard and letting distinct hidden keys collapse into duplicate visible rows. Now, for DISTINCT, an ORDER BY expression is only rewritten when it already matches a visible select-list expression (rewritten to that visible ordinal); otherwise it is rejected with the same error as the ordinary ORDER BY path. Non-distinct behavior is unchanged. Co-Authored-By: Claude Opus 4.8 --- pkg/sql/plan/query_builder.go | 94 +++++++++++++++++++++++++----- pkg/sql/plan/query_builder_test.go | 82 ++++++++++++++++++++++++-- 2 files changed, 155 insertions(+), 21 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 55ddbb2270437..2b71cbe73002e 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3086,7 +3086,7 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR if len(groupByExprsList) > 1 && !selectClause.GroupBy.Apart { var branchExprs tree.SelectExprs var unionOrderBy tree.OrderBy - branchExprs, unionOrderBy, err = prepareGroupingSetOrderByProjects(builder, astOrderBy, selectClause.Exprs) + branchExprs, unionOrderBy, err = prepareGroupingSetOrderByProjects(builder, astOrderBy, selectClause.Exprs, selectClause.Distinct) if err != nil { return 0, err } @@ -3844,9 +3844,30 @@ func prepareGroupingSetOrderByProjects( builder *QueryBuilder, astOrderBy tree.OrderBy, selectList tree.SelectExprs, + distinct bool, ) (tree.SelectExprs, tree.OrderBy, error) { branchSelectList := append(tree.SelectExprs(nil), selectList...) unionOrderBy := make(tree.OrderBy, len(astOrderBy)) + + // For SELECT DISTINCT we must not inject hidden order keys: they would + // participate in the branch-level DISTINCT and then be trimmed from the + // output, changing the visible result. Instead, an ORDER BY expression is + // only allowed when it already matches a visible select-list expression, + // in which case it is rewritten to that visible ordinal. Otherwise we + // reject it exactly like the ordinary ORDER BY path does. + visibleOrdinals := make(map[string]int) + if distinct { + for selectIdx := range selectList { + key, err := groupingOrderExprKey(builder, selectList, selectList[selectIdx].Expr) + if err != nil { + return nil, nil, err + } + if _, exists := visibleOrdinals[key]; !exists { + visibleOrdinals[key] = selectIdx + 1 + } + } + } + for i, order := range astOrderBy { orderCopy := *order orderCopy.Expr = cloneTreeExpr(order.Expr) @@ -3856,23 +3877,19 @@ func prepareGroupingSetOrderByProjects( continue } - hiddenExpr := cloneTreeExpr(order.Expr) - protectedNames := protectGroupingFunctionArguments(hiddenExpr) - aliasCtx := NewBindContext(builder, nil) - for selectIdx := range selectList { - selectExpr := selectList[selectIdx] - if selectExpr.As == nil || selectExpr.As.Empty() { - continue + if distinct { + key, err := groupingOrderExprKey(builder, selectList, order.Expr) + if err != nil { + return nil, nil, err } - alias := selectExpr.As.Compare() - aliasCtx.aliasMap[alias] = &aliasItem{ - idx: int32(selectIdx), - astExpr: cloneTreeExpr(selectExpr.Expr), + if projectPos, ok := visibleOrdinals[key]; ok { + orderCopy.Expr = tree.NewNumVal(int64(projectPos), strconv.FormatInt(int64(projectPos), 10), false, tree.P_int64) + continue } + return nil, nil, moerr.NewSyntaxError(builder.GetContext(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") } - var err error - hiddenExpr, err = aliasCtx.qualifyColumnNames(hiddenExpr, AliasBeforeColumn) - restoreProtectedGroupingFunctionArguments(protectedNames) + + hiddenExpr, err := qualifyGroupingOrderExpr(builder, selectList, order.Expr) if err != nil { return nil, nil, err } @@ -3884,6 +3901,53 @@ func prepareGroupingSetOrderByProjects( return branchSelectList, unionOrderBy, nil } +// qualifyGroupingOrderExpr clones astExpr and canonicalizes it the same way a +// grouping-set order key is materialized: GROUPING() arguments are protected +// from alias rewriting, then select-list aliases are resolved via +// AliasBeforeColumn. The returned expression is safe to append as a hidden +// branch projection. +func qualifyGroupingOrderExpr( + builder *QueryBuilder, + selectList tree.SelectExprs, + astExpr tree.Expr, +) (tree.Expr, error) { + qualified := cloneTreeExpr(astExpr) + protectedNames := protectGroupingFunctionArguments(qualified) + aliasCtx := NewBindContext(builder, nil) + for selectIdx := range selectList { + selectExpr := selectList[selectIdx] + if selectExpr.As == nil || selectExpr.As.Empty() { + continue + } + alias := selectExpr.As.Compare() + aliasCtx.aliasMap[alias] = &aliasItem{ + idx: int32(selectIdx), + astExpr: cloneTreeExpr(selectExpr.Expr), + } + } + qualified, err := aliasCtx.qualifyColumnNames(qualified, AliasBeforeColumn) + restoreProtectedGroupingFunctionArguments(protectedNames) + if err != nil { + return nil, err + } + return qualified, nil +} + +// groupingOrderExprKey returns a canonical string key for astExpr so that an +// ORDER BY expression can be matched against a visible select-list expression +// regardless of alias-vs-column spelling. +func groupingOrderExprKey( + builder *QueryBuilder, + selectList tree.SelectExprs, + astExpr tree.Expr, +) (string, error) { + qualified, err := qualifyGroupingOrderExpr(builder, selectList, astExpr) + if err != nil { + return "", err + } + return tree.String(qualified, dialect.MYSQL), nil +} + func cloneTreeExpr(astExpr tree.Expr) tree.Expr { if astExpr == nil { return nil diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index e06a7fee3f3ac..7511d1d90c04f 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -724,7 +724,7 @@ func TestAppendGroupingSetOrderByProjects(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 4) require.Len(t, selectClause.Exprs, 2) @@ -763,7 +763,7 @@ func TestAppendGroupingSetOrderByProjectsIgnoresAliasShadowing(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 4) @@ -788,7 +788,7 @@ func TestAppendGroupingSetOrderByProjectsDefersAmbiguousColumnToBinder(t *testin selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 3) @@ -813,7 +813,7 @@ func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 4) @@ -824,6 +824,76 @@ func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(t *testing.T) { require.Equal(t, int64(4), pos) } +// For SELECT DISTINCT, an ORDER BY expression that is already a visible +// select-list expression must be rewritten to that visible ordinal instead of +// being injected as a hidden order key, so DISTINCT semantics are preserved. +func TestAppendGroupingSetOrderByProjectsDistinctReusesVisibleProjection(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select distinct grouping(a), count(*) + from select_test.bind_select + group by a with rollup + order by grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, true) + require.NoError(t, err) + // No hidden order key appended: the branch list stays equal to the select list. + require.Len(t, branchExprs, len(selectClause.Exprs)) + + rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(1), pos) +} + +// For SELECT DISTINCT, an ORDER BY expression that is not a visible select-list +// expression must be rejected with the same error as the ordinary ORDER BY path +// rather than silently injecting a hidden order key. +func TestAppendGroupingSetOrderByProjectsDistinctRejectsHiddenKey(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select distinct grouping(a) + from select_test.bind_select + group by a with rollup + order by grouping(a) + a`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + _, _, err = prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, true) + require.Error(t, err) + require.Contains(t, err.Error(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") +} + +// End-to-end guard: the DISTINCT rejection must also fire through the full plan +// builder, confirming selectClause.Distinct is propagated into the rewrite. +func TestQueryBuilderBuildRollupOrderByGroupingExpressionDistinctRejectsHiddenKey(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select distinct grouping(a) + from select_test.bind_select + group by a with rollup + order by grouping(a) + a`, + 1, + ) + require.NoError(t, err) + + _, err = BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.Error(t, err) + require.Contains(t, err.Error(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") +} + func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { testCases := []struct { name string @@ -873,7 +943,7 @@ func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { selectClause := selectStmt.Select.(*tree.SelectClause) selectExprBefore := tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL) orderExprBefore := tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL) - _, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + _, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) _, rewritten := rewrittenOrderBy[0].Expr.(*tree.NumVal) @@ -901,7 +971,7 @@ func TestAppendGroupingSetOrderByNestedQualifiedProject(t *testing.T) { orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) groupingFunc := orderFunc.Exprs[0].(*tree.FuncExpr) groupingFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 3) From 30bd3650df3ebbfdf7507da2ab2e5ee1eb281ec8 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Mon, 6 Jul 2026 16:20:47 +0800 Subject: [PATCH 09/15] fix(plan): normalize GROUPING() args for DISTINCT comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For SELECT DISTINCT over ROLLUP/CUBE, the ORDER BY expression key used to match against visible select-list expressions was computed via tree.String without stripping table qualifiers or unwrapping redundant parentheses. This meant GROUPING(t.a) and GROUPING(a) produced different keys, so the DISTINCT guard could reject queries where the ORDER BY expression was already in the select list under a different spelling. Add normalizeGroupingArgsForComparison which strips table qualifiers (UnresolvedName.NumParts → 1) and unwraps ParenExpr from GROUPING() arguments before computing the comparison key. Note: the MO parser currently rejects GROUPING(t.a) and GROUPING((a)), so these cannot be reached through SQL today. The normalization serves as defense-in-depth for when the parser is extended, and for any scenario where qualifyColumnNames introduces qualifiers after alias expansion. Co-Authored-By: Claude Opus 4.8 --- pkg/sql/plan/query_builder.go | 64 +++++++++++++++++- pkg/sql/plan/query_builder_test.go | 100 +++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 2b71cbe73002e..218f7eac9e7c0 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3935,7 +3935,8 @@ func qualifyGroupingOrderExpr( // groupingOrderExprKey returns a canonical string key for astExpr so that an // ORDER BY expression can be matched against a visible select-list expression -// regardless of alias-vs-column spelling. +// regardless of alias-vs-column spelling, table qualifiers, or redundant +// parentheses. func groupingOrderExprKey( builder *QueryBuilder, selectList tree.SelectExprs, @@ -3945,9 +3946,70 @@ func groupingOrderExprKey( if err != nil { return "", err } + normalizeGroupingArgsForComparison(qualified) return tree.String(qualified, dialect.MYSQL), nil } +// normalizeGroupingArgsForComparison strips table qualifiers and unwraps +// redundant parentheses from GROUPING() arguments so that, for example, +// GROUPING(t.a), GROUPING(a), and GROUPING((a)) all canonicalize to the +// same comparison key used by the DISTINCT guard. +func normalizeGroupingArgsForComparison(astExpr tree.Expr) { + visited := make(map[uintptr]struct{}) + var walk func(reflect.Value) + walk = func(value reflect.Value) { + if !value.IsValid() { + return + } + if value.Kind() == reflect.Interface { + if value.IsNil() { + return + } + walk(value.Elem()) + return + } + if value.Kind() == reflect.Pointer { + if value.IsNil() { + return + } + pointer := value.Pointer() + if _, ok := visited[pointer]; ok { + return + } + visited[pointer] = struct{}{} + + if funcExpr, ok := value.Interface().(*tree.FuncExpr); ok && + funcExpr.FuncName != nil && + funcExpr.FuncName.Compare() == "grouping" { + for i := range funcExpr.Exprs { + funcExpr.Exprs[i] = unwrapParenExpr(funcExpr.Exprs[i]) + if name, ok := funcExpr.Exprs[i].(*tree.UnresolvedName); ok && + name.NumParts > 1 && !name.Star { + name.NumParts = 1 + } + } + return + } + walk(value.Elem()) + return + } + switch value.Kind() { + case reflect.Struct: + valueType := value.Type() + for i := 0; i < value.NumField(); i++ { + if valueType.Field(i).PkgPath == "" { + walk(value.Field(i)) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < value.Len(); i++ { + walk(value.Index(i)) + } + } + } + walk(reflect.ValueOf(astExpr)) +} + func cloneTreeExpr(astExpr tree.Expr) tree.Expr { if astExpr == nil { return nil diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 7511d1d90c04f..945b628c6bea5 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -894,6 +894,106 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpressionDistinctRejectsHiddenKe require.Contains(t, err.Error(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") } +// normalizeGroupingArgsForComparison strips table qualifiers from GROUPING() +// arguments so that GROUPING(t.a) normalizes to GROUPING(a) for comparison. +func TestNormalizeGroupingArgsForComparisonStripsTableQualifier(t *testing.T) { + groupingExpr := &tree.FuncExpr{ + FuncName: tree.NewCStr("grouping", 0), + Exprs: tree.Exprs{ + &tree.UnresolvedName{ + NumParts: 2, + CStrParts: [4]*tree.CStr{ + tree.NewCStr("a", 0), + tree.NewCStr("t", 0), + }, + }, + }, + } + normalizeGroupingArgsForComparison(groupingExpr) + arg, ok := groupingExpr.Exprs[0].(*tree.UnresolvedName) + require.True(t, ok) + require.Equal(t, 1, arg.NumParts) + require.Equal(t, "a", arg.CStrParts[0].Compare()) +} + +// normalizeGroupingArgsForComparison unwraps redundant parentheses from +// GROUPING() arguments so that GROUPING((a)) normalizes to GROUPING(a). +func TestNormalizeGroupingArgsForComparisonUnwrapsParenExpr(t *testing.T) { + groupingExpr := &tree.FuncExpr{ + FuncName: tree.NewCStr("grouping", 0), + Exprs: tree.Exprs{ + &tree.ParenExpr{ + Expr: &tree.UnresolvedName{ + NumParts: 1, + CStrParts: [4]*tree.CStr{ + tree.NewCStr("a", 0), + }, + }, + }, + }, + } + normalizeGroupingArgsForComparison(groupingExpr) + arg, ok := groupingExpr.Exprs[0].(*tree.UnresolvedName) + require.True(t, ok) + require.Equal(t, 1, arg.NumParts) + require.Equal(t, "a", arg.CStrParts[0].Compare()) +} + +// groupingOrderExprKey must return the same key for GROUPING(a) and +// GROUPING(t.a) so that the DISTINCT guard matches them as the same expression. +func TestGroupingOrderExprKeyNormalizesQualifiedColumn(t *testing.T) { + // Parse a query to obtain a well-formed GROUPING(a) FuncExpr. + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) from select_test.bind_select group by a with rollup`, + 1, + ) + require.NoError(t, err) + selectList := stmts[0].(*tree.Select).Select.(*tree.SelectClause).Exprs + unqualifiedGrouping := selectList[0].Expr + + // Clone it and replace the argument with a qualified t.a reference. + qualifiedGrouping := cloneTreeExpr(unqualifiedGrouping) + qualifiedGrouping.(*tree.FuncExpr).Exprs[0] = &tree.UnresolvedName{ + NumParts: 2, + CStrParts: [4]*tree.CStr{tree.NewCStr("a", 0), tree.NewCStr("t", 0)}, + } + + key1, err := groupingOrderExprKey(nil, nil, unqualifiedGrouping) + require.NoError(t, err) + key2, err := groupingOrderExprKey(nil, nil, qualifiedGrouping) + require.NoError(t, err) + require.Equal(t, key1, key2) +} + +// groupingOrderExprKey must return the same key for GROUPING((a)) and +// GROUPING(a) so that the DISTINCT guard matches them as the same expression. +func TestGroupingOrderExprKeyNormalizesParenExpr(t *testing.T) { + // Parse a query to obtain a well-formed GROUPING(a) FuncExpr. + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select grouping(a) from select_test.bind_select group by a with rollup`, + 1, + ) + require.NoError(t, err) + selectList := stmts[0].(*tree.Select).Select.(*tree.SelectClause).Exprs + unwrappedGrouping := selectList[0].Expr + + // Clone it and wrap the argument in a redundant ParenExpr. + wrappedGrouping := cloneTreeExpr(unwrappedGrouping) + wrappedGrouping.(*tree.FuncExpr).Exprs[0] = &tree.ParenExpr{ + Expr: wrappedGrouping.(*tree.FuncExpr).Exprs[0], + } + + key1, err := groupingOrderExprKey(nil, nil, unwrappedGrouping) + require.NoError(t, err) + key2, err := groupingOrderExprKey(nil, nil, wrappedGrouping) + require.NoError(t, err) + require.Equal(t, key1, key2) +} + func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { testCases := []struct { name string From 6813f01eae0cf92b1042d211e9ea0e330f4bf919 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Tue, 7 Jul 2026 11:08:20 +0800 Subject: [PATCH 10/15] fix: gofmt alignment in query_builder_test.go Co-Authored-By: Claude Opus 4.8 --- pkg/sql/plan/query_builder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 945b628c6bea5..75ef469ce0544 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -956,7 +956,7 @@ func TestGroupingOrderExprKeyNormalizesQualifiedColumn(t *testing.T) { // Clone it and replace the argument with a qualified t.a reference. qualifiedGrouping := cloneTreeExpr(unqualifiedGrouping) qualifiedGrouping.(*tree.FuncExpr).Exprs[0] = &tree.UnresolvedName{ - NumParts: 2, + NumParts: 2, CStrParts: [4]*tree.CStr{tree.NewCStr("a", 0), tree.NewCStr("t", 0)}, } From 05833c56d7fd0ee3571f7f2e659c6077bb697878 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Tue, 7 Jul 2026 11:36:24 +0800 Subject: [PATCH 11/15] fix(plan): fold case and unwrap outer parens in DISTINCT grouping order key The DISTINCT grouping-set ORDER BY match key preserved the original spelling, so equivalent expressions were wrongly rejected: SELECT DISTINCT GROUPING(A) ... ORDER BY grouping(a) -- func-name case SELECT DISTINCT GROUPING(a) ... ORDER BY (grouping(a)) -- outer parens groupingOrderExprKey now unwraps parentheses around the whole expression before qualification, and normalizeExprForComparison (renamed from normalizeGroupingArgsForComparison) folds every function name to lower case in addition to stripping GROUPING() argument qualifiers and inner parentheses. Case-sensitive string literals are left untouched. Co-Authored-By: Claude Opus 4.8 --- pkg/sql/plan/query_builder.go | 46 ++++++++----- pkg/sql/plan/query_builder_test.go | 101 +++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 24 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 218f7eac9e7c0..4edf8761e85e3 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3935,26 +3935,30 @@ func qualifyGroupingOrderExpr( // groupingOrderExprKey returns a canonical string key for astExpr so that an // ORDER BY expression can be matched against a visible select-list expression -// regardless of alias-vs-column spelling, table qualifiers, or redundant -// parentheses. +// regardless of alias-vs-column spelling, table qualifiers, function-name case, +// or redundant parentheses. func groupingOrderExprKey( builder *QueryBuilder, selectList tree.SelectExprs, astExpr tree.Expr, ) (string, error) { - qualified, err := qualifyGroupingOrderExpr(builder, selectList, astExpr) + // Unwrap parentheses wrapping the whole expression, e.g. (GROUPING(a)), + // before qualification so both ORDER BY and select-list spellings agree. + qualified, err := qualifyGroupingOrderExpr(builder, selectList, unwrapParenExpr(astExpr)) if err != nil { return "", err } - normalizeGroupingArgsForComparison(qualified) + normalizeExprForComparison(qualified) return tree.String(qualified, dialect.MYSQL), nil } -// normalizeGroupingArgsForComparison strips table qualifiers and unwraps -// redundant parentheses from GROUPING() arguments so that, for example, -// GROUPING(t.a), GROUPING(a), and GROUPING((a)) all canonicalize to the -// same comparison key used by the DISTINCT guard. -func normalizeGroupingArgsForComparison(astExpr tree.Expr) { +// normalizeExprForComparison rewrites a cloned expression into a canonical form +// for textual comparison: every function name is folded to lower case, and +// GROUPING() arguments have their table qualifiers stripped and redundant +// parentheses unwrapped. So, for example, GROUPING(A), grouping(a), +// GROUPING(t.a) and GROUPING((a)) all canonicalize to the same key, while +// case-sensitive string literals are left untouched. +func normalizeExprForComparison(astExpr tree.Expr) { visited := make(map[uintptr]struct{}) var walk func(reflect.Value) walk = func(value reflect.Value) { @@ -3978,17 +3982,23 @@ func normalizeGroupingArgsForComparison(astExpr tree.Expr) { } visited[pointer] = struct{}{} - if funcExpr, ok := value.Interface().(*tree.FuncExpr); ok && - funcExpr.FuncName != nil && - funcExpr.FuncName.Compare() == "grouping" { - for i := range funcExpr.Exprs { - funcExpr.Exprs[i] = unwrapParenExpr(funcExpr.Exprs[i]) - if name, ok := funcExpr.Exprs[i].(*tree.UnresolvedName); ok && - name.NumParts > 1 && !name.Star { - name.NumParts = 1 + if funcExpr, ok := value.Interface().(*tree.FuncExpr); ok { + isGrouping := false + if funcExpr.FuncName != nil { + // Fold the function-name case, e.g. GROUPING -> grouping. + isGrouping = strings.EqualFold(funcExpr.FuncName.Origin(), "grouping") + funcExpr.FuncName = tree.NewCStr(strings.ToLower(funcExpr.FuncName.Origin()), 0) + } + if isGrouping { + for i := range funcExpr.Exprs { + funcExpr.Exprs[i] = unwrapParenExpr(funcExpr.Exprs[i]) + if name, ok := funcExpr.Exprs[i].(*tree.UnresolvedName); ok && + name.NumParts > 1 && !name.Star { + name.NumParts = 1 + } } + return } - return } walk(value.Elem()) return diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 75ef469ce0544..f7ea59bf3bbe0 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -894,9 +894,43 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpressionDistinctRejectsHiddenKe require.Contains(t, err.Error(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") } -// normalizeGroupingArgsForComparison strips table qualifiers from GROUPING() +// End-to-end: for SELECT DISTINCT, an ORDER BY expression that differs from the +// select-list expression only by function-name case or by outer parentheses is +// a valid visible-projection match and must not be rejected. +func TestQueryBuilderBuildRollupOrderByGroupingExpressionDistinctAcceptsEquivalent(t *testing.T) { + testCases := []struct { + name string + sql string + }{ + { + name: "function name case", + sql: `select distinct GROUPING(A) + from select_test.bind_select + group by a with rollup + order by grouping(a)`, + }, + { + name: "outer parens", + sql: `select distinct grouping(a) + from select_test.bind_select + group by a with rollup + order by (grouping(a))`, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + stmts, err := parsers.Parse(context.TODO(), dialect.MYSQL, testCase.sql, 1) + require.NoError(t, err) + + _, err = BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.NoError(t, err) + }) + } +} + +// normalizeExprForComparison strips table qualifiers from GROUPING() // arguments so that GROUPING(t.a) normalizes to GROUPING(a) for comparison. -func TestNormalizeGroupingArgsForComparisonStripsTableQualifier(t *testing.T) { +func TestNormalizeExprForComparisonStripsTableQualifier(t *testing.T) { groupingExpr := &tree.FuncExpr{ FuncName: tree.NewCStr("grouping", 0), Exprs: tree.Exprs{ @@ -909,16 +943,16 @@ func TestNormalizeGroupingArgsForComparisonStripsTableQualifier(t *testing.T) { }, }, } - normalizeGroupingArgsForComparison(groupingExpr) + normalizeExprForComparison(groupingExpr) arg, ok := groupingExpr.Exprs[0].(*tree.UnresolvedName) require.True(t, ok) require.Equal(t, 1, arg.NumParts) require.Equal(t, "a", arg.CStrParts[0].Compare()) } -// normalizeGroupingArgsForComparison unwraps redundant parentheses from +// normalizeExprForComparison unwraps redundant parentheses from // GROUPING() arguments so that GROUPING((a)) normalizes to GROUPING(a). -func TestNormalizeGroupingArgsForComparisonUnwrapsParenExpr(t *testing.T) { +func TestNormalizeExprForComparisonUnwrapsParenExpr(t *testing.T) { groupingExpr := &tree.FuncExpr{ FuncName: tree.NewCStr("grouping", 0), Exprs: tree.Exprs{ @@ -932,13 +966,29 @@ func TestNormalizeGroupingArgsForComparisonUnwrapsParenExpr(t *testing.T) { }, }, } - normalizeGroupingArgsForComparison(groupingExpr) + normalizeExprForComparison(groupingExpr) arg, ok := groupingExpr.Exprs[0].(*tree.UnresolvedName) require.True(t, ok) require.Equal(t, 1, arg.NumParts) require.Equal(t, "a", arg.CStrParts[0].Compare()) } +// normalizeExprForComparison folds function-name case so that GROUPING(a) and +// grouping(a) canonicalize to the same comparison key. +func TestNormalizeExprForComparisonFoldsFunctionNameCase(t *testing.T) { + groupingExpr := &tree.FuncExpr{ + FuncName: tree.NewCStr("GROUPING", 0), + Exprs: tree.Exprs{ + &tree.UnresolvedName{ + NumParts: 1, + CStrParts: [4]*tree.CStr{tree.NewCStr("a", 0)}, + }, + }, + } + normalizeExprForComparison(groupingExpr) + require.Equal(t, "grouping", groupingExpr.FuncName.Origin()) +} + // groupingOrderExprKey must return the same key for GROUPING(a) and // GROUPING(t.a) so that the DISTINCT guard matches them as the same expression. func TestGroupingOrderExprKeyNormalizesQualifiedColumn(t *testing.T) { @@ -994,6 +1044,45 @@ func TestGroupingOrderExprKeyNormalizesParenExpr(t *testing.T) { require.Equal(t, key1, key2) } +// groupingOrderExprKey must return the same key for GROUPING(A) and grouping(a) +// so the DISTINCT guard treats function-name case as insignificant. +func TestGroupingOrderExprKeyFoldsFunctionNameCase(t *testing.T) { + upper := parseSingleSelectExpr(t, `select GROUPING(A) from select_test.bind_select group by a with rollup`) + lower := parseSingleSelectExpr(t, `select grouping(a) from select_test.bind_select group by a with rollup`) + + key1, err := groupingOrderExprKey(nil, nil, upper) + require.NoError(t, err) + key2, err := groupingOrderExprKey(nil, nil, lower) + require.NoError(t, err) + require.Equal(t, key1, key2) +} + +// groupingOrderExprKey must return the same key for (GROUPING(a)) and +// GROUPING(a) so the DISTINCT guard ignores parentheses wrapping the whole +// ORDER BY expression. +func TestGroupingOrderExprKeyUnwrapsOuterParens(t *testing.T) { + wrapped := parseSingleOrderByExpr(t, `select grouping(a) from select_test.bind_select group by a with rollup order by (grouping(a))`) + bare := parseSingleSelectExpr(t, `select grouping(a) from select_test.bind_select group by a with rollup`) + + key1, err := groupingOrderExprKey(nil, nil, wrapped) + require.NoError(t, err) + key2, err := groupingOrderExprKey(nil, nil, bare) + require.NoError(t, err) + require.Equal(t, key1, key2) +} + +func parseSingleSelectExpr(t *testing.T, sql string) tree.Expr { + stmts, err := parsers.Parse(context.TODO(), dialect.MYSQL, sql, 1) + require.NoError(t, err) + return stmts[0].(*tree.Select).Select.(*tree.SelectClause).Exprs[0].Expr +} + +func parseSingleOrderByExpr(t *testing.T, sql string) tree.Expr { + stmts, err := parsers.Parse(context.TODO(), dialect.MYSQL, sql, 1) + require.NoError(t, err) + return stmts[0].(*tree.Select).OrderBy[0].Expr +} + func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { testCases := []struct { name string From f6638040041902a998e186cb0bedabe732a7f291 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Tue, 7 Jul 2026 14:50:38 +0800 Subject: [PATCH 12/15] fix(plan): recurse into GROUPING() argument expressions during normalization normalizeExprForComparison returned after unwrapping GROUPING()'s direct arguments, which prevented the walker from descending into the argument expression subtrees. When a GROUPING() argument was itself an expression (e.g. GROUPING(abs(t.col))), qualifiers on names nested inside that expression were never stripped, leading to different string keys for semantically equivalent expressions. Remove the early return and add a generic UnresolvedName handler that strips qualifiers from ANY UnresolvedName in the expression tree, not just those that are direct children of GROUPING(). Note: the MO parser currently only accepts GROUPING(bare_column_name), so GROUPING(expr(...)) forms cannot be reached through SQL today. The fix is defense-in-depth for parser extensions and edge cases. Co-Authored-By: Claude Opus 4.8 --- pkg/sql/plan/query_builder.go | 5 ++++- pkg/sql/plan/query_builder_test.go | 31 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index cd69f6cfde658..c7765aa4aa32b 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3999,9 +3999,12 @@ func normalizeExprForComparison(astExpr tree.Expr) { name.NumParts = 1 } } - return } } + if name, ok := value.Interface().(*tree.UnresolvedName); ok && + name.NumParts > 1 && !name.Star { + name.NumParts = 1 + } walk(value.Elem()) return } diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index f7ea59bf3bbe0..d313dbf12690b 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -989,6 +989,37 @@ func TestNormalizeExprForComparisonFoldsFunctionNameCase(t *testing.T) { require.Equal(t, "grouping", groupingExpr.FuncName.Origin()) } +// normalizeExprForComparison must recurse into the expression subtrees of +// GROUPING() arguments so that qualifiers on nested expression arguments are +// also stripped. E.g. GROUPING(abs(t.col)) should canonicalize its inner t.col +// to col so the key matches GROUPING(abs(col)). +func TestNormalizeExprForComparisonStripsQualifierInsideGroupingArgExpr(t *testing.T) { + expr := &tree.FuncExpr{ + FuncName: tree.NewCStr("GROUPING", 0), + Exprs: tree.Exprs{ + &tree.FuncExpr{ + FuncName: tree.NewCStr("ABS", 0), + Exprs: tree.Exprs{ + &tree.UnresolvedName{ + NumParts: 2, + CStrParts: [4]*tree.CStr{ + tree.NewCStr("col", 0), + tree.NewCStr("t", 0), + }, + }, + }, + }, + }, + } + normalizeExprForComparison(expr) + require.Equal(t, "grouping", expr.FuncName.Origin()) + nestedFunc := expr.Exprs[0].(*tree.FuncExpr) + require.Equal(t, "abs", nestedFunc.FuncName.Origin()) + innerName := nestedFunc.Exprs[0].(*tree.UnresolvedName) + require.Equal(t, 1, innerName.NumParts) + require.Equal(t, "col", innerName.CStrParts[0].Compare()) +} + // groupingOrderExprKey must return the same key for GROUPING(a) and // GROUPING(t.a) so that the DISTINCT guard matches them as the same expression. func TestGroupingOrderExprKeyNormalizesQualifiedColumn(t *testing.T) { From 6706420491d2593fdc8e0d9323f0b70bf018679b Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Wed, 8 Jul 2026 14:32:38 +0800 Subject: [PATCH 13/15] fix(plan): scope qualifier stripping to GROUPING argument subtree only The previous normalizeExprForComparison had a generic UnresolvedName handler that stripped qualifiers from every name in the expression tree. This caused names outside GROUPING() to lose their qualifiers, making semantically different expressions compare equal: SELECT DISTINCT grouping(a) + t1.b ... ORDER BY grouping(a) + t2.b This should be rejected because t1.b != t2.b, but it was silently allowed because both canonicalized to "grouping(a) + b". Replace the generic handler with stripGroupingArgQualifiers that deeply walks only the GROUPING argument subtree and strips qualifiers there. Names outside GROUPING retain their qualifiers for correct DISTINCT matching. Co-Authored-By: Claude Opus 4.8 --- pkg/sql/plan/query_builder.go | 54 +++++++++++++++++++++++++++--- pkg/sql/plan/query_builder_test.go | 18 ++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index c7765aa4aa32b..f521f296dd270 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3994,13 +3994,59 @@ func normalizeExprForComparison(astExpr tree.Expr) { if isGrouping { for i := range funcExpr.Exprs { funcExpr.Exprs[i] = unwrapParenExpr(funcExpr.Exprs[i]) - if name, ok := funcExpr.Exprs[i].(*tree.UnresolvedName); ok && - name.NumParts > 1 && !name.Star { - name.NumParts = 1 - } + stripGroupingArgQualifiers(funcExpr.Exprs[i]) } } } + walk(value.Elem()) + return + } + switch value.Kind() { + case reflect.Struct: + valueType := value.Type() + for i := 0; i < value.NumField(); i++ { + if valueType.Field(i).PkgPath == "" { + walk(value.Field(i)) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < value.Len(); i++ { + walk(value.Index(i)) + } + } + } + walk(reflect.ValueOf(astExpr)) +} + +// stripGroupingArgQualifiers deeply walks an expression inside a GROUPING() +// argument and strips table qualifiers from every UnresolvedName. This is +// scoped to the GROUPING argument subtree only so that names outside +// GROUPING() (e.g. t1.b vs t2.b in grouping(a) + t1.b vs grouping(a) + t2.b) +// retain their qualifiers for correct DISTINCT expression matching. +func stripGroupingArgQualifiers(astExpr tree.Expr) { + visited := make(map[uintptr]struct{}) + var walk func(reflect.Value) + walk = func(value reflect.Value) { + if !value.IsValid() { + return + } + if value.Kind() == reflect.Interface { + if value.IsNil() { + return + } + walk(value.Elem()) + return + } + if value.Kind() == reflect.Pointer { + if value.IsNil() { + return + } + pointer := value.Pointer() + if _, ok := visited[pointer]; ok { + return + } + visited[pointer] = struct{}{} + if name, ok := value.Interface().(*tree.UnresolvedName); ok && name.NumParts > 1 && !name.Star { name.NumParts = 1 diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index d313dbf12690b..3722151835821 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -928,6 +928,24 @@ func TestQueryBuilderBuildRollupOrderByGroupingExpressionDistinctAcceptsEquivale } } +// For SELECT DISTINCT, qualifiers on names outside GROUPING() must be preserved +// so that e.g. t1.b and t2.b are not matched as the same expression. +func TestQueryBuilderBuildRollupOrderByGroupingExpressionDistinctPreservesOuterQualifiers(t *testing.T) { + // t1.b vs t2.b — the ORDER BY expression is NOT the visible select-list + // expression, so DISTINCT must reject it. + sql := `select distinct grouping(a) + t1.b + from (select a, b from select_test.bind_select) as t1, + (select b from select_test.bind_select) as t2 + group by t1.a, t1.b, t2.b with rollup + order by grouping(a) + t2.b` + stmts, err := parsers.Parse(context.TODO(), dialect.MYSQL, sql, 1) + require.NoError(t, err) + + _, err = BuildPlan(NewMockCompilerContext(true), stmts[0], false) + require.Error(t, err) + require.Contains(t, err.Error(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") +} + // normalizeExprForComparison strips table qualifiers from GROUPING() // arguments so that GROUPING(t.a) normalizes to GROUPING(a) for comparison. func TestNormalizeExprForComparisonStripsTableQualifier(t *testing.T) { From 146fd6f8a6e12fb098a21bd57da78a3b9d0743c0 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Wed, 8 Jul 2026 23:15:12 +0800 Subject: [PATCH 14/15] fix(plan): validate ORDER BY ordinals against visible select list in grouping-set path Hidden GROUPING() sort-key projections expanded the internal projection list, making positional ORDER BY references beyond the visible select list incorrectly valid. Now ordinals are validated against the visible length before hidden keys are appended, matching the ordinary ORDER BY contract. Co-Authored-By: Claude --- pkg/sql/plan/query_builder.go | 16 ++++++ pkg/sql/plan/query_builder_test.go | 79 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index f521f296dd270..569f82df1eb1e 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -3870,6 +3870,22 @@ func prepareGroupingSetOrderByProjects( } } + // Validate positional ORDER BY references against the visible select list + // length, not the hidden-expanded projection list. Hidden grouping-set + // projections are internal and must not be addressable via ordinal. + visibleLen := len(selectList) + for _, order := range astOrderBy { + if numVal, ok := order.Expr.(*tree.NumVal); ok && numVal.Kind() == tree.Int { + colPos, _ := numVal.Int64() + if numVal.Negative() { + colPos = -colPos + } + if colPos < 1 || int(colPos) > visibleLen { + return nil, nil, moerr.NewSyntaxErrorf(builder.GetContext(), "ORDER BY position %v is not in select list", colPos) + } + } + } + for i, order := range astOrderBy { orderCopy := *order orderCopy.Expr = cloneTreeExpr(order.Expr) diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 3722151835821..9bf515b0f152f 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -3697,3 +3697,82 @@ func TestBaseBinder_baseBindExpr(t *testing.T) { }) } } + +// TestAppendGroupingSetOrderByProjectsOrdinalExceedsVisibleLen verifies that a +// positional ORDER BY reference beyond the visible select list is rejected even +// when hidden GROUPING() projections would make it valid in the expanded list. +func TestAppendGroupingSetOrderByProjectsOrdinalExceedsVisibleLen(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select a, count(*) + from select_test.bind_select + group by a with rollup + order by 3, grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + _, _, err = prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.Error(t, err) + require.Contains(t, err.Error(), "ORDER BY position 3 is not in select list") +} + +// TestAppendGroupingSetOrderByProjectsOrdinalWithinVisibleLen verifies that a +// positional ORDER BY reference within the visible select list is accepted. +func TestAppendGroupingSetOrderByProjectsOrdinalWithinVisibleLen(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select a, count(*) + from select_test.bind_select + group by a with rollup + order by 2, grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + // visible: [a, count(*)] + hidden: [grouping(a)] = 3 + require.Len(t, branchExprs, 3) + + // First ORDER BY: ordinal 2 -> passes through + existingPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok := existingPos.Int64() + require.True(t, ok) + require.Equal(t, int64(2), pos) + + // Second ORDER BY: grouping(a) -> rewritten to hidden position 3 + rewrittenPos, ok := rewrittenOrderBy[1].Expr.(*tree.NumVal) + require.True(t, ok) + pos, ok = rewrittenPos.Int64() + require.True(t, ok) + require.Equal(t, int64(3), pos) +} + +// TestAppendGroupingSetOrderByProjectsOrdinalOneWithinVisibleLen verifies that +// ordinal 1 within a 2-column visible select list is accepted. +func TestAppendGroupingSetOrderByProjectsOrdinalOneWithinVisibleLen(t *testing.T) { + stmts, err := parsers.Parse( + context.TODO(), + dialect.MYSQL, + `select a, count(*) + from select_test.bind_select + group by a with rollup + order by 1, grouping(a)`, + 1, + ) + require.NoError(t, err) + + selectStmt := stmts[0].(*tree.Select) + selectClause := selectStmt.Select.(*tree.SelectClause) + branchExprs, _, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + require.Len(t, branchExprs, 3) +} From 6b86dce1e1cf70744f29e9445571b80c2a51c390 Mon Sep 17 00:00:00 2001 From: Cao Kai Date: Thu, 9 Jul 2026 21:42:48 +0800 Subject: [PATCH 15/15] fix(plan): resolve grouping-set ORDER BY ordinals after star-expansion The grouping-set ORDER BY path (prepareGroupingSetOrderByProjects) operated on the pre-star-expansion AST select list, causing two bugs when SELECT * was used: 1. Positional ORDER BY references were validated against len(raw AST list) instead of the star-expanded visible width, incorrectly rejecting valid ordinals like ORDER BY 3 when * expanded to 4+ visible columns. 2. GROUPING() sort keys were rewritten to ordinals computed in the raw AST space (len(branchSelectList)), pointing to wrong visible columns instead of the intended hidden grouping projections. Fix: Move ordinal validation and hidden-column resolution out of prepareGroupingSetOrderByProjects (where the true visible width is unknown) into buildUnionWithResultLen (where resultLen is already computed from the star-expanded projection). A new hiddenOrderIdx return value tracks which ORDER BY entries map to which appended hidden grouping key, resolved to ColPos = resultLen + k at binding time. - Removed the pre-expansion ordinal check that used len(selectList) - Added []int return (hiddenOrderIdx) to prepareGroupingSetOrderByProjects - Added hiddenOrderIdx parameter to buildUnionWithResultLen (nil = no-op) - Hidden keys resolve directly to star-expanded tail positions - User ordinals on the grouping-set path are validated against resultLen - Regular UNION path is unchanged (nil hiddenOrderIdx) Co-Authored-By: Claude --- pkg/sql/plan/query_builder.go | 104 ++- pkg/sql/plan/query_builder_test.go | 181 +++-- test/distributed/cases/window/rollup.result | 847 +++++++++++--------- test/distributed/cases/window/rollup.sql | 18 + 4 files changed, 646 insertions(+), 504 deletions(-) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 569f82df1eb1e..f2ed82a1fabd8 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -2230,7 +2230,7 @@ func (builder *QueryBuilder) createQuery() (*Query, error) { } func (builder *QueryBuilder) buildUnion(stmt *tree.UnionClause, astOrderBy tree.OrderBy, astLimit *tree.Limit, astRankOption *tree.RankOption, ctx *BindContext, isRoot bool) (int32, error) { - return builder.buildUnionWithResultLen(stmt, astOrderBy, astLimit, astRankOption, ctx, isRoot, 0) + return builder.buildUnionWithResultLen(stmt, astOrderBy, astLimit, astRankOption, ctx, isRoot, 0, nil) } func (builder *QueryBuilder) buildUnionWithResultLen( @@ -2241,6 +2241,13 @@ func (builder *QueryBuilder) buildUnionWithResultLen( ctx *BindContext, isRoot bool, hiddenResultLen int, + // hiddenOrderIdx is parallel to astOrderBy and only set on the grouping-set + // path (nil for ordinary UNION). hiddenOrderIdx[i] >= 0 means ORDER BY entry + // i references the k-th appended hidden grouping-set sort key; it is resolved + // against the star-expanded projection tail. When hiddenOrderIdx is non-nil, + // positional ORDER BY references are additionally validated against the + // visible width (resultLen) so hidden keys are not addressable by ordinal. + hiddenOrderIdx []int, ) (int32, error) { if builder.isForUpdate { return 0, moerr.NewInternalError(builder.GetContext(), "not support select union for update") @@ -2497,14 +2504,50 @@ func (builder *QueryBuilder) buildUnionWithResultLen( orderBinder := NewOrderBinder(projectionBinder, nil) orderBys = make([]*plan.OrderBySpec, 0, len(astOrderBy)) - for _, order := range astOrderBy { + for oi, order := range astOrderBy { if isNullAstExpr(unwrapParenExpr(order.Expr)) { continue } - expr, err := orderBinder.BindExpr(order.Expr) - if err != nil { - return 0, err + var expr *plan.Expr + switch { + case hiddenOrderIdx != nil && oi < len(hiddenOrderIdx) && hiddenOrderIdx[oi] >= 0: + // Reference to a hidden grouping-set sort key. Hidden projections + // occupy the star-expanded projection tail [resultLen, projectLen); + // the k-th appended hidden column lives at resultLen+k. + colPos := resultLen + hiddenOrderIdx[oi] + if colPos < 0 || colPos >= len(ctx.projects) { + return 0, moerr.NewInternalError(builder.GetContext(), "hidden grouping order column out of range") + } + expr = &plan.Expr{ + Typ: ctx.projects[colPos].Typ, + Expr: &plan.Expr_Col{ + Col: &plan.ColRef{ + RelPos: ctx.projectTag, + ColPos: int32(colPos), + }, + }, + } + default: + // On the grouping-set path a positional ORDER BY may only address + // the visible (star-expanded) select list, not hidden sort keys, + // so validate against resultLen rather than the full projection. + if hiddenOrderIdx != nil { + if numVal, ok := order.Expr.(*tree.NumVal); ok && numVal.Kind() == tree.Int { + colPos, _ := numVal.Int64() + if numVal.Negative() { + colPos = -colPos + } + if colPos < 1 || int(colPos) > resultLen { + return 0, moerr.NewSyntaxErrorf(builder.GetContext(), "ORDER BY position %v is not in select list", colPos) + } + } + } + var err error + expr, err = orderBinder.BindExpr(order.Expr) + if err != nil { + return 0, err + } } orderBy := &plan.OrderBySpec{ @@ -3088,7 +3131,8 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR if len(groupByExprsList) > 1 && !selectClause.GroupBy.Apart { var branchExprs tree.SelectExprs var unionOrderBy tree.OrderBy - branchExprs, unionOrderBy, err = prepareGroupingSetOrderByProjects(builder, astOrderBy, selectClause.Exprs, selectClause.Distinct) + var hiddenOrderIdx []int + branchExprs, unionOrderBy, hiddenOrderIdx, err = prepareGroupingSetOrderByProjects(builder, astOrderBy, selectClause.Exprs, selectClause.Distinct) if err != nil { return 0, err } @@ -3132,6 +3176,7 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR ctx, false, hiddenResultLen, + hiddenOrderIdx, ); err != nil { return } @@ -3847,10 +3892,24 @@ func prepareGroupingSetOrderByProjects( astOrderBy tree.OrderBy, selectList tree.SelectExprs, distinct bool, -) (tree.SelectExprs, tree.OrderBy, error) { +) (tree.SelectExprs, tree.OrderBy, []int, error) { branchSelectList := append(tree.SelectExprs(nil), selectList...) unionOrderBy := make(tree.OrderBy, len(astOrderBy)) + // hiddenOrderIdx[i] >= 0 marks ORDER BY entry i as a reference to the k-th + // appended hidden grouping-set sort key (0-based). The absolute projection + // position of a hidden key is only known in the star-expanded projection + // space, which does not exist yet here (the FROM clause is not bound). It is + // therefore finalized by buildUnionWithResultLen once the true visible width + // is known. All other entries stay -1. Note: we deliberately do NOT validate + // positional ORDER BY references here — selectList is the pre-star-expansion + // AST list, so len(selectList) is not the visible column count; that check + // also moves to buildUnionWithResultLen. + hiddenOrderIdx := make([]int, len(astOrderBy)) + for i := range hiddenOrderIdx { + hiddenOrderIdx[i] = -1 + } + // For SELECT DISTINCT we must not inject hidden order keys: they would // participate in the branch-level DISTINCT and then be trimmed from the // output, changing the visible result. Instead, an ORDER BY expression is @@ -3862,7 +3921,7 @@ func prepareGroupingSetOrderByProjects( for selectIdx := range selectList { key, err := groupingOrderExprKey(builder, selectList, selectList[selectIdx].Expr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } if _, exists := visibleOrdinals[key]; !exists { visibleOrdinals[key] = selectIdx + 1 @@ -3870,22 +3929,6 @@ func prepareGroupingSetOrderByProjects( } } - // Validate positional ORDER BY references against the visible select list - // length, not the hidden-expanded projection list. Hidden grouping-set - // projections are internal and must not be addressable via ordinal. - visibleLen := len(selectList) - for _, order := range astOrderBy { - if numVal, ok := order.Expr.(*tree.NumVal); ok && numVal.Kind() == tree.Int { - colPos, _ := numVal.Int64() - if numVal.Negative() { - colPos = -colPos - } - if colPos < 1 || int(colPos) > visibleLen { - return nil, nil, moerr.NewSyntaxErrorf(builder.GetContext(), "ORDER BY position %v is not in select list", colPos) - } - } - } - for i, order := range astOrderBy { orderCopy := *order orderCopy.Expr = cloneTreeExpr(order.Expr) @@ -3898,25 +3941,26 @@ func prepareGroupingSetOrderByProjects( if distinct { key, err := groupingOrderExprKey(builder, selectList, order.Expr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } if projectPos, ok := visibleOrdinals[key]; ok { orderCopy.Expr = tree.NewNumVal(int64(projectPos), strconv.FormatInt(int64(projectPos), 10), false, tree.P_int64) continue } - return nil, nil, moerr.NewSyntaxError(builder.GetContext(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") + return nil, nil, nil, moerr.NewSyntaxError(builder.GetContext(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") } hiddenExpr, err := qualifyGroupingOrderExpr(builder, selectList, order.Expr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } + // Record which appended hidden column this entry maps to (0-based). Its + // absolute ordinal is resolved later against the star-expanded width. + hiddenOrderIdx[i] = len(branchSelectList) - len(selectList) branchSelectList = append(branchSelectList, tree.SelectExpr{Expr: hiddenExpr}) - projectPos := int64(len(branchSelectList)) - orderCopy.Expr = tree.NewNumVal(projectPos, strconv.FormatInt(projectPos, 10), false, tree.P_int64) } - return branchSelectList, unionOrderBy, nil + return branchSelectList, unionOrderBy, hiddenOrderIdx, nil } // qualifyGroupingOrderExpr clones astExpr and canonicalizes it the same way a diff --git a/pkg/sql/plan/query_builder_test.go b/pkg/sql/plan/query_builder_test.go index 9bf515b0f152f..e0322f2a5811c 100644 --- a/pkg/sql/plan/query_builder_test.go +++ b/pkg/sql/plan/query_builder_test.go @@ -724,29 +724,22 @@ func TestAppendGroupingSetOrderByProjects(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, rewrittenOrderBy, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 4) require.Len(t, selectClause.Exprs, 2) - rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok := rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(3), pos) + // Hidden grouping keys are tracked positionally (k-th appended hidden column) + // and resolved to an absolute ordinal later against the star-expanded width. + // grouping(a, b) -> 0th hidden, ordinal 2 passes through, grouping(a) -> 1st hidden. + require.Equal(t, []int{0, -1, 1}, hiddenOrderIdx) require.Equal(t, tree.Descending, selectStmt.OrderBy[0].Direction) existingPos, ok := rewrittenOrderBy[1].Expr.(*tree.NumVal) require.True(t, ok) - pos, ok = existingPos.Int64() + pos, ok := existingPos.Int64() require.True(t, ok) require.Equal(t, int64(2), pos) - - rewrittenPos, ok = rewrittenOrderBy[2].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok = rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(4), pos) } func TestAppendGroupingSetOrderByProjectsIgnoresAliasShadowing(t *testing.T) { @@ -763,15 +756,12 @@ func TestAppendGroupingSetOrderByProjectsIgnoresAliasShadowing(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 4) - rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok := rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(4), pos) + // grouping(b) -> 0th appended hidden key (visible len 3, hidden at tail). + require.Equal(t, []int{0}, hiddenOrderIdx) } func TestAppendGroupingSetOrderByProjectsDefersAmbiguousColumnToBinder(t *testing.T) { @@ -788,15 +778,11 @@ func TestAppendGroupingSetOrderByProjectsDefersAmbiguousColumnToBinder(t *testin selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 3) - rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok := rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(3), pos) + require.Equal(t, []int{0}, hiddenOrderIdx) } func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(t *testing.T) { @@ -813,15 +799,11 @@ func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 4) - rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok := rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(4), pos) + require.Equal(t, []int{0}, hiddenOrderIdx) } // For SELECT DISTINCT, an ORDER BY expression that is already a visible @@ -841,10 +823,12 @@ func TestAppendGroupingSetOrderByProjectsDistinctReusesVisibleProjection(t *test selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, true) + branchExprs, rewrittenOrderBy, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, true) require.NoError(t, err) // No hidden order key appended: the branch list stays equal to the select list. require.Len(t, branchExprs, len(selectClause.Exprs)) + // DISTINCT rewrites to an existing visible ordinal, so nothing is hidden. + require.Equal(t, []int{-1}, hiddenOrderIdx) rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) @@ -870,7 +854,7 @@ func TestAppendGroupingSetOrderByProjectsDistinctRejectsHiddenKey(t *testing.T) selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - _, _, err = prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, true) + _, _, _, err = prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, true) require.Error(t, err) require.Contains(t, err.Error(), "for SELECT DISTINCT, ORDER BY expressions must appear in select list") } @@ -1181,11 +1165,12 @@ func TestAppendGroupingSetOrderByNestedProjects(t *testing.T) { selectClause := selectStmt.Select.(*tree.SelectClause) selectExprBefore := tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL) orderExprBefore := tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL) - _, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + _, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) - _, rewritten := rewrittenOrderBy[0].Expr.(*tree.NumVal) - require.Equal(t, testCase.expectRewritten, rewritten) + // A recognized grouping key is appended as a hidden column (idx >= 0); + // an ordinary expression is left for the binder (idx == -1). + require.Equal(t, testCase.expectRewritten, hiddenOrderIdx[0] >= 0) require.Equal(t, selectExprBefore, tree.String(selectClause.Exprs[0].Expr, dialect.MYSQL)) require.Equal(t, orderExprBefore, tree.String(selectStmt.OrderBy[0].Expr, dialect.MYSQL)) }) @@ -1209,15 +1194,11 @@ func TestAppendGroupingSetOrderByNestedQualifiedProject(t *testing.T) { orderFunc := selectStmt.OrderBy[0].Expr.(*tree.FuncExpr) groupingFunc := orderFunc.Exprs[0].(*tree.FuncExpr) groupingFunc.Exprs[0] = tree.NewUnresolvedName(tree.NewCStr("t", 0), tree.NewCStr("a", 0)) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 3) - rewrittenPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok := rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(3), pos) + require.Equal(t, []int{0}, hiddenOrderIdx) } func TestQueryBuilder_bindHaving(t *testing.T) { @@ -3698,30 +3679,9 @@ func TestBaseBinder_baseBindExpr(t *testing.T) { } } -// TestAppendGroupingSetOrderByProjectsOrdinalExceedsVisibleLen verifies that a -// positional ORDER BY reference beyond the visible select list is rejected even -// when hidden GROUPING() projections would make it valid in the expanded list. -func TestAppendGroupingSetOrderByProjectsOrdinalExceedsVisibleLen(t *testing.T) { - stmts, err := parsers.Parse( - context.TODO(), - dialect.MYSQL, - `select a, count(*) - from select_test.bind_select - group by a with rollup - order by 3, grouping(a)`, - 1, - ) - require.NoError(t, err) - - selectStmt := stmts[0].(*tree.Select) - selectClause := selectStmt.Select.(*tree.SelectClause) - _, _, err = prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) - require.Error(t, err) - require.Contains(t, err.Error(), "ORDER BY position 3 is not in select list") -} - // TestAppendGroupingSetOrderByProjectsOrdinalWithinVisibleLen verifies that a -// positional ORDER BY reference within the visible select list is accepted. +// positional ORDER BY reference within the visible select list passes through, +// while a grouping key is tracked as a hidden column. func TestAppendGroupingSetOrderByProjectsOrdinalWithinVisibleLen(t *testing.T) { stmts, err := parsers.Parse( context.TODO(), @@ -3736,24 +3696,18 @@ func TestAppendGroupingSetOrderByProjectsOrdinalWithinVisibleLen(t *testing.T) { selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, rewrittenOrderBy, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, rewrittenOrderBy, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) // visible: [a, count(*)] + hidden: [grouping(a)] = 3 require.Len(t, branchExprs, 3) - // First ORDER BY: ordinal 2 -> passes through + // First ORDER BY: ordinal 2 -> passes through; second: grouping(a) -> 0th hidden. + require.Equal(t, []int{-1, 0}, hiddenOrderIdx) existingPos, ok := rewrittenOrderBy[0].Expr.(*tree.NumVal) require.True(t, ok) pos, ok := existingPos.Int64() require.True(t, ok) require.Equal(t, int64(2), pos) - - // Second ORDER BY: grouping(a) -> rewritten to hidden position 3 - rewrittenPos, ok := rewrittenOrderBy[1].Expr.(*tree.NumVal) - require.True(t, ok) - pos, ok = rewrittenPos.Int64() - require.True(t, ok) - require.Equal(t, int64(3), pos) } // TestAppendGroupingSetOrderByProjectsOrdinalOneWithinVisibleLen verifies that @@ -3772,7 +3726,84 @@ func TestAppendGroupingSetOrderByProjectsOrdinalOneWithinVisibleLen(t *testing.T selectStmt := stmts[0].(*tree.Select) selectClause := selectStmt.Select.(*tree.SelectClause) - branchExprs, _, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) require.NoError(t, err) require.Len(t, branchExprs, 3) + require.Equal(t, []int{-1, 0}, hiddenOrderIdx) +} + +// TestGroupingSetOrderByOrdinalStarExpansion is an end-to-end guard for the +// star-expansion contract in the grouping-set ORDER BY path: +// - positional ORDER BY references are validated against the *visible*, +// star-expanded select list, not the raw AST list nor the hidden-expanded +// projection; and +// - a grouping() sort key resolves to its hidden projection column, not to a +// visible column, even when '*' widens the visible list. +// +// bind_select has columns (a, b, c), so `*, count(*)` yields 4 visible columns. +func TestGroupingSetOrderByOrdinalStarExpansion(t *testing.T) { + mock := NewMockOptimizer(false) + + // Valid visible ordinals (3 -> c, 4 -> count(*)) must be accepted even though + // the raw AST select list has only 2 entries. + pass := []string{ + "select *, count(*) from select_test.bind_select group by a, b, c with rollup order by 3, grouping(a)", + "select *, count(*) from select_test.bind_select group by a, b, c with rollup order by 4", + } + for _, sql := range pass { + _, err := runOneStmt(mock, t, sql) + require.NoError(t, err, "sql=%s", sql) + } + + // Ordinal 5 addresses a hidden grouping key (visible width is 4) and must be + // rejected; ordinal 6 is out of range entirely. + fail := []string{ + "select *, count(*) from select_test.bind_select group by a, b, c with rollup order by 5, grouping(a)", + "select *, count(*) from select_test.bind_select group by a, b, c with rollup order by 6", + } + for _, sql := range fail { + _, err := runOneStmt(mock, t, sql) + require.Error(t, err, "sql=%s", sql) + require.Contains(t, err.Error(), "is not in select list", "sql=%s", sql) + } +} + +// TestGroupingSetOrderByGroupingResolvesHiddenColumnWithStar verifies Bug 2: a +// grouping() ORDER BY key must sort by the appended hidden projection column, +// not by a visible column shifted in by '*' expansion. +func TestGroupingSetOrderByGroupingResolvesHiddenColumnWithStar(t *testing.T) { + mock := NewMockOptimizer(false) + + starPlan, err := runOneStmt(mock, t, + "select *, count(*) from select_test.bind_select group by a, b, c with rollup order by grouping(a)") + require.NoError(t, err) + starSort := firstSortColPos(t, starPlan) + + // Without '*' the visible list is [a, b, c, count(*)] and the hidden + // grouping(a) column lands right after it. With '*' the visible list is the + // same width, so the hidden column position must be identical. + explicitPlan, err := runOneStmt(mock, t, + "select a, b, c, count(*) from select_test.bind_select group by a, b, c with rollup order by grouping(a)") + require.NoError(t, err) + explicitSort := firstSortColPos(t, explicitPlan) + + require.Equal(t, explicitSort, starSort, + "grouping(a) must resolve to the same hidden column with and without '*'") + // The hidden grouping column sits past the 4 visible columns (0-based >= 4). + require.GreaterOrEqual(t, starSort, int32(4)) +} + +// firstSortColPos returns the ColPos of the first ORDER BY key of the first SORT +// node in the plan, failing the test if the key is not a plain column reference. +func firstSortColPos(t *testing.T, p *Plan) int32 { + for _, n := range p.GetQuery().Nodes { + if n.NodeType == plan.Node_SORT { + require.NotEmpty(t, n.OrderBy) + col := n.OrderBy[0].Expr.GetCol() + require.NotNil(t, col, "first sort key is not a column reference") + return col.ColPos + } + } + t.Fatal("no SORT node in plan") + return -1 } diff --git a/test/distributed/cases/window/rollup.result b/test/distributed/cases/window/rollup.result index b793f0a935f8d..6fbba36d9de6e 100644 --- a/test/distributed/cases/window/rollup.result +++ b/test/distributed/cases/window/rollup.result @@ -7,22 +7,22 @@ insert into sales values (2000, 4525.32321); insert into sales values (2001, 31214321.3432423); insert into sales values (null, null); select * from sales; -col1 col2 -2000.0 4525.32321 -2001.0 3.12143213432423E7 -null null +➤ col1[7,24,0] ¦ col2[8,54,0] 𝄀 +2000.0 ¦ 4525.32321 𝄀 +2001.0 ¦ 3.12143213432423E7 𝄀 +null ¦ null select col1, sum(col2) as profit from sales group by col1 with rollup; -col1 profit -2000.0 4525.32321 -2001.0 3.12143213432423E7 -null null -null 3.12188466664523E7 +➤ col1[7,24,0] ¦ profit[8,54,0] 𝄀 +null ¦ 3.12188466664523E7 𝄀 +2000.0 ¦ 4525.32321 𝄀 +2001.0 ¦ 3.12143213432423E7 𝄀 +null ¦ null select col1, avg(col2) as profit from sales group by col1 with rollup; -col1 profit -2000.0 4525.32321 -2001.0 3.12143213432423E7 -null null -null 1.560942333322615E7 +➤ col1[7,24,0] ¦ profit[8,54,0] 𝄀 +2000.0 ¦ 4525.32321 𝄀 +2001.0 ¦ 3.12143213432423E7 𝄀 +null ¦ null 𝄀 +null ¦ 1.560942333322615E7 drop table sales; drop table if exists sales; create table sales(year int, quarter int, amount int); @@ -35,67 +35,71 @@ insert into sales values (2022,2,350); insert into sales values (2022,3,400); insert into sales values (2022,4,450); select * from sales; -year quarter amount -2021 1 100 -2021 2 150 -2021 3 200 -2021 4 250 -2022 1 300 -2022 2 350 -2022 3 400 -2022 4 450 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ amount[4,32,0] 𝄀 +2021 ¦ 1 ¦ 100 𝄀 +2021 ¦ 2 ¦ 150 𝄀 +2021 ¦ 3 ¦ 200 𝄀 +2021 ¦ 4 ¦ 250 𝄀 +2022 ¦ 1 ¦ 300 𝄀 +2022 ¦ 2 ¦ 350 𝄀 +2022 ¦ 3 ¦ 400 𝄀 +2022 ¦ 4 ¦ 450 select year, quarter, sum(amount) as total_sales from sales group by year, quarter with rollup; -year quarter total_sales -2021 1 100 -2021 2 150 -2021 3 200 -2021 4 250 -2022 1 300 -2022 2 350 -2022 3 400 -2022 4 450 -null null 2200 -2021 null 700 -2022 null 1500 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ total_sales[-5,64,0] 𝄀 +null ¦ null ¦ 2200 𝄀 +2021 ¦ null ¦ 700 𝄀 +2022 ¦ null ¦ 1500 𝄀 +2021 ¦ 1 ¦ 100 𝄀 +2021 ¦ 2 ¦ 150 𝄀 +2021 ¦ 3 ¦ 200 𝄀 +2021 ¦ 4 ¦ 250 𝄀 +2022 ¦ 1 ¦ 300 𝄀 +2022 ¦ 2 ¦ 350 𝄀 +2022 ¦ 3 ¦ 400 𝄀 +2022 ¦ 4 ¦ 450 select year, quarter, avg(amount) as avg_sales from sales group by year, quarter with rollup; -year quarter avg_sales -2021 1 100.0 -2021 2 150.0 -2021 3 200.0 -2021 4 250.0 -2022 1 300.0 -2022 2 350.0 -2022 3 400.0 -2022 4 450.0 -null null 275.0 -2021 null 175.0 -2022 null 375.0 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ avg_sales[8,54,0] 𝄀 +null ¦ null ¦ 275.0 𝄀 +2021 ¦ null ¦ 175.0 𝄀 +2022 ¦ null ¦ 375.0 𝄀 +2021 ¦ 1 ¦ 100.0 𝄀 +2021 ¦ 2 ¦ 150.0 𝄀 +2021 ¦ 3 ¦ 200.0 𝄀 +2021 ¦ 4 ¦ 250.0 𝄀 +2022 ¦ 1 ¦ 300.0 𝄀 +2022 ¦ 2 ¦ 350.0 𝄀 +2022 ¦ 3 ¦ 400.0 𝄀 +2022 ¦ 4 ¦ 450.0 select year, quarter, count(amount) as avg_sales from sales group by year, quarter with rollup order by avg_sales desc; -year quarter avg_sales -null null 8 -2021 null 4 -2022 null 4 -2021 1 1 -2021 2 1 -2021 3 1 -2021 4 1 -2022 1 1 -2022 2 1 -2022 3 1 -2022 4 1 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ avg_sales[-5,64,0] 𝄀 +null ¦ null ¦ 8 𝄀 +2021 ¦ null ¦ 4 𝄀 +2022 ¦ null ¦ 4 𝄀 +2021 ¦ 1 ¦ 1 𝄀 +2021 ¦ 2 ¦ 1 𝄀 +2021 ¦ 3 ¦ 1 𝄀 +2021 ¦ 4 ¦ 1 𝄀 +2022 ¦ 1 ¦ 1 𝄀 +2022 ¦ 2 ¦ 1 𝄀 +2022 ¦ 3 ¦ 1 𝄀 +2022 ¦ 4 ¦ 1 show create table sales; -Table Create Table -sales CREATE TABLE `sales` (\n `year` int DEFAULT NULL,\n `quarter` int DEFAULT NULL,\n `amount` int DEFAULT NULL\n) +➤ Table[12,-1,0] ¦ Create Table[12,-1,0] 𝄀 +sales ¦ CREATE TABLE `sales` ( + `year` int DEFAULT NULL, + `quarter` int DEFAULT NULL, + `amount` int DEFAULT NULL +) select * from sales; -year quarter amount -2021 1 100 -2021 2 150 -2021 3 200 -2021 4 250 -2022 1 300 -2022 2 350 -2022 3 400 -2022 4 450 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ amount[4,32,0] 𝄀 +2021 ¦ 1 ¦ 100 𝄀 +2021 ¦ 2 ¦ 150 𝄀 +2021 ¦ 3 ¦ 200 𝄀 +2021 ¦ 4 ¦ 250 𝄀 +2022 ¦ 1 ¦ 300 𝄀 +2022 ¦ 2 ¦ 350 𝄀 +2022 ¦ 3 ¦ 400 𝄀 +2022 ¦ 4 ¦ 450 truncate sales; drop table sales; drop table if exists sales; @@ -124,14 +128,14 @@ sales group by year, quarter with rollup; -year quarter product total_sales -2022 Q1 null 2500.00 -2022 Q2 null 4500.00 -2023 Q1 null 6500.00 -2023 Q2 null 8500.00 -2022 null null 7000.00 -2023 null null 15000.00 -null null null 22000.00 +➤ year[4,32,0] ¦ quarter[1,2,0] ¦ product[12,4,0] ¦ total_sales[3,38,2] 𝄀 +2022 ¦ null ¦ null ¦ 7000.00 𝄀 +2023 ¦ null ¦ null ¦ 15000.00 𝄀 +2022 ¦ Q1 ¦ null ¦ 2500.00 𝄀 +2022 ¦ Q2 ¦ null ¦ 4500.00 𝄀 +2023 ¦ Q1 ¦ null ¦ 6500.00 𝄀 +2023 ¦ Q2 ¦ null ¦ 8500.00 𝄀 +null ¦ null ¦ null ¦ 22000.00 select year, quarter, @@ -143,22 +147,22 @@ group by year, quarter, product with rollup; -year quarter product total_sales -null null null 22000.00 -2022 null null 7000.00 -2023 null null 15000.00 -2022 Q1 null 2500.00 -2022 Q2 null 4500.00 -2023 Q1 null 6500.00 -2023 Q2 null 8500.00 -2022 Q1 Product A 1000.00 -2022 Q1 Product B 1500.00 -2022 Q2 Product A 2000.00 -2022 Q2 Product C 2500.00 -2023 Q1 Product A 3000.00 -2023 Q1 Product B 3500.00 -2023 Q2 Product B 4000.00 -2023 Q2 Product C 4500.00 +➤ year[4,32,0] ¦ quarter[1,2,0] ¦ product[12,0,0] ¦ total_sales[3,38,2] 𝄀 +null ¦ null ¦ null ¦ 22000.00 𝄀 +2022 ¦ Q1 ¦ null ¦ 2500.00 𝄀 +2022 ¦ Q2 ¦ null ¦ 4500.00 𝄀 +2023 ¦ Q1 ¦ null ¦ 6500.00 𝄀 +2023 ¦ Q2 ¦ null ¦ 8500.00 𝄀 +2022 ¦ Q1 ¦ Product A ¦ 1000.00 𝄀 +2022 ¦ Q1 ¦ Product B ¦ 1500.00 𝄀 +2022 ¦ Q2 ¦ Product A ¦ 2000.00 𝄀 +2022 ¦ Q2 ¦ Product C ¦ 2500.00 𝄀 +2023 ¦ Q1 ¦ Product A ¦ 3000.00 𝄀 +2023 ¦ Q1 ¦ Product B ¦ 3500.00 𝄀 +2023 ¦ Q2 ¦ Product B ¦ 4000.00 𝄀 +2023 ¦ Q2 ¦ Product C ¦ 4500.00 𝄀 +2022 ¦ null ¦ null ¦ 7000.00 𝄀 +2023 ¦ null ¦ null ¦ 15000.00 select year, quarter, @@ -173,18 +177,18 @@ product with rollup having (product is not null and quarter is not null) or (product is null and quarter is null); -year quarter product total_sales -null null null 22000.00 -2022 null null 7000.00 -2023 null null 15000.00 -2022 Q1 Product A 1000.00 -2022 Q1 Product B 1500.00 -2022 Q2 Product A 2000.00 -2022 Q2 Product C 2500.00 -2023 Q1 Product A 3000.00 -2023 Q1 Product B 3500.00 -2023 Q2 Product B 4000.00 -2023 Q2 Product C 4500.00 +➤ year[4,32,0] ¦ quarter[1,2,0] ¦ product[12,0,0] ¦ total_sales[3,38,2] 𝄀 +null ¦ null ¦ null ¦ 22000.00 𝄀 +2022 ¦ null ¦ null ¦ 7000.00 𝄀 +2023 ¦ null ¦ null ¦ 15000.00 𝄀 +2022 ¦ Q1 ¦ Product A ¦ 1000.00 𝄀 +2022 ¦ Q1 ¦ Product B ¦ 1500.00 𝄀 +2022 ¦ Q2 ¦ Product A ¦ 2000.00 𝄀 +2022 ¦ Q2 ¦ Product C ¦ 2500.00 𝄀 +2023 ¦ Q1 ¦ Product A ¦ 3000.00 𝄀 +2023 ¦ Q1 ¦ Product B ¦ 3500.00 𝄀 +2023 ¦ Q2 ¦ Product B ¦ 4000.00 𝄀 +2023 ¦ Q2 ¦ Product C ¦ 4500.00 select year, quarter, @@ -198,13 +202,13 @@ quarter, product with rollup having (product = 'Product A') or (product = 'Product B'); -year quarter product total_sales -2022 Q1 Product A 1000.00 -2022 Q1 Product B 1500.00 -2022 Q2 Product A 2000.00 -2023 Q1 Product A 3000.00 -2023 Q1 Product B 3500.00 -2023 Q2 Product B 4000.00 +➤ year[4,32,0] ¦ quarter[1,2,0] ¦ product[12,0,0] ¦ total_sales[3,38,2] 𝄀 +2022 ¦ Q1 ¦ Product A ¦ 1000.00 𝄀 +2022 ¦ Q1 ¦ Product B ¦ 1500.00 𝄀 +2022 ¦ Q2 ¦ Product A ¦ 2000.00 𝄀 +2023 ¦ Q1 ¦ Product A ¦ 3000.00 𝄀 +2023 ¦ Q1 ¦ Product B ¦ 3500.00 𝄀 +2023 ¦ Q2 ¦ Product B ¦ 4000.00 drop table sales; drop table if exists orders; drop table if exists order_items; @@ -223,11 +227,11 @@ insert into orders (order_id, customer_id, order_date, status) values (3, 3, '2024-01-03', 'Delivered'), (4, 1, '2024-01-04', 'Delivered'); select * from orders; -order_id customer_id order_date status -1 1 2024-01-01 Delivered -2 2 2024-01-02 Shipped -3 3 2024-01-03 Delivered -4 1 2024-01-04 Delivered +➤ order_id[4,32,0] ¦ customer_id[4,32,0] ¦ order_date[91,64,0] ¦ status[12,-1,0] 𝄀 +1 ¦ 1 ¦ 2024-01-01 ¦ Delivered 𝄀 +2 ¦ 2 ¦ 2024-01-02 ¦ Shipped 𝄀 +3 ¦ 3 ¦ 2024-01-03 ¦ Delivered 𝄀 +4 ¦ 1 ¦ 2024-01-04 ¦ Delivered create table order_items ( item_id int primary key, order_id int, @@ -242,12 +246,12 @@ insert into order_items (item_id, order_id, product_id, quantity, price) values (4, 3, 104, 1, 49.99), (5, 4, 101, 1, 19.99); select * from order_items; -item_id order_id product_id quantity price -1 1 101 2 19.99 -2 1 102 1 29.99 -3 2 103 3 9.99 -4 3 104 1 49.99 -5 4 101 1 19.99 +➤ item_id[4,32,0] ¦ order_id[4,32,0] ¦ product_id[4,32,0] ¦ quantity[4,32,0] ¦ price[3,10,2] 𝄀 +1 ¦ 1 ¦ 101 ¦ 2 ¦ 19.99 𝄀 +2 ¦ 1 ¦ 102 ¦ 1 ¦ 29.99 𝄀 +3 ¦ 2 ¦ 103 ¦ 3 ¦ 9.99 𝄀 +4 ¦ 3 ¦ 104 ¦ 1 ¦ 49.99 𝄀 +5 ¦ 4 ¦ 101 ¦ 1 ¦ 19.99 create table customers ( customer_id int primary key, first_name varchar(50), @@ -259,10 +263,10 @@ insert into customers (customer_id, first_name, last_name, city) values (2, 'Jane', 'Smith', 'Los Angeles'), (3, 'Alice', 'Johnson', 'Chicago'); select * from customers; -customer_id first_name last_name city -1 John Doe New York -2 Jane Smith Los Angeles -3 Alice Johnson Chicago +➤ customer_id[4,32,0] ¦ first_name[12,-1,0] ¦ last_name[12,-1,0] ¦ city[1,50,0] 𝄀 +1 ¦ John ¦ Doe ¦ New York 𝄀 +2 ¦ Jane ¦ Smith ¦ Los Angeles 𝄀 +3 ¦ Alice ¦ Johnson ¦ Chicago create table products ( product_id int primary key, product_name varchar(100), @@ -274,11 +278,11 @@ insert into products (product_id, product_name, category_id) values (103, 'Product C', 2), (104, 'Product D', 2); select * from products; -product_id product_name category_id -101 Product A 1 -102 Product B 1 -103 Product C 2 -104 Product D 2 +➤ product_id[4,32,0] ¦ product_name[12,-1,0] ¦ category_id[4,32,0] 𝄀 +101 ¦ Product A ¦ 1 𝄀 +102 ¦ Product B ¦ 1 𝄀 +103 ¦ Product C ¦ 2 𝄀 +104 ¦ Product D ¦ 2 create table categories ( category_id int primary key, category_name varchar(100) @@ -287,9 +291,9 @@ insert into categories (category_id, category_name) values (1, 'Electronics'), (2, 'Books'); select * from categories; -category_id category_name -1 Electronics -2 Books +➤ category_id[4,32,0] ¦ category_name[12,-1,0] 𝄀 +1 ¦ Electronics 𝄀 +2 ¦ Books select year(o.order_date) as order_year, month(o.order_date) as order_month, @@ -374,59 +378,59 @@ insert into uint_64 values (3824, 13289392, 123213.99898); insert into uint_64 values (2438294, 1, 2); insert into uint_64 values (3824, 13289392, 1); select * from uint_64; -i j k -18446744073709551615 2147483647 123214 -4294967295 2147483647 2 -18446744073709551615 1 2 -2147483647 23289483 123214 -13289392 2 2 -18446744073709551615 23289483 1 -3824 13289392 123214 -2438294 1 2 -3824 13289392 1 +➤ i[-5,64,0] ¦ j[-5,64,0] ¦ k[3,39,0] 𝄀 +18446744073709551615 ¦ 2147483647 ¦ 123214 𝄀 +4294967295 ¦ 2147483647 ¦ 2 𝄀 +18446744073709551615 ¦ 1 ¦ 2 𝄀 +2147483647 ¦ 23289483 ¦ 123214 𝄀 +13289392 ¦ 2 ¦ 2 𝄀 +18446744073709551615 ¦ 23289483 ¦ 1 𝄀 +3824 ¦ 13289392 ¦ 123214 𝄀 +2438294 ¦ 1 ¦ 2 𝄀 +3824 ¦ 13289392 ¦ 1 select i, j, sum(k) as total from uint_64 group by i, j with rollup; -i j total -null null 369652 -18446744073709551615 2147483647 123214 -4294967295 2147483647 2 -18446744073709551615 1 2 -2147483647 23289483 123214 -13289392 2 2 -18446744073709551615 23289483 1 -3824 13289392 123215 -2438294 1 2 -18446744073709551615 null 123217 -4294967295 null 2 -2147483647 null 123214 -13289392 null 2 -3824 null 123215 -2438294 null 2 +➤ i[-5,64,0] ¦ j[-5,64,0] ¦ total[3,39,0] 𝄀 +null ¦ null ¦ 369652 𝄀 +18446744073709551615 ¦ null ¦ 123217 𝄀 +4294967295 ¦ null ¦ 2 𝄀 +2147483647 ¦ null ¦ 123214 𝄀 +13289392 ¦ null ¦ 2 𝄀 +3824 ¦ null ¦ 123215 𝄀 +2438294 ¦ null ¦ 2 𝄀 +18446744073709551615 ¦ 2147483647 ¦ 123214 𝄀 +4294967295 ¦ 2147483647 ¦ 2 𝄀 +18446744073709551615 ¦ 1 ¦ 2 𝄀 +2147483647 ¦ 23289483 ¦ 123214 𝄀 +13289392 ¦ 2 ¦ 2 𝄀 +18446744073709551615 ¦ 23289483 ¦ 1 𝄀 +3824 ¦ 13289392 ¦ 123215 𝄀 +2438294 ¦ 1 ¦ 2 select i, j, sum(k) as total from uint_64 group by j, i with rollup; -i j total -null null 369652 -18446744073709551615 2147483647 123214 -4294967295 2147483647 2 -18446744073709551615 1 2 -2147483647 23289483 123214 -13289392 2 2 -18446744073709551615 23289483 1 -3824 13289392 123215 -2438294 1 2 -null 2147483647 123216 -null 1 4 -null 23289483 123215 -null 2 2 -null 13289392 123215 +➤ i[-5,64,0] ¦ j[-5,64,0] ¦ total[3,39,0] 𝄀 +null ¦ null ¦ 369652 𝄀 +null ¦ 2147483647 ¦ 123216 𝄀 +null ¦ 1 ¦ 4 𝄀 +null ¦ 23289483 ¦ 123215 𝄀 +null ¦ 2 ¦ 2 𝄀 +null ¦ 13289392 ¦ 123215 𝄀 +18446744073709551615 ¦ 2147483647 ¦ 123214 𝄀 +4294967295 ¦ 2147483647 ¦ 2 𝄀 +18446744073709551615 ¦ 1 ¦ 2 𝄀 +2147483647 ¦ 23289483 ¦ 123214 𝄀 +13289392 ¦ 2 ¦ 2 𝄀 +18446744073709551615 ¦ 23289483 ¦ 1 𝄀 +3824 ¦ 13289392 ¦ 123215 𝄀 +2438294 ¦ 1 ¦ 2 select i, j, sum(k) as total from @@ -435,15 +439,15 @@ group by j, i with rollup having (i is not null and j is not null); -i j total -18446744073709551615 2147483647 123214 -4294967295 2147483647 2 -18446744073709551615 1 2 -2147483647 23289483 123214 -13289392 2 2 -18446744073709551615 23289483 1 -3824 13289392 123215 -2438294 1 2 +➤ i[-5,64,0] ¦ j[-5,64,0] ¦ total[3,39,0] 𝄀 +18446744073709551615 ¦ 2147483647 ¦ 123214 𝄀 +4294967295 ¦ 2147483647 ¦ 2 𝄀 +18446744073709551615 ¦ 1 ¦ 2 𝄀 +2147483647 ¦ 23289483 ¦ 123214 𝄀 +13289392 ¦ 2 ¦ 2 𝄀 +18446744073709551615 ¦ 23289483 ¦ 1 𝄀 +3824 ¦ 13289392 ¦ 123215 𝄀 +2438294 ¦ 1 ¦ 2 drop table uint_64; drop table if exists sales; create table sales ( @@ -474,22 +478,22 @@ quarter, region with rollup order by total_sales desc; -year quarter region total_sales -null null null 220000.00 -2022 null null 150000.00 -2022 2 null 85000.00 -2021 null null 70000.00 -2022 1 null 65000.00 -2022 2 South 45000.00 -2021 2 null 45000.00 -2022 2 North 40000.00 -2022 1 South 35000.00 -2022 1 North 30000.00 -2021 2 South 25000.00 -2021 1 null 25000.00 -2021 2 North 20000.00 -2021 1 South 15000.00 -2021 1 North 10000.00 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ region[12,0,0] ¦ total_sales[3,38,2] 𝄀 +null ¦ null ¦ null ¦ 220000.00 𝄀 +2022 ¦ null ¦ null ¦ 150000.00 𝄀 +2022 ¦ 2 ¦ null ¦ 85000.00 𝄀 +2021 ¦ null ¦ null ¦ 70000.00 𝄀 +2022 ¦ 1 ¦ null ¦ 65000.00 𝄀 +2021 ¦ 2 ¦ null ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2021 ¦ 1 ¦ null ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 select if(grouping(year), 'Total', cast(year as char)) as year, if(grouping(quarter), 'Total', cast(quarter as char)) as quarter, @@ -501,22 +505,22 @@ group by year, quarter, region with rollup; -year quarter region total_sales -2021 Total Total 70000.00 -2022 Total Total 150000.00 -Total Total Total 220000.00 -2021 1 Total 25000.00 -2021 2 Total 45000.00 -2022 1 Total 65000.00 -2022 2 Total 85000.00 -2021 1 North 10000.00 -2021 1 South 15000.00 -2021 2 North 20000.00 -2021 2 South 25000.00 -2022 1 North 30000.00 -2022 1 South 35000.00 -2022 2 North 40000.00 -2022 2 South 45000.00 +➤ year[12,-1,0] ¦ quarter[12,-1,0] ¦ region[12,0,0] ¦ total_sales[3,38,2] 𝄀 +Total ¦ Total ¦ Total ¦ 220000.00 𝄀 +2021 ¦ Total ¦ Total ¦ 70000.00 𝄀 +2022 ¦ Total ¦ Total ¦ 150000.00 𝄀 +2021 ¦ 1 ¦ Total ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ Total ¦ 45000.00 𝄀 +2022 ¦ 1 ¦ Total ¦ 65000.00 𝄀 +2022 ¦ 2 ¦ Total ¦ 85000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 select grouping(year) as year, grouping(quarter) as quarter, @@ -528,22 +532,22 @@ group by year, quarter, region with rollup; -year quarter region total_sales -1 1 1 220000.00 -0 1 1 70000.00 -0 1 1 150000.00 -0 0 0 10000.00 -0 0 0 15000.00 -0 0 0 20000.00 -0 0 0 25000.00 -0 0 0 30000.00 -0 0 0 35000.00 -0 0 0 40000.00 -0 0 0 45000.00 -0 0 1 25000.00 -0 0 1 45000.00 -0 0 1 65000.00 -0 0 1 85000.00 +➤ year[-5,64,0] ¦ quarter[-5,64,0] ¦ region[-5,64,0] ¦ total_sales[3,38,2] 𝄀 +1 ¦ 1 ¦ 1 ¦ 220000.00 𝄀 +0 ¦ 1 ¦ 1 ¦ 70000.00 𝄀 +0 ¦ 1 ¦ 1 ¦ 150000.00 𝄀 +0 ¦ 0 ¦ 1 ¦ 25000.00 𝄀 +0 ¦ 0 ¦ 1 ¦ 45000.00 𝄀 +0 ¦ 0 ¦ 1 ¦ 65000.00 𝄀 +0 ¦ 0 ¦ 1 ¦ 85000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 10000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 15000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 20000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 25000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 30000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 35000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 40000.00 𝄀 +0 ¦ 0 ¦ 0 ¦ 45000.00 select grouping(year) as year, grouping(quarter) as quarter, @@ -555,22 +559,22 @@ group by year, quarter, region with rollup; -year quarter region total_sales -1 1 1 8 -0 1 1 4 -0 1 1 4 -0 0 1 2 -0 0 1 2 -0 0 1 2 -0 0 1 2 -0 0 0 1 -0 0 0 1 -0 0 0 1 -0 0 0 1 -0 0 0 1 -0 0 0 1 -0 0 0 1 -0 0 0 1 +➤ year[-5,64,0] ¦ quarter[-5,64,0] ¦ region[-5,64,0] ¦ total_sales[-5,64,0] 𝄀 +1 ¦ 1 ¦ 1 ¦ 8 𝄀 +0 ¦ 1 ¦ 1 ¦ 4 𝄀 +0 ¦ 1 ¦ 1 ¦ 4 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 ¦ 1 𝄀 +0 ¦ 0 ¦ 1 ¦ 2 𝄀 +0 ¦ 0 ¦ 1 ¦ 2 𝄀 +0 ¦ 0 ¦ 1 ¦ 2 𝄀 +0 ¦ 0 ¦ 1 ¦ 2 select if(grouping(year), 'Total', cast(year as char)) as year, if(grouping(quarter), 'Total', cast(quarter as char)) as quarter, @@ -584,22 +588,22 @@ quarter, region with rollup order by total_sales desc; -year quarter region total_sales -2022 2 South 45000.00 -2022 2 Total 40000.00 -2022 2 North 40000.00 -2022 1 South 35000.00 -2022 1 North 30000.00 -2022 1 Total 30000.00 -2022 Total Total 30000.00 -2021 2 South 25000.00 -2021 2 North 20000.00 -2021 2 Total 20000.00 -2021 1 South 15000.00 -2021 1 North 10000.00 -Total Total Total 10000.00 -2021 1 Total 10000.00 -2021 Total Total 10000.00 +➤ year[12,-1,0] ¦ quarter[12,-1,0] ¦ region[12,0,0] ¦ total_sales[3,10,2] 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 2 ¦ Total ¦ 40000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ Total ¦ Total ¦ 30000.00 𝄀 +2022 ¦ 1 ¦ Total ¦ 30000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 2 ¦ Total ¦ 20000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +Total ¦ Total ¦ Total ¦ 10000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 𝄀 +2021 ¦ 1 ¦ Total ¦ 10000.00 𝄀 +2021 ¦ Total ¦ Total ¦ 10000.00 select if(grouping(year), 'Total', cast(year as char)) as year, if(grouping(quarter), 'Total', cast(quarter as char)) as quarter, @@ -611,22 +615,22 @@ group by year, quarter, region with rollup; -year quarter region total_sales -Total Total Total 45000.00 -2021 Total Total 25000.00 -2022 Total Total 45000.00 -2021 1 North 10000.00 -2021 1 South 15000.00 -2021 2 North 20000.00 -2021 2 South 25000.00 -2022 1 North 30000.00 -2022 1 South 35000.00 -2022 2 North 40000.00 -2022 2 South 45000.00 -2021 1 Total 15000.00 -2021 2 Total 25000.00 -2022 1 Total 35000.00 -2022 2 Total 45000.00 +➤ year[12,-1,0] ¦ quarter[12,-1,0] ¦ region[12,0,0] ¦ total_sales[3,10,2] 𝄀 +Total ¦ Total ¦ Total ¦ 45000.00 𝄀 +2021 ¦ Total ¦ Total ¦ 25000.00 𝄀 +2022 ¦ Total ¦ Total ¦ 45000.00 𝄀 +2021 ¦ 1 ¦ Total ¦ 15000.00 𝄀 +2021 ¦ 2 ¦ Total ¦ 25000.00 𝄀 +2022 ¦ 1 ¦ Total ¦ 35000.00 𝄀 +2022 ¦ 2 ¦ Total ¦ 45000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 select year, quarter, @@ -639,22 +643,22 @@ year, quarter, region with rollup order by total_sales desc; -year quarter region total_sales -null null null 220000.00 -2022 null null 150000.00 -2022 2 null 85000.00 -2021 null null 70000.00 -2022 1 null 65000.00 -2022 2 South 45000.00 -2021 2 null 45000.00 -2022 2 North 40000.00 -2022 1 South 35000.00 -2022 1 North 30000.00 -2021 2 South 25000.00 -2021 1 null 25000.00 -2021 2 North 20000.00 -2021 1 South 15000.00 -2021 1 North 10000.00 +➤ year[4,32,0] ¦ quarter[4,32,0] ¦ region[12,0,0] ¦ total_sales[3,38,2] 𝄀 +null ¦ null ¦ null ¦ 220000.00 𝄀 +2022 ¦ null ¦ null ¦ 150000.00 𝄀 +2022 ¦ 2 ¦ null ¦ 85000.00 𝄀 +2021 ¦ null ¦ null ¦ 70000.00 𝄀 +2022 ¦ 1 ¦ null ¦ 65000.00 𝄀 +2021 ¦ 2 ¦ null ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2021 ¦ 1 ¦ null ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 select if(grouping(year), 'All years', year) as year, if(grouping(quarter), 'Quarter', quarter) as quarter, @@ -667,22 +671,22 @@ year, quarter, region with rollup order by total_sales desc; -year quarter region total_sales -All years Quarter null 220000.00 -2022 Quarter null 150000.00 -2022 2 null 85000.00 -2021 Quarter null 70000.00 -2022 1 null 65000.00 -2021 2 null 45000.00 -2022 2 South 45000.00 -2022 2 North 40000.00 -2022 1 South 35000.00 -2022 1 North 30000.00 -2021 1 null 25000.00 -2021 2 South 25000.00 -2021 2 North 20000.00 -2021 1 South 15000.00 -2021 1 North 10000.00 +➤ year[12,-1,0] ¦ quarter[12,-1,0] ¦ region[12,0,0] ¦ total_sales[3,38,2] 𝄀 +All years ¦ Quarter ¦ null ¦ 220000.00 𝄀 +2022 ¦ Quarter ¦ null ¦ 150000.00 𝄀 +2022 ¦ 2 ¦ null ¦ 85000.00 𝄀 +2021 ¦ Quarter ¦ null ¦ 70000.00 𝄀 +2022 ¦ 1 ¦ null ¦ 65000.00 𝄀 +2021 ¦ 2 ¦ null ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2021 ¦ 1 ¦ null ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 drop table sales; drop table if exists sales_details; create table sales_details ( @@ -742,33 +746,33 @@ sort_state asc, sort_city asc, sort_product_category asc, sort_product_name asc; -year quarter region country state city product_category product_name total_sales sort_year sort_quarter sort_region sort_country sort_state sort_city sort_product_category sort_product_name -null null null null null null null null 13500.00 9999 ZZ ZZZ ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2023 Q3 asia Japan Tokyo Tokyo Electronics Tablet 9000.00 2023 Q3 Q3 Japan Tokyo Tokyo Electronics Tablet -2023 Q3 asia Japan Tokyo Tokyo Electronics null 9000.00 2023 Q3 Q3 Japan Tokyo Tokyo Electronics ZZZZZZZZZZZZZZZZ -2023 Q3 asia Japan Tokyo Tokyo null null 9000.00 2023 Q3 Q3 Japan Tokyo Tokyo ZZZZZZZ ZZZZZZZZZZZZZZZZ -2023 Q3 asia Japan Tokyo null null null 9000.00 2023 Q3 Q3 Japan Tokyo ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2023 Q3 asia Japan null null null null 9000.00 2023 Q3 Q3 Japan ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2023 Q3 asia null null null null null 9000.00 2023 Q3 Q3 ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2023 Q3 null null null null null null 9000.00 2023 Q3 ZZZ ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2023 null null null null null null null 9000.00 2023 ZZ ZZZ ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 Europe France Ile-de-France Paris Furniture Sofa 2000.00 2022 Q1 Q1 France Ile-de-France Paris Furniture Sofa -2022 Q1 Europe France Ile-de-France Paris Furniture null 2000.00 2022 Q1 Q1 France Ile-de-France Paris Furniture ZZZZZZZZZZZZZZZZ -2022 Q1 Europe France Ile-de-France Paris null null 2000.00 2022 Q1 Q1 France Ile-de-France Paris ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 Europe France Ile-de-France null null null 2000.00 2022 Q1 Q1 France Ile-de-France ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 Europe France null null null null 2000.00 2022 Q1 Q1 France ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 North America USA California Los Angeles Electronics Laptop 1000.00 2022 Q1 Q1 USA California Los Angeles Electronics Laptop -2022 Q1 North America USA California Los Angeles Electronics null 1000.00 2022 Q1 Q1 USA California Los Angeles Electronics ZZZZZZZZZZZZZZZZ -2022 Q1 North America USA California Los Angeles null null 1000.00 2022 Q1 Q1 USA California Los Angeles ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 North America USA California San Francisco Electronics Smartphone 1500.00 2022 Q1 Q1 USA California San Francisco Electronics Smartphone -2022 Q1 North America USA California San Francisco Electronics null 1500.00 2022 Q1 Q1 USA California San Francisco Electronics ZZZZZZZZZZZZZZZZ -2022 Q1 North America USA California San Francisco null null 1500.00 2022 Q1 Q1 USA California San Francisco ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 North America USA California null null null 2500.00 2022 Q1 Q1 USA California ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 North America USA null null null null 2500.00 2022 Q1 Q1 USA ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 North America null null null null null 2500.00 2022 Q1 Q1 ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 Europe null null null null null 2000.00 2022 Q1 Q1 ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 Q1 null null null null null null 4500.00 2022 Q1 ZZZ ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ -2022 null null null null null null null 4500.00 2022 ZZ ZZZ ZZZZ ZZZZZ ZZZZZZ ZZZZZZZ ZZZZZZZZZZZZZZZZ +➤ year[4,32,0] ¦ quarter[12,-1,0] ¦ region[1,50,0] ¦ country[12,-1,0] ¦ state[12,-1,0] ¦ city[12,-1,0] ¦ product_category[12,-1,0] ¦ product_name[12,-1,0] ¦ total_sales[3,38,2] ¦ sort_year[12,-1,0] ¦ sort_quarter[12,-1,0] ¦ sort_region[12,-1,0] ¦ sort_country[12,-1,0] ¦ sort_state[12,-1,0] ¦ sort_city[12,-1,0] ¦ sort_product_category[12,-1,0] ¦ sort_product_name[12,-1,0] 𝄀 +null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 13500.00 ¦ 9999 ¦ ZZ ¦ ZZZ ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ Q3 ¦ asia ¦ Japan ¦ Tokyo ¦ Tokyo ¦ Electronics ¦ Tablet ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ Q3 ¦ Japan ¦ Tokyo ¦ Tokyo ¦ Electronics ¦ Tablet 𝄀 +2023 ¦ Q3 ¦ asia ¦ Japan ¦ Tokyo ¦ Tokyo ¦ Electronics ¦ null ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ Q3 ¦ Japan ¦ Tokyo ¦ Tokyo ¦ Electronics ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ Q3 ¦ asia ¦ Japan ¦ Tokyo ¦ Tokyo ¦ null ¦ null ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ Q3 ¦ Japan ¦ Tokyo ¦ Tokyo ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ Q3 ¦ asia ¦ Japan ¦ Tokyo ¦ null ¦ null ¦ null ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ Q3 ¦ Japan ¦ Tokyo ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ Q3 ¦ asia ¦ Japan ¦ null ¦ null ¦ null ¦ null ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ Q3 ¦ Japan ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ Q3 ¦ asia ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ Q3 ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ Q3 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 9000.00 ¦ 2023 ¦ Q3 ¦ ZZZ ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2023 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 9000.00 ¦ 2023 ¦ ZZ ¦ ZZZ ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ Europe ¦ France ¦ Ile-de-France ¦ Paris ¦ Furniture ¦ Sofa ¦ 2000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ France ¦ Ile-de-France ¦ Paris ¦ Furniture ¦ Sofa 𝄀 +2022 ¦ Q1 ¦ Europe ¦ France ¦ Ile-de-France ¦ Paris ¦ Furniture ¦ null ¦ 2000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ France ¦ Ile-de-France ¦ Paris ¦ Furniture ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ Europe ¦ France ¦ Ile-de-France ¦ Paris ¦ null ¦ null ¦ 2000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ France ¦ Ile-de-France ¦ Paris ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ Europe ¦ France ¦ Ile-de-France ¦ null ¦ null ¦ null ¦ 2000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ France ¦ Ile-de-France ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ Europe ¦ France ¦ null ¦ null ¦ null ¦ null ¦ 2000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ France ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ Los Angeles ¦ Electronics ¦ Laptop ¦ 1000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ Los Angeles ¦ Electronics ¦ Laptop 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ Los Angeles ¦ Electronics ¦ null ¦ 1000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ Los Angeles ¦ Electronics ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ Los Angeles ¦ null ¦ null ¦ 1000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ Los Angeles ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ San Francisco ¦ Electronics ¦ Smartphone ¦ 1500.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ San Francisco ¦ Electronics ¦ Smartphone 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ San Francisco ¦ Electronics ¦ null ¦ 1500.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ San Francisco ¦ Electronics ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ San Francisco ¦ null ¦ null ¦ 1500.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ San Francisco ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ California ¦ null ¦ null ¦ null ¦ 2500.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ California ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ USA ¦ null ¦ null ¦ null ¦ null ¦ 2500.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ USA ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ Europe ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 2000.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ North America ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 2500.00 ¦ 2022 ¦ Q1 ¦ Q1 ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ Q1 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 4500.00 ¦ 2022 ¦ Q1 ¦ ZZZ ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ 𝄀 +2022 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 4500.00 ¦ 2022 ¦ ZZ ¦ ZZZ ¦ ZZZZ ¦ ZZZZZ ¦ ZZZZZZ ¦ ZZZZZZZ ¦ ZZZZZZZZZZZZZZZZ drop table sales_details; drop table if exists sales_mixed_types; create table sales_mixed_types ( @@ -798,22 +802,22 @@ year, quarter, region with rollup order by year_label, quarter_label, region; -year_label quarter_label region total_sales -2021 1 North 10000.00 -2021 1 South 15000.00 -2021 1 null 25000.00 -2021 2 North 20000.00 -2021 2 South 25000.00 -2021 2 null 45000.00 -2021 All quarters null 70000.00 -2022 1 North 30000.00 -2022 1 South 35000.00 -2022 1 null 65000.00 -2022 2 North 40000.00 -2022 2 South 45000.00 -2022 2 null 85000.00 -2022 All quarters null 150000.00 -All years All quarters null 220000.00 +➤ year_label[12,-1,0] ¦ quarter_label[12,-1,0] ¦ region[12,0,0] ¦ total_sales[3,38,2] 𝄀 +2021 ¦ 1 ¦ null ¦ 25000.00 𝄀 +2021 ¦ 1 ¦ North ¦ 10000.00 𝄀 +2021 ¦ 1 ¦ South ¦ 15000.00 𝄀 +2021 ¦ 2 ¦ null ¦ 45000.00 𝄀 +2021 ¦ 2 ¦ North ¦ 20000.00 𝄀 +2021 ¦ 2 ¦ South ¦ 25000.00 𝄀 +2021 ¦ All quarters ¦ null ¦ 70000.00 𝄀 +2022 ¦ 1 ¦ null ¦ 65000.00 𝄀 +2022 ¦ 1 ¦ North ¦ 30000.00 𝄀 +2022 ¦ 1 ¦ South ¦ 35000.00 𝄀 +2022 ¦ 2 ¦ null ¦ 85000.00 𝄀 +2022 ¦ 2 ¦ North ¦ 40000.00 𝄀 +2022 ¦ 2 ¦ South ¦ 45000.00 𝄀 +2022 ¦ All quarters ¦ null ¦ 150000.00 𝄀 +All years ¦ All quarters ¦ null ¦ 220000.00 select if(grouping(year) = 0, year, 'Grand Total') AS year_label, sum(amount) as total_sales @@ -822,10 +826,10 @@ sales_mixed_types group by year with rollup order by year_label; -year_label total_sales -2021 70000.00 -2022 150000.00 -Grand Total 220000.00 +➤ year_label[12,-1,0] ¦ total_sales[3,38,2] 𝄀 +2021 ¦ 70000.00 𝄀 +2022 ¦ 150000.00 𝄀 +Grand Total ¦ 220000.00 select if(grouping(year), 'All', cast(year as char)) AS year_str, if(grouping(quarter), 'Total', quarter) as quarter_val, @@ -836,14 +840,14 @@ group by year, quarter with rollup order by year_str, quarter_val; -year_str quarter_val cnt -2021 1 2 -2021 2 2 -2021 Total 4 -2022 1 2 -2022 2 2 -2022 Total 4 -All Total 8 +➤ year_str[12,-1,0] ¦ quarter_val[12,-1,0] ¦ cnt[-5,64,0] 𝄀 +2021 ¦ 1 ¦ 2 𝄀 +2021 ¦ 2 ¦ 2 𝄀 +2021 ¦ Total ¦ 4 𝄀 +2022 ¦ 1 ¦ 2 𝄀 +2022 ¦ 2 ¦ 2 𝄀 +2022 ¦ Total ¦ 4 𝄀 +All ¦ Total ¦ 8 select year, if(grouping(region), if(grouping(year), 'Grand Total', 'Year Total'), region) as region_label, @@ -854,14 +858,14 @@ group by year, region with rollup order by year, region_label; -year region_label total_sales -null Grand Total 220000.00 -2021 North 30000.00 -2021 South 40000.00 -2021 Year Total 70000.00 -2022 North 70000.00 -2022 South 80000.00 -2022 Year Total 150000.00 +➤ year[4,32,0] ¦ region_label[12,0,0] ¦ total_sales[3,38,2] 𝄀 +null ¦ Grand Total ¦ 220000.00 𝄀 +2021 ¦ North ¦ 30000.00 𝄀 +2021 ¦ South ¦ 40000.00 𝄀 +2021 ¦ Year Total ¦ 70000.00 𝄀 +2022 ¦ North ¦ 70000.00 𝄀 +2022 ¦ South ¦ 80000.00 𝄀 +2022 ¦ Year Total ¦ 150000.00 select case when grouping(year) = 1 then 'All years' else cast(year as char) end AS year_label, case when grouping(quarter) = 1 then 'All quarters' else cast(quarter as char) end as quarter_label, @@ -872,14 +876,14 @@ group by year, quarter with rollup order by year_label, quarter_label; -year_label quarter_label total_sales -2021 1 25000.00 -2021 2 45000.00 -2021 All quarters 70000.00 -2022 1 65000.00 -2022 2 85000.00 -2022 All quarters 150000.00 -All years All quarters 220000.00 +➤ year_label[12,-1,0] ¦ quarter_label[12,-1,0] ¦ total_sales[3,38,2] 𝄀 +2021 ¦ 1 ¦ 25000.00 𝄀 +2021 ¦ 2 ¦ 45000.00 𝄀 +2021 ¦ All quarters ¦ 70000.00 𝄀 +2022 ¦ 1 ¦ 65000.00 𝄀 +2022 ¦ 2 ¦ 85000.00 𝄀 +2022 ¦ All quarters ¦ 150000.00 𝄀 +All years ¦ All quarters ¦ 220000.00 select concat('Year: ', if(grouping(year), 'Total', year)) AS year_label, sum(amount) as total_sales @@ -888,10 +892,10 @@ sales_mixed_types group by year with rollup order by year_label; -year_label total_sales -Year: 2021 70000.00 -Year: 2022 150000.00 -Year: Total 220000.00 +➤ year_label[12,-1,0] ¦ total_sales[3,38,2] 𝄀 +Year: 2021 ¦ 70000.00 𝄀 +Year: 2022 ¦ 150000.00 𝄀 +Year: Total ¦ 220000.00 drop table sales_mixed_types; drop table if exists grouping_order; create table grouping_order ( @@ -917,15 +921,60 @@ sum(amount) as sum_amount from grouping_order group by region, product with rollup order by grouping(region), region_label, grouping(product), product_label; -region_label grp_region product_label grp_product count_all count_amount sum_amount -NULL 0 tea 0 2 2 30 -NULL 0 ROLLUP 1 2 2 30 -east 0 NULL 0 1 1 5 -east 0 coffee 0 1 1 7 -east 0 tea 0 1 0 null -east 0 ROLLUP 1 3 2 12 -west 0 tea 0 1 1 3 -west 0 ROLLUP 1 1 1 3 -ROLLUP 1 ROLLUP 1 6 5 45 +➤ region_label[12,-1,0] ¦ grp_region[-5,64,0] ¦ product_label[12,-1,0] ¦ grp_product[-5,64,0] ¦ count_all[-5,64,0] ¦ count_amount[-5,64,0] ¦ sum_amount[-5,64,0] 𝄀 +NULL ¦ 0 ¦ tea ¦ 0 ¦ 2 ¦ 2 ¦ 30 𝄀 +NULL ¦ 0 ¦ ROLLUP ¦ 1 ¦ 2 ¦ 2 ¦ 30 𝄀 +east ¦ 0 ¦ NULL ¦ 0 ¦ 1 ¦ 1 ¦ 5 𝄀 +east ¦ 0 ¦ coffee ¦ 0 ¦ 1 ¦ 1 ¦ 7 𝄀 +east ¦ 0 ¦ tea ¦ 0 ¦ 1 ¦ 0 ¦ null 𝄀 +east ¦ 0 ¦ ROLLUP ¦ 1 ¦ 3 ¦ 2 ¦ 12 𝄀 +west ¦ 0 ¦ tea ¦ 0 ¦ 1 ¦ 1 ¦ 3 𝄀 +west ¦ 0 ¦ ROLLUP ¦ 1 ¦ 1 ¦ 1 ¦ 3 𝄀 +ROLLUP ¦ 1 ¦ ROLLUP ¦ 1 ¦ 6 ¦ 5 ¦ 45 +select *, count(*) as cnt +from grouping_order +group by region, product, amount with rollup +order by grouping(region), 1, 2, 3; +➤ region[12,-1,0] ¦ product[12,-1,0] ¦ amount[4,32,0] ¦ cnt[-5,64,0] 𝄀 +null ¦ null ¦ null ¦ 2 𝄀 +null ¦ tea ¦ null ¦ 2 𝄀 +null ¦ tea ¦ 10 ¦ 1 𝄀 +null ¦ tea ¦ 20 ¦ 1 𝄀 +east ¦ null ¦ null ¦ 1 𝄀 +east ¦ null ¦ null ¦ 3 𝄀 +east ¦ null ¦ 5 ¦ 1 𝄀 +east ¦ coffee ¦ null ¦ 1 𝄀 +east ¦ coffee ¦ 7 ¦ 1 𝄀 +east ¦ tea ¦ null ¦ 1 𝄀 +east ¦ tea ¦ null ¦ 1 𝄀 +west ¦ null ¦ null ¦ 1 𝄀 +west ¦ tea ¦ null ¦ 1 𝄀 +west ¦ tea ¦ 3 ¦ 1 𝄀 +null ¦ null ¦ null ¦ 6 +select *, count(*) as cnt +from grouping_order +group by region, product, amount with rollup +order by 4 desc, 1, 2, 3; +➤ region[12,-1,0] ¦ product[12,-1,0] ¦ amount[4,32,0] ¦ cnt[-5,64,0] 𝄀 +null ¦ null ¦ null ¦ 6 𝄀 +east ¦ null ¦ null ¦ 3 𝄀 +null ¦ null ¦ null ¦ 2 𝄀 +null ¦ tea ¦ null ¦ 2 𝄀 +null ¦ tea ¦ 10 ¦ 1 𝄀 +null ¦ tea ¦ 20 ¦ 1 𝄀 +east ¦ null ¦ null ¦ 1 𝄀 +east ¦ null ¦ 5 ¦ 1 𝄀 +east ¦ coffee ¦ null ¦ 1 𝄀 +east ¦ coffee ¦ 7 ¦ 1 𝄀 +east ¦ tea ¦ null ¦ 1 𝄀 +east ¦ tea ¦ null ¦ 1 𝄀 +west ¦ null ¦ null ¦ 1 𝄀 +west ¦ tea ¦ null ¦ 1 𝄀 +west ¦ tea ¦ 3 ¦ 1 +select *, count(*) as cnt +from grouping_order +group by region, product, amount with rollup +order by 6; +SQL syntax error: ORDER BY position 6 is not in select list drop table grouping_order; drop database rollup_test; diff --git a/test/distributed/cases/window/rollup.sql b/test/distributed/cases/window/rollup.sql index 10680c07475ad..93837fddd7cf6 100644 --- a/test/distributed/cases/window/rollup.sql +++ b/test/distributed/cases/window/rollup.sql @@ -619,5 +619,23 @@ from grouping_order group by region, product with rollup order by grouping(region), region_label, grouping(product), product_label; +-- star-expanded select list with rollup order by: positional ORDER BY must be +-- validated against the visible (star-expanded) columns, and grouping() must +-- resolve to its hidden sort key rather than a visible column shifted by '*'. +select *, count(*) as cnt +from grouping_order +group by region, product, amount with rollup +order by grouping(region), 1, 2, 3; + +select *, count(*) as cnt +from grouping_order +group by region, product, amount with rollup +order by 4 desc, 1, 2, 3; + +select *, count(*) as cnt +from grouping_order +group by region, product, amount with rollup +order by 6; + drop table grouping_order; drop database rollup_test;