cockroachdb / cockroachdb/cockroach

sql: () in GROUP BY is handled as a scalar tuple, so nested empties like GROUP BY ((())) are accepted

Open
#173,468 1 comment 0 reactions 0 assignees View on GitHub
A-sql-pgcompat A-sql-semantics C-bug T-sql-queries
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

## Describe the problem

In `GROUP BY` position, `()` is not a row constructor — it is the **empty
grouping set**, a grouping element in its own right, and should be treated as
such. CockroachDB instead parses it as an ordinary scalar expression (it
supports `()` as a general zero-field tuple, an extension over PostgreSQL) and
relies on tuple-flattening to collapse it to zero grouping columns. The right
answer falls out for the plain `GROUP BY ()` case, but by the wrong mechanism.

Because the mechanism is expression flattening rather than a dedicated grouping
production, the empty grouping set is affected: nested forms like `GROUP BY
(())` and `GROUP BY ((()))` are silently accepted and behave identically to it,
recursively flattening to zero grouping columns. PostgreSQL rejects both at
parse time, as does the SQL standard's grammar.

No wrong results — this is an *accepts-invalid* divergence. It matters because
it conflates a grouping element with a scalar value, leaving `()` in grouping
position with two readings that happen to coincide today.

## To Reproduce

```sql
CREATE TABLE t (a INT, b INT);
INSERT INTO t VALUES (1, 10), (2, 20);
```

| Query | PostgreSQL 19devel | CockroachDB |
| --- | --- | --- |
| `SELECT count(*) FROM t GROUP BY ();` | 1 row (`2`) | 1 row (`2`) — matches |
| `SELECT count(*) FROM t GROUP BY (), ();` | 1 row (`2`) | 1 row (`2`) — matches |
| `SELECT count(*) FROM t GROUP BY (());` | `ERROR 42601: syntax error at or near ")"` | **1 row (`2`)** |
| `SELECT count(*) FROM t GROUP BY ((()));` | `ERROR 42601: syntax error at or near ")"` | **1 row (`2`)** |
| `SELECT ();` | `ERROR 42601: syntax error at or near ")"` | **`()`** (intentional extension) |
| `SELECT ROW();` | `()` | `()` — matches |

The nested forms are equivalent to the empty grouping set on empty input too,
taking the scalar-aggregation path:

```sql
SELECT count(*) FROM t WHERE false GROUP BY ((())); -- CockroachDB: 1 row (0); PostgreSQL: syntax error
SELECT count(*) FROM t WHERE false GROUP BY (); -- both: 1 row (0)
```

That `()` is reaching `GROUP BY` as a scalar, not as grouping syntax:

```sql
SELECT pg_typeof(((()))); -- CockroachDB: record
SELECT (()) IS NOT DISTINCT FROM ROW(); -- CockroachDB: true
```

## Expected behavior

`()` in `GROUP BY` should be recognized as the empty grouping set, not routed
through scalar expression handling. Concretely:

* `GROUP BY (())` and `GROUP BY ((()))` should be syntax errors, matching
PostgreSQL.
* `GROUP BY ()` and `GROUP BY (), ()` should keep working as they do today —
both match PostgreSQL, and the latter is explicitly sanctioned by the
standard.

## Additional context

**Why it happens.** `GROUP BY (())` parses as a one-element tuple wrapping an
empty tuple, because `()` is a tested CockroachDB expression extension
(`expr_tuple_unambiguous` in `pkg/sql/parser/sql.y` accepts "zero or more").
`buildGrouping` (`pkg/sql/opt/optbuilder/groupby.go`) then calls
`flattenTuples` (`pkg/sql/opt/optbuilder/util.go`), which recurses into nested
tuples and contributes nothing for the empty one — leaving `groupingColSet`
empty, so `buildAggregation` takes the `ConstructScalarGroupBy` path.

**The standard agrees `()` is not an expression here.** ISO/IEC 9075-2 subclause
7.9 gives it a dedicated production, and grouping items are column references,
never expressions:

```
::=

::=

|
```

Subclause 7.1 has no zero-degree row constructor either (the element list
requires at least one element), so `(())` is non-conforming as well as
non-PostgreSQL.

**Forward-looking.** GROUP BY GROUPING SETS ((), (())) exposes the ambiguity. Allowing nested (()) basically ask us to decide, what does () mean, if not empty grouping set(because empty grouping set cannot be nested inside parentheses in both standard and postgres)? Is it empty scalar tuple? then how do you represent empty grouping set? is it empty grouping set? then we are in the realm of inventing behaviour for empty grouping set.

**Related.** Shares the `flattenTuples` root cause with #173264 (`GROUP BY
ROW(...)` is incorrectly flattened), but the two are independently fixable.

## Environment

* CockroachDB v26.4.0-alpha (master, August 2026), single-node `cockroach demo`
* Compared against PostgreSQL 19devel

Jira issue: CRDB-66785

Contributor guide

Open the contributing guide

Research direction

Reproduce the nested GROUP BY queries, then read the grouping grammar in pkg/sql/parser/sql.y and the tuple handling in pkg/sql/opt/optbuilder/groupby.go and util.go. Trace how buildGrouping and flattenTuples treat empty tuples; done means GROUP BY (()) and GROUP BY ((())) are rejected while GROUP BY () and GROUP BY (), () continue to work.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.