Support group_by_use_nulls in WITH TOTALS#107106
Conversation
|
cc @novikd @vdimir — could you review this? It rebases #39574 and ports the |
|
Workflow [PR], commit [2c47bef] Summary: ❌
AI ReviewSummaryThis PR makes analyzer plans for Missing context / blind spots
Final VerdictStatus: ✅ Approve |
7507ca2 to
a54354e
Compare
Pre-PR validation gate (updated after addressing review)
Session id: cron:clickhouse-worker-slot-15:20260611-020200 |
a54354e to
a458d3e
Compare
Pre-PR validation gateRe-run for the test coverage update (a458d3e), which adds expression-level cases to
Session id: cron:clickhouse-worker-slot-31:20260611-030000 |
a458d3e to
878b0ce
Compare
Pre-PR validation gate (docs follow-up, head 878b0ce)This commit is a docs-only follow-up to the clickhouse-gh[bot] change-request: align the user-facing docs with the new
Session id: cron:clickhouse-worker-slot-4:20260611-042300 |
878b0ce to
a7eba4d
Compare
Pre-PR validation gateFor the
Session id: cron:clickhouse-worker-slot-8:20260611-051100 |
a7eba4d to
92e774a
Compare
Rebase and port of ClickHouse#39574 (original author @novikd) to the analyzer. With group_by_use_nulls enabled, GROUP BY keys are reported as Nullable for GROUPING SETS, ROLLUP and CUBE, but plain GROUP BY ... WITH TOTALS was left out: the keys stayed non-nullable and the TOTALS summary row showed 0 instead of NULL. The original PR fixed this in the old interpreter by converting the keys inside AggregatingStep. The analyzer is now the default and needs the same behavior, so this ports the fix to the analyzer in two places that gate the group_by_use_nulls handling on the GROUP BY modifier and previously listed only GROUPING SETS / ROLLUP / CUBE: * IdentifierResolveScope: include WITH TOTALS so the query tree resolves the GROUP BY keys (and expressions over them) as Nullable. Without this the planner-side conversion below produces a Nullable key column at runtime while query-tree analysis still treats the key as non-nullable, e.g. 'k IS NULL' over the TOTALS row folds to 0 and toTypeName(k) reports the non-nullable type, and a wrapping subquery sees the wrong key type. * PlannerExpressionAnalysis: include WITH TOTALS so the result type of the keys is analyzed as Nullable, consistent with the scope above. * Planner: insert a toNullable conversion for the GROUP BY keys right before the TotalsHaving step. The TOTALS row is built from default values, so a Nullable key column yields NULL there. This runs for CUBE/ROLLUP WITH TOTALS as well, because their own nullable conversion happens after TotalsHaving and would otherwise leave the TOTALS row non-NULL. Keys that cannot be made Nullable are skipped. GROUP BY ALL needs an extra step. Its key list is empty until expandGroupByAll collects the keys from the projection, and that normally runs near the end of resolveQuery, after resolveGroupByNode is the only place that registers keys in nullable_group_by_keys. So with the new WITH TOTALS gate, GROUP BY ALL took the delayed-resolution path with no registered keys: the projection was typed non-nullable while validateAggregates compared it against keys it had converted to Nullable, raising NOT_AN_AGGREGATE. resolveQuery now expands GROUP BY ALL before resolveGroupByNode when group_by_use_nulls is set. The expansion uses a throwaway clone of the projection so the real projection keeps its aliases and is not constant-folded against the pre-nullable types; the clone's resolved key nodes still match the real keys structurally, because nullable_group_by_keys ignores aliases and types. GROUP BY ALL WITH TOTALS then behaves exactly like the explicit GROUP BY case, and GROUP BY ALL WITH ROLLUP/CUBE (which had the same NOT_AN_AGGREGATE error before) works too. Constant GROUP BY keys are folded away before aggregation and are not real key columns, so they keep their constant value in the TOTALS row instead of becoming NULL. This is the same as CUBE/ROLLUP/GROUPING SETS WITH TOTALS, where a folded constant key likewise stays constant in the summary rows. 02373 covers the raw key value in the TOTALS row, a pure constant key, and a mixed real plus constant key. It also covers the expression level so the IdentifierResolveScope typing is protected: toTypeName(k) over the WITH TOTALS result, 'k IS NULL' which is 1 in the TOTALS row, and a wrapping subquery that selects the TOTALS row with WHERE k IS NULL. Without the IdentifierResolveScope change these report the non-nullable type and fold 'k IS NULL' to 0 while the raw value cases still pass, so they are the regression guard for that part of the fix. The same expression-level cases are added for GROUP BY ALL WITH TOTALS as the regression guard for the expansion-order fix above. Documentation: the group_by_use_nulls setting text in src/Core/Settings.cpp and the WITH TOTALS section in docs/en/sql-reference/statements/select/group-by.md now state that with group_by_use_nulls = 1 the real GROUP BY keys in the WITH TOTALS row are reported as NULL, qualified to only keys whose types can be wrapped in Nullable. Keys whose types cannot be made Nullable (for example Array and Map, which fail canBeInsideNullable) keep their default value, as does a folded constant key. The behavior is analyzer-only; the affected tests set enable_analyzer = 1. Co-authored-by: Dmitry Novik <novikd@clickhouse.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92e774a to
0d313cb
Compare
Pre-PR validation gateThis round addresses the clickhouse-gh[bot] change-request on
Session id: cron:clickhouse-worker-slot-2:20260611-063900 |
|
CI finished on
The remaining red marks (Mergeable Check, PR) are derivative aggregators of the above. All other checks pass. Session: cron:our-pr-ci-monitor:20260611-120000 |
|
CI finished on HEAD Session id: cron:our-pr-ci-monitor:20260611-160000 |
|
@groeneai, as the bad integration test is fixed, update the branch in this PR, so we will have a chance to merge it. |
|
@alexey-milovidov done. Branch updated to the latest master (merged |
The group_by_use_nulls conversion for WITH TOTALS keys is implemented only in the analyzer planner (PlannerExpressionAnalysis / Planner). With the old query interpreter (enable_analyzer = 0) the keys are not converted, and the totals row keeps the default key value. Qualify the group-by.md WITH TOTALS section and the group_by_use_nulls setting text in Settings.cpp so the documented public contract matches the analyzer-only behavior. Verified: enable_analyzer=1 totals key = NULL / Nullable(UInt64); enable_analyzer=0 totals key = 0 / UInt64 (unchanged). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gateDocs/changelog-only change (qualify WITH TOTALS +
Session id: cron:clickhouse-worker-slot-1:20260613-144000 |
|
@alexey-milovidov Branch is on the latest master. CI is now green except The fuzzer hit Evidence:
#104350 ("Fix correlated subquery + GROUP BY ROLLUP under group_by_use_nulls", issue #91119) is the dedicated fix and is modifier-agnostic. Folding that analyzer change into this PR would duplicate and conflict with #104350 (both edit the same My recommendation is to treat this fuzzer finding as #91119 (tracked, fix in #104350) and not block this PR on it, or to land #104350 first. How would you like to proceed? |
|
@novikd Crash: Minimal repro (this PR, WITH TOTALS): SELECT (SELECT sum(number)), number FROM numbers(10) GROUP BY number WITH TOTALS SETTINGS group_by_use_nulls = 1;Same crash on pure master (no commits from this PR), CUBE/ROLLUP: SELECT (SELECT sum(number)), number FROM numbers(10) GROUP BY CUBE(number) SETTINGS group_by_use_nulls = 1;
SELECT (SELECT sum(number)), number FROM numbers(10) GROUP BY ROLLUP(number) SETTINGS group_by_use_nulls = 1;Verified on master Root cause: Since you own the decorrelation code and this feature, your call on scope: a. Land WITH TOTALS here and fix the general decorrelation + I have the repro and the type trace ready and can implement whichever you prefer. |
LLVM Coverage ReportChanged lines: Changed C/C++ lines covered by tests: 56/60 (93.33%) | Lost baseline coverage: none · Uncovered code |

Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
GROUP BY ... WITH TOTALSnow respects thegroup_by_use_nullssetting when the analyzer is enabled (enable_analyzer = 1, the default): the GROUP BY keys in the TOTALS summary row are reported asNULLinstead of their default values, consistent withGROUPING SETS,ROLLUPandCUBE. Keys whose types cannot be wrapped inNullable(for exampleArrayorMap) keep their default value. With the old query analyzer (enable_analyzer = 0) the keys keep their default value as before.Description
Rebase and port of #39574 (original author @ novikd) on top of current master.
With
group_by_use_nulls = 1, GROUP BY keys are already reported asNullableforGROUPING SETS,ROLLUPandCUBE, but plainGROUP BY ... WITH TOTALSwas left out: the keys stayed non-nullable and the TOTALS summary row showed0(the default value) instead ofNULL.The original PR fixed this in the old interpreter by converting the keys inside
AggregatingStep. The analyzer is now the default, so this ports the fix to the analyzer planner:PlannerExpressionAnalysis: includeWITH TOTALSin thegroup_by_use_nullscondition so the result type of the keys is analyzed asNullable.Planner: insert atoNullableconversion for the GROUP BY keys right before theTotalsHavingstep. The TOTALS row is filled from default values, so aNullablekey column yieldsNULLthere. This also coversCUBE/ROLLUP WITH TOTALS, whose own nullable conversion happens afterTotalsHavingand would otherwise leave the TOTALS row non-NULL. Keys that cannot be madeNullableare skipped.Example (before this change the last row was
0):The behavior is analyzer-only, so the affected tests set
enable_analyzer = 1, and the documentation/changelog qualify the behavior as applying only with the analyzer. Updated tests:02343_group_by_use_nulls,02535_analyzer_group_by_use_nulls,03912_tuple_comparison_group_by_use_nulls, and the new02373_group_by_use_nulls_with_totals(from the original PR).