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 c1619a9eed11b..f2ed82a1fabd8 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" @@ -2229,6 +2230,25 @@ 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, nil) +} + +func (builder *QueryBuilder) buildUnionWithResultLen( + stmt *tree.UnionClause, + astOrderBy tree.OrderBy, + astLimit *tree.Limit, + astRankOption *tree.RankOption, + 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") } @@ -2472,6 +2492,10 @@ 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 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 @@ -2480,14 +2504,50 @@ func (builder *QueryBuilder) buildUnion(stmt *tree.UnionClause, astOrderBy tree. 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{ @@ -2597,6 +2657,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...) @@ -3037,11 +3099,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 { @@ -3058,27 +3122,35 @@ 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 { - groupingCount := len(selectClause.GroupBy.GroupByExprsList) + if len(groupByExprsList) > 1 && !selectClause.GroupBy.Apart { + var branchExprs tree.SelectExprs + var unionOrderBy tree.OrderBy + var hiddenOrderIdx []int + branchExprs, unionOrderBy, hiddenOrderIdx, err = prepareGroupingSetOrderByProjects(builder, astOrderBy, selectClause.Exprs, selectClause.Distinct) + 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 } selectStmts[i] = &tree.SelectClause{ Distinct: selectClause.Distinct, - Exprs: selectClause.Exprs, + Exprs: branchExprs, From: selectClause.From, Where: selectClause.Where, GroupBy: &tree.GroupByClause{ - GroupByExprsList: selectClause.GroupBy.GroupByExprsList, + GroupByExprsList: groupByExprsList, GroupingSet: list, Apart: true, Cube: false, @@ -3096,7 +3168,22 @@ func (builder *QueryBuilder) bindSelect(stmt *tree.Select, ctx *BindContext, isR } leftClause = &tree.UnionClause{Type: tree.UNION, Left: leftClause, Right: stmt, All: true} } - return builder.buildUnion(leftClause, astOrderBy, astLimit, astRankOption, ctx, isRoot) + if nodeID, err = builder.buildUnionWithResultLen( + leftClause, + unionOrderBy, + astLimit, + astRankOption, + ctx, + false, + hiddenResultLen, + hiddenOrderIdx, + ); err != nil { + return + } + if isRoot { + builder.qry.Headings = append(builder.qry.Headings, ctx.headings...) + } + return } } @@ -3800,6 +3887,420 @@ func (builder *QueryBuilder) bindTimeWindow( return } +func prepareGroupingSetOrderByProjects( + builder *QueryBuilder, + astOrderBy tree.OrderBy, + selectList tree.SelectExprs, + distinct bool, +) (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 + // 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, nil, err + } + if _, exists := visibleOrdinals[key]; !exists { + visibleOrdinals[key] = selectIdx + 1 + } + } + } + + 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 + } + + if distinct { + key, err := groupingOrderExprKey(builder, selectList, order.Expr) + if err != nil { + 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, 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, 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}) + } + return branchSelectList, unionOrderBy, hiddenOrderIdx, 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, table qualifiers, function-name case, +// or redundant parentheses. +func groupingOrderExprKey( + builder *QueryBuilder, + selectList tree.SelectExprs, + astExpr tree.Expr, +) (string, error) { + // 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 + } + normalizeExprForComparison(qualified) + return tree.String(qualified, dialect.MYSQL), nil +} + +// 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) { + 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 { + 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]) + 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 + } + 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 + } + 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 + } +} + +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 + } + case *tree.Subquery: + return false + } + return !found + }) + return found +} + +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 + } + + 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( 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..e0322f2a5811c 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" @@ -412,6 +413,794 @@ 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(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 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(), + 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)) + + 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) { + 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(3), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 4) +} + +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(2), sortNode.OrderBy[0].Expr.GetCol().ColPos) + require.Len(t, query.Nodes[sortNode.Children[0]].ProjectList, 3) +} + +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(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 TestAppendGroupingSetOrderByProjects(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) + 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) + + // 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() + require.True(t, ok) + require.Equal(t, int64(2), pos) +} + +func TestAppendGroupingSetOrderByProjectsIgnoresAliasShadowing(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) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + require.Len(t, branchExprs, 4) + + // grouping(b) -> 0th appended hidden key (visible len 3, hidden at tail). + require.Equal(t, []int{0}, hiddenOrderIdx) +} + +func TestAppendGroupingSetOrderByProjectsDefersAmbiguousColumnToBinder(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) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + require.Len(t, branchExprs, 3) + + require.Equal(t, []int{0}, hiddenOrderIdx) +} + +func TestAppendGroupingSetOrderByProjectsSupportsGroupAlias(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) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + require.Len(t, branchExprs, 4) + + require.Equal(t, []int{0}, hiddenOrderIdx) +} + +// 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, 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) + 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") +} + +// 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) + }) + } +} + +// 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) { + 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), + }, + }, + }, + } + 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 unwraps redundant parentheses from +// GROUPING() arguments so that GROUPING((a)) normalizes to GROUPING(a). +func TestNormalizeExprForComparisonUnwrapsParenExpr(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), + }, + }, + }, + }, + } + 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()) +} + +// 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) { + // 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) +} + +// 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 + 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: true, + }, + { + 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: true, + }, + } + + 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) + _, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + + // 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)) + }) + } +} + +func TestAppendGroupingSetOrderByNestedQualifiedProject(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)) + branchExprs, _, hiddenOrderIdx, err := prepareGroupingSetOrderByProjects(nil, selectStmt.OrderBy, selectClause.Exprs, false) + require.NoError(t, err) + require.Len(t, branchExprs, 3) + + require.Equal(t, []int{0}, hiddenOrderIdx) +} + func TestQueryBuilder_bindHaving(t *testing.T) { builder, bindCtx := genBuilderAndCtx() @@ -2889,3 +3678,132 @@ func TestBaseBinder_baseBindExpr(t *testing.T) { }) } } + +// TestAppendGroupingSetOrderByProjectsOrdinalWithinVisibleLen verifies that a +// 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(), + 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, 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; 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) +} + +// 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, _, 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/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" : { diff --git a/test/distributed/cases/window/rollup.result b/test/distributed/cases/window/rollup.result index bc23b58dd4c44..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,9 +892,89 @@ 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 ( +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[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 e58bc146cec9f..93837fddd7cf6 100644 --- a/test/distributed/cases/window/rollup.sql +++ b/test/distributed/cases/window/rollup.sql @@ -592,4 +592,50 @@ 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; + +-- 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;