From 450184ad3485eaaf4bc9daf1aa8d81500f79edd0 Mon Sep 17 00:00:00 2001 From: Juanjo Alvarez Date: Thu, 11 Apr 2019 18:20:46 +0200 Subject: [PATCH 1/4] Validation rule to detect tuples as distinct argument Signed-off-by: Juanjo Alvarez --- engine_test.go | 1 + sql/analyzer/optimization_rules.go | 3 ++- sql/analyzer/resolve_columns.go | 25 +++++++++++++++++++++++++ sql/analyzer/resolve_columns_test.go | 20 ++++++++++++++++++++ sql/analyzer/rules.go | 2 ++ sql/analyzer/validation_rules.go | 2 +- 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/engine_test.go b/engine_test.go index dfd90bc4a..5d851de5e 100644 --- a/engine_test.go +++ b/engine_test.go @@ -909,6 +909,7 @@ var queries = []struct { "SELECT FROM_BASE64('YmFy')", []sql.Row{{string("bar")}}, }, +<<<<<<< HEAD { "SELECT DATE_ADD('2018-05-02', INTERVAL 1 DAY)", []sql.Row{{time.Date(2018, time.May, 3, 0, 0, 0, 0, time.UTC)}}, diff --git a/sql/analyzer/optimization_rules.go b/sql/analyzer/optimization_rules.go index f8795348b..3de847b7e 100644 --- a/sql/analyzer/optimization_rules.go +++ b/sql/analyzer/optimization_rules.go @@ -1,7 +1,7 @@ package analyzer import ( - errors "gopkg.in/src-d/go-errors.v1" + "gopkg.in/src-d/go-errors.v1" "gopkg.in/src-d/go-mysql-server.v0/sql" "gopkg.in/src-d/go-mysql-server.v0/sql/expression" "gopkg.in/src-d/go-mysql-server.v0/sql/plan" @@ -33,6 +33,7 @@ func optimizeDistinct(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, e defer span.Finish() a.Log("optimize distinct, node of type: %T", node) + if node, ok := node.(*plan.Distinct); ok { var isSorted bool _, _ = node.TransformUp(func(node sql.Node) (sql.Node, error) { diff --git a/sql/analyzer/resolve_columns.go b/sql/analyzer/resolve_columns.go index 2e6cb439f..e171eeb2d 100644 --- a/sql/analyzer/resolve_columns.go +++ b/sql/analyzer/resolve_columns.go @@ -38,6 +38,31 @@ func checkAliases(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { return n, err } +func checkDistinctNoTuples(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error) { + span, _ := ctx.Span("no_distinct_tuples") + defer span.Finish() + + a.Log("check no tuples as distinct projection, node of type: %T", node) + var err error + if node, ok := node.(*plan.Distinct); ok { + _, err = node.TransformUp(func(node sql.Node) (sql.Node, error) { + project, ok := node.(*plan.Project) + if ok { + for _, col := range project.Projections { + _, ok := col.(expression.Tuple) + if ok { + return node, ErrDistinctTuple.New() + } + } + } + + return node, nil + }) + } + + return node, err +} + func lookForAliasDeclarations(node sql.Expressioner) map[string]struct{} { var ( aliases = map[string]struct{}{} diff --git a/sql/analyzer/resolve_columns_test.go b/sql/analyzer/resolve_columns_test.go index afd180fd4..a7245ec1f 100644 --- a/sql/analyzer/resolve_columns_test.go +++ b/sql/analyzer/resolve_columns_test.go @@ -75,6 +75,26 @@ func TestMisusedAlias(t *testing.T) { require.EqualError(err, ErrMisusedAlias.New("alias_i").Error()) } +func TestDistinctNoTuples(t *testing.T) { + require := require.New(t) + f := getRule("check_distinct_no_tuples") + + table := mem.NewTable("mytable", sql.Schema{ + {Name: "i", Type: sql.Int32}, + }) + + node := plan.NewProject([]sql.Expression{ + expression.NewTuple( + expression.NewLiteral(1, sql.Int64), + expression.NewLiteral(2, sql.Int64), + ), + }, plan.NewResolvedTable(table)) + d := plan.NewDistinct(node) + + _, err := f.Apply(sql.NewEmptyContext(), nil, d) + require.EqualError(err, ErrDistinctTuple.New().Error()) +} + func TestQualifyColumns(t *testing.T) { require := require.New(t) f := getRule("qualify_columns") diff --git a/sql/analyzer/rules.go b/sql/analyzer/rules.go index 0386fe57f..a9ddda1fd 100644 --- a/sql/analyzer/rules.go +++ b/sql/analyzer/rules.go @@ -28,6 +28,7 @@ var OnceBeforeDefault = []Rule{ {"resolve_subqueries", resolveSubqueries}, {"resolve_tables", resolveTables}, {"check_aliases", checkAliases}, + {"check_distinct_no_tuples", checkDistinctNoTuples}, } // OnceAfterDefault contains the rules to be applied just once after the @@ -66,4 +67,5 @@ var ( // ErrMisusedAlias is returned when a alias is defined and used in the same projection. ErrMisusedAlias = errors.NewKind("column %q does not exist in scope, but there is an alias defined in" + " this projection with that name. Aliases cannot be used in the same projection they're defined in") + ErrDistinctTuple = errors.NewKind("tuple used as DISTINCT argument, remove the ()") ) diff --git a/sql/analyzer/validation_rules.go b/sql/analyzer/validation_rules.go index c1cbadcc5..d17666197 100644 --- a/sql/analyzer/validation_rules.go +++ b/sql/analyzer/validation_rules.go @@ -3,7 +3,7 @@ package analyzer import ( "strings" - errors "gopkg.in/src-d/go-errors.v1" + "gopkg.in/src-d/go-errors.v1" "gopkg.in/src-d/go-mysql-server.v0/sql" "gopkg.in/src-d/go-mysql-server.v0/sql/expression" "gopkg.in/src-d/go-mysql-server.v0/sql/expression/function" From 20fabe44070d138c4ad1027ef785f284a29b62b3 Mon Sep 17 00:00:00 2001 From: Juanjo Alvarez Date: Thu, 11 Apr 2019 19:16:59 +0200 Subject: [PATCH 2/4] Fix ordering in README, remove spurious import aliases. Signed-off-by: Juanjo Alvarez --- README.md | 2 +- engine_test.go | 1 + sql/analyzer/optimization_rules.go | 1 - sql/analyzer/resolve_columns.go | 48 +++++++++++++++++----------- sql/analyzer/resolve_columns_test.go | 48 +++++++++++++++++++++++++--- sql/analyzer/rules.go | 4 +-- 6 files changed, 76 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 805a07e9e..d26fa0cf8 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ We support and actively test against certain third-party clients to ensure compa |`DAYOFWEEK(date)`|Returns the day of the week of the given date.| |`DAYOFYEAR(date)`|Returns the day of the year of the given date.| |`FLOOR(number)`|Return the largest integer value that is less than or equal to `number`.| +|`FROM_BASE64(str)`|Decodes the base64-encoded string str.| |`HOUR(date)`|Returns the hours of the given date.| |`IFNULL(expr1, expr2)`|If expr1 is not NULL, IFNULL() returns expr1; otherwise it returns expr2.| |`IS_BINARY(blob)`|Returns whether a BLOB is a binary file or not.| @@ -110,7 +111,6 @@ We support and actively test against certain third-party clients to ensure compa |`SUBSTRING(str, pos, [len])`|Return a substring from the provided string starting at `pos` with a length of `len` characters. If no `len` is provided, all characters from `pos` until the end will be taken.| |`SUM(expr)`|Returns the sum of expr in all rows.| |`TO_BASE64(str)`|Encodes the string str in base64 format.| -|`FROM_BASE64(str)`|Decodes the base64-encoded string str.| |`TRIM(str)`|Returns the string str with all spaces removed.| |`UPPER(str)`|Returns the string str with all characters in upper case.| |`WEEKDAY(date)`|Returns the weekday of the given date.| diff --git a/engine_test.go b/engine_test.go index 5d851de5e..ee323dd1b 100644 --- a/engine_test.go +++ b/engine_test.go @@ -909,6 +909,7 @@ var queries = []struct { "SELECT FROM_BASE64('YmFy')", []sql.Row{{string("bar")}}, }, +<<<<<<< HEAD <<<<<<< HEAD { "SELECT DATE_ADD('2018-05-02', INTERVAL 1 DAY)", diff --git a/sql/analyzer/optimization_rules.go b/sql/analyzer/optimization_rules.go index 3de847b7e..e45c1411a 100644 --- a/sql/analyzer/optimization_rules.go +++ b/sql/analyzer/optimization_rules.go @@ -33,7 +33,6 @@ func optimizeDistinct(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, e defer span.Finish() a.Log("optimize distinct, node of type: %T", node) - if node, ok := node.(*plan.Distinct); ok { var isSorted bool _, _ = node.TransformUp(func(node sql.Node) (sql.Node, error) { diff --git a/sql/analyzer/resolve_columns.go b/sql/analyzer/resolve_columns.go index e171eeb2d..bfbc6353f 100644 --- a/sql/analyzer/resolve_columns.go +++ b/sql/analyzer/resolve_columns.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - errors "gopkg.in/src-d/go-errors.v1" + "gopkg.in/src-d/go-errors.v1" "gopkg.in/src-d/go-mysql-server.v0/sql" "gopkg.in/src-d/go-mysql-server.v0/sql/expression" "gopkg.in/src-d/go-mysql-server.v0/sql/plan" @@ -38,29 +38,39 @@ func checkAliases(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { return n, err } -func checkDistinctNoTuples(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error) { - span, _ := ctx.Span("no_distinct_tuples") +func checkNoTuplesProjected(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error) { + span, _ := ctx.Span("no_tuples_projected") defer span.Finish() - a.Log("check no tuples as distinct projection, node of type: %T", node) - var err error - if node, ok := node.(*plan.Distinct); ok { - _, err = node.TransformUp(func(node sql.Node) (sql.Node, error) { - project, ok := node.(*plan.Project) - if ok { - for _, col := range project.Projections { - _, ok := col.(expression.Tuple) - if ok { - return node, ErrDistinctTuple.New() - } + a.Log("check no tuples as in projection, node of type: %T", node) + return node.TransformUp(func(node sql.Node) (sql.Node, error) { + project, ok := node.(*plan.Project) + if ok { + for _, col := range project.Projections { + _, ok := col.(expression.Tuple) + if ok { + return node, ErrTupleProjected.New() } } + } + groupby, ok := node.(*plan.GroupBy) + if ok { + for _, c := range groupby.Grouping { + _, ok := c.(expression.Tuple) + if ok { + return node, ErrTupleProjected.New() + } + } + for _, c := range groupby.Aggregate { + _, ok := c.(expression.Tuple) + if ok { + return node, ErrTupleProjected.New() + } + } + } - return node, nil - }) - } - - return node, err + return node, nil + }) } func lookForAliasDeclarations(node sql.Expressioner) map[string]struct{} { diff --git a/sql/analyzer/resolve_columns_test.go b/sql/analyzer/resolve_columns_test.go index a7245ec1f..bf3d5814a 100644 --- a/sql/analyzer/resolve_columns_test.go +++ b/sql/analyzer/resolve_columns_test.go @@ -75,9 +75,9 @@ func TestMisusedAlias(t *testing.T) { require.EqualError(err, ErrMisusedAlias.New("alias_i").Error()) } -func TestDistinctNoTuples(t *testing.T) { +func TestNoTuplesProjected(t *testing.T) { require := require.New(t) - f := getRule("check_distinct_no_tuples") + f := getRule("no_tuples_projected") table := mem.NewTable("mytable", sql.Schema{ {Name: "i", Type: sql.Int32}, @@ -89,10 +89,48 @@ func TestDistinctNoTuples(t *testing.T) { expression.NewLiteral(2, sql.Int64), ), }, plan.NewResolvedTable(table)) - d := plan.NewDistinct(node) - _, err := f.Apply(sql.NewEmptyContext(), nil, d) - require.EqualError(err, ErrDistinctTuple.New().Error()) + _, err := f.Apply(sql.NewEmptyContext(), nil, node) + require.EqualError(err, ErrTupleProjected.New().Error()) +} + +func TestNoTuplesGroupBy(t *testing.T) { + require := require.New(t) + f := getRule("no_tuples_projected") + + table := mem.NewTable("mytable", sql.Schema{ + {Name: "i", Type: sql.Int32}, + }) + + node := plan.NewGroupBy([]sql.Expression{ + expression.NewUnresolvedColumn("a"), + expression.NewUnresolvedColumn("b"), + }, + []sql.Expression{ + expression.NewTuple( + expression.NewLiteral(1, sql.Int64), + expression.NewLiteral(2, sql.Int64), + ), + }, + plan.NewResolvedTable(table)) + + _, err := f.Apply(sql.NewEmptyContext(), nil, node) + require.EqualError(err, ErrTupleProjected.New().Error()) + + node = plan.NewGroupBy([]sql.Expression{ + expression.NewTuple( + expression.NewLiteral(1, sql.Int64), + expression.NewLiteral(2, sql.Int64), + ), + }, + []sql.Expression{ + expression.NewUnresolvedColumn("a"), + expression.NewUnresolvedColumn("b"), + }, + plan.NewResolvedTable(table)) + + _, err = f.Apply(sql.NewEmptyContext(), nil, node) + require.EqualError(err, ErrTupleProjected.New().Error()) } func TestQualifyColumns(t *testing.T) { diff --git a/sql/analyzer/rules.go b/sql/analyzer/rules.go index a9ddda1fd..2c0099f7c 100644 --- a/sql/analyzer/rules.go +++ b/sql/analyzer/rules.go @@ -28,7 +28,7 @@ var OnceBeforeDefault = []Rule{ {"resolve_subqueries", resolveSubqueries}, {"resolve_tables", resolveTables}, {"check_aliases", checkAliases}, - {"check_distinct_no_tuples", checkDistinctNoTuples}, + {"no_tuples_projected", checkNoTuplesProjected}, } // OnceAfterDefault contains the rules to be applied just once after the @@ -67,5 +67,5 @@ var ( // ErrMisusedAlias is returned when a alias is defined and used in the same projection. ErrMisusedAlias = errors.NewKind("column %q does not exist in scope, but there is an alias defined in" + " this projection with that name. Aliases cannot be used in the same projection they're defined in") - ErrDistinctTuple = errors.NewKind("tuple used as DISTINCT argument, remove the ()") + ErrTupleProjected = errors.NewKind("unexpected tuple found, maybe remove the ()?") ) From 56556f16095f7d992dea8a01316435e5eb51aa33 Mon Sep 17 00:00:00 2001 From: Juanjo Alvarez Date: Mon, 22 Apr 2019 15:23:11 +0200 Subject: [PATCH 3/4] Fix validation rule for tuples in Projections Signed-off-by: Juanjo Alvarez --- engine_test.go | 2 - sql/analyzer/resolve_columns.go | 35 ---------------- sql/analyzer/resolve_columns_test.go | 58 --------------------------- sql/analyzer/rules.go | 2 - sql/analyzer/validation_rules.go | 27 ++++++++----- sql/analyzer/validation_rules_test.go | 13 +++++- 6 files changed, 30 insertions(+), 107 deletions(-) diff --git a/engine_test.go b/engine_test.go index ee323dd1b..dfd90bc4a 100644 --- a/engine_test.go +++ b/engine_test.go @@ -909,8 +909,6 @@ var queries = []struct { "SELECT FROM_BASE64('YmFy')", []sql.Row{{string("bar")}}, }, -<<<<<<< HEAD -<<<<<<< HEAD { "SELECT DATE_ADD('2018-05-02', INTERVAL 1 DAY)", []sql.Row{{time.Date(2018, time.May, 3, 0, 0, 0, 0, time.UTC)}}, diff --git a/sql/analyzer/resolve_columns.go b/sql/analyzer/resolve_columns.go index bfbc6353f..27587262c 100644 --- a/sql/analyzer/resolve_columns.go +++ b/sql/analyzer/resolve_columns.go @@ -38,41 +38,6 @@ func checkAliases(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { return n, err } -func checkNoTuplesProjected(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error) { - span, _ := ctx.Span("no_tuples_projected") - defer span.Finish() - - a.Log("check no tuples as in projection, node of type: %T", node) - return node.TransformUp(func(node sql.Node) (sql.Node, error) { - project, ok := node.(*plan.Project) - if ok { - for _, col := range project.Projections { - _, ok := col.(expression.Tuple) - if ok { - return node, ErrTupleProjected.New() - } - } - } - groupby, ok := node.(*plan.GroupBy) - if ok { - for _, c := range groupby.Grouping { - _, ok := c.(expression.Tuple) - if ok { - return node, ErrTupleProjected.New() - } - } - for _, c := range groupby.Aggregate { - _, ok := c.(expression.Tuple) - if ok { - return node, ErrTupleProjected.New() - } - } - } - - return node, nil - }) -} - func lookForAliasDeclarations(node sql.Expressioner) map[string]struct{} { var ( aliases = map[string]struct{}{} diff --git a/sql/analyzer/resolve_columns_test.go b/sql/analyzer/resolve_columns_test.go index bf3d5814a..afd180fd4 100644 --- a/sql/analyzer/resolve_columns_test.go +++ b/sql/analyzer/resolve_columns_test.go @@ -75,64 +75,6 @@ func TestMisusedAlias(t *testing.T) { require.EqualError(err, ErrMisusedAlias.New("alias_i").Error()) } -func TestNoTuplesProjected(t *testing.T) { - require := require.New(t) - f := getRule("no_tuples_projected") - - table := mem.NewTable("mytable", sql.Schema{ - {Name: "i", Type: sql.Int32}, - }) - - node := plan.NewProject([]sql.Expression{ - expression.NewTuple( - expression.NewLiteral(1, sql.Int64), - expression.NewLiteral(2, sql.Int64), - ), - }, plan.NewResolvedTable(table)) - - _, err := f.Apply(sql.NewEmptyContext(), nil, node) - require.EqualError(err, ErrTupleProjected.New().Error()) -} - -func TestNoTuplesGroupBy(t *testing.T) { - require := require.New(t) - f := getRule("no_tuples_projected") - - table := mem.NewTable("mytable", sql.Schema{ - {Name: "i", Type: sql.Int32}, - }) - - node := plan.NewGroupBy([]sql.Expression{ - expression.NewUnresolvedColumn("a"), - expression.NewUnresolvedColumn("b"), - }, - []sql.Expression{ - expression.NewTuple( - expression.NewLiteral(1, sql.Int64), - expression.NewLiteral(2, sql.Int64), - ), - }, - plan.NewResolvedTable(table)) - - _, err := f.Apply(sql.NewEmptyContext(), nil, node) - require.EqualError(err, ErrTupleProjected.New().Error()) - - node = plan.NewGroupBy([]sql.Expression{ - expression.NewTuple( - expression.NewLiteral(1, sql.Int64), - expression.NewLiteral(2, sql.Int64), - ), - }, - []sql.Expression{ - expression.NewUnresolvedColumn("a"), - expression.NewUnresolvedColumn("b"), - }, - plan.NewResolvedTable(table)) - - _, err = f.Apply(sql.NewEmptyContext(), nil, node) - require.EqualError(err, ErrTupleProjected.New().Error()) -} - func TestQualifyColumns(t *testing.T) { require := require.New(t) f := getRule("qualify_columns") diff --git a/sql/analyzer/rules.go b/sql/analyzer/rules.go index 2c0099f7c..0386fe57f 100644 --- a/sql/analyzer/rules.go +++ b/sql/analyzer/rules.go @@ -28,7 +28,6 @@ var OnceBeforeDefault = []Rule{ {"resolve_subqueries", resolveSubqueries}, {"resolve_tables", resolveTables}, {"check_aliases", checkAliases}, - {"no_tuples_projected", checkNoTuplesProjected}, } // OnceAfterDefault contains the rules to be applied just once after the @@ -67,5 +66,4 @@ var ( // ErrMisusedAlias is returned when a alias is defined and used in the same projection. ErrMisusedAlias = errors.NewKind("column %q does not exist in scope, but there is an alias defined in" + " this projection with that name. Aliases cannot be used in the same projection they're defined in") - ErrTupleProjected = errors.NewKind("unexpected tuple found, maybe remove the ()?") ) diff --git a/sql/analyzer/validation_rules.go b/sql/analyzer/validation_rules.go index d17666197..288163fbd 100644 --- a/sql/analyzer/validation_rules.go +++ b/sql/analyzer/validation_rules.go @@ -195,27 +195,36 @@ func validateSchema(t *plan.ResolvedTable) error { return nil } -func validateProjectTuples(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { - span, _ := ctx.Span("validate_project_tuples") - defer span.Finish() +func findProjectTuples(n sql.Node) (sql.Node, error) { + if n == nil { + return n, nil + } switch n := n.(type) { - case *plan.Project: - for i, e := range n.Projections { + case *plan.Project, *plan.GroupBy: + for i, e := range n.(sql.Expressioner).Expressions() { if sql.IsTuple(e.Type()) { return nil, ErrProjectTuple.New(i+1, sql.NumColumns(e.Type())) } } - case *plan.GroupBy: - for i, e := range n.Aggregate { - if sql.IsTuple(e.Type()) { - return nil, ErrProjectTuple.New(i+1, sql.NumColumns(e.Type())) + default: + for _, ch := range n.Children() { + _, err := findProjectTuples(ch) + if err != nil { + return nil, err } } } + return n, nil } +func validateProjectTuples(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { + span, _ := ctx.Span("validate_project_tuples") + defer span.Finish() + return findProjectTuples(n) +} + func validateCaseResultTypes(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { span, ctx := ctx.Span("validate_case_result_types") defer span.Finish() diff --git a/sql/analyzer/validation_rules_test.go b/sql/analyzer/validation_rules_test.go index 85993b84e..a38706d9b 100644 --- a/sql/analyzer/validation_rules_test.go +++ b/sql/analyzer/validation_rules_test.go @@ -235,11 +235,22 @@ func TestValidateProjectTuples(t *testing.T) { plan.NewProject([]sql.Expression{ expression.NewTuple( expression.NewLiteral(1, sql.Int64), - expression.NewLiteral(1, sql.Int64), + expression.NewLiteral(2, sql.Int64), ), }, nil), false, }, + { + "distinct with a 2 elem tuple inside the project", + plan.NewDistinct( + plan.NewProject([]sql.Expression{ + expression.NewTuple( + expression.NewLiteral(1, sql.Int64), + expression.NewLiteral(2, sql.Int64), + ), + }, nil)), + false, + }, { "groupby with no tuple", plan.NewGroupBy([]sql.Expression{ From b9b49122e8f3788980dfa29f9caf715fc791042e Mon Sep 17 00:00:00 2001 From: Juanjo Alvarez Date: Mon, 22 Apr 2019 15:42:34 +0200 Subject: [PATCH 4/4] Add a test with an alias to a projected tuple Signed-off-by: Juanjo Alvarez --- sql/analyzer/validation_rules_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sql/analyzer/validation_rules_test.go b/sql/analyzer/validation_rules_test.go index a38706d9b..b1fa35986 100644 --- a/sql/analyzer/validation_rules_test.go +++ b/sql/analyzer/validation_rules_test.go @@ -251,6 +251,22 @@ func TestValidateProjectTuples(t *testing.T) { }, nil)), false, }, + { + "alias with a tuple", + plan.NewProject( + []sql.Expression{ + expression.NewAlias( + expression.NewTuple( + expression.NewLiteral(1, sql.Int64), + expression.NewLiteral(2, sql.Int64), + ), + "foo", + ), + }, + plan.NewUnresolvedTable("dual", ""), + ), + false, + }, { "groupby with no tuple", plan.NewGroupBy([]sql.Expression{