Skip to content

fix(sql-parser): accept global aggregates without GROUP BY - #826

Open
milindsrivastava1997 wants to merge 2 commits into
mainfrom
817-sql-aggregation-without-group-by-is-rejected
Open

milindsrivastava1997 wants to merge 2 commits into
mainfrom
817-sql-aggregation-without-group-by-is-rejected

Conversation

@milindsrivastava1997

@milindsrivastava1997 milindsrivastava1997 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Before this change, SQL global aggregates without GROUP BY were rejected by SQLPatternParser, because its empty grouping-label set was treated as a parse failure. For example, these queries could not be planned:

SELECT SUM(value) FROM data
WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW();

SELECT QUANTILE(0.95, value) FROM quantile_wide
WHERE time BETWEEN DATEADD(s, -1, NOW()) AND NOW();

After this change, an omitted GROUP BY parses as an empty grouping-label set. Single-layer global SUM, COUNT(column), MIN, MAX, QUANTILE, and COUNT(DISTINCT column) queries now plan and build an execution context, yielding a global result with no output grouping labels.

The parser also preserves empty grouping labels within nested SQL ASTs. Nested matching and execution remain intentionally unsupported and are deferred to #819.

Tests

  • cargo fmt --check
  • cargo test -p sql_utilities
  • cargo test -p query_engine_rust --test sql_query_support

Closes #817

@milindsrivastava1997 milindsrivastava1997 linked an issue Oct 11, 2026 that may be closed by this pull request

@milindsrivastava1997 milindsrivastava1997 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated code review: 6 findings, posted inline. Findings 1, 2, 5 and 6 concern code outside this diff, so they are anchored on the Some(group_bys) change that makes those paths reachable.

} else {
Some(group_bys)
}
Some(group_bys)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[correctness] Global SUM/COUNT can match a label-keyed CMS via capability fallback (asap-query-engine/src/engines/simple_engine/sql.rs:258)

Global aggregates now reach find_compatible_aggregation with empty grouping_labels, and labels_compatible (capability_matching.rs:157) compares only grouping_labels, never aggregated_labels.

Scenario: the planner plans SELECT job, SUM(value) ... GROUP BY job, giving CountMinSketch(sum) with grouping=[] and aggregated=[job]. An unplanned SELECT SUM(value) FROM data ... has grouping=[], matches that config, and point-queries the empty key, returning ~0 instead of the total. Before this PR these queries were rejected at parse time.

Suggested fix: require aggregated_labels to be empty when the requirement has no grouping labels, or exclude configs with aggregated labels.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Fixed in c9bc666. Non-top-k global aggregates now require an exact query configuration instead of capability fallback, so a grouped CMS cannot be selected for an unlabeled scalar query. Added global_aggregate_does_not_fallback_to_grouped_config.

} else {
Some(group_bys)
}
Some(group_bys)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[correctness] Global COUNT also matches top-k CountMinSketchWithHeap configs (asap-query-engine/src/engines/simple_engine/sql.rs:258)

Statistic::Count lists CountMinSketchWithHeap as compatible, and cms_family_sub_type_compatible only checks count vs sum mode. Heap configs always have grouping=[].

Scenario: SELECT srcip, COUNT(value) AS c ... GROUP BY srcip ORDER BY c DESC LIMIT 5 (heap: grouping=[], aggregated=[srcip], count) plus an unplanned SELECT COUNT(value) FROM t .... The global COUNT is served by the srcip-keyed heap sketch and returns a bogus empty-key estimate instead of the event count.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Fixed by the same c9bc666 guard: a non-top-k global COUNT cannot capability-match a keyed heap configuration. Exact configurations remain eligible, and existing top-k capability-fallback tests still pass.

} else {
Some(group_bys)
}
Some(group_bys)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[correctness] Subquery path skips the unexpected-SELECT-fields check (sqlpattern_parser.rs:244)

Inner subqueries without GROUP BY now parse, but the subquery branch never runs the DISTINCT/HAVING/QUALIFY check that the flat path applies.

Scenario: SELECT SUM(t) FROM (SELECT SUM(value) AS t FROM x WHERE time BETWEEN ... HAVING SUM(value) > 5) used to be rejected by get_groupbys and now parses with the HAVING dropped. Only the matcher's NestedQueryUnsupported stops it today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Deferred intentionally: this PR changes parsing only, while nested-query matching and execution remain unsupported. c9bc666 adds a matcher regression asserting NestedQueryUnsupported, so the parsed shape cannot enter execution. Tightening unsupported-clause validation inside nested parsing belongs with the nested-execution work.

} else {
Some(group_bys)
}
Some(group_bys)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[efficiency] Global SUM/COUNT plans a full CMS + DeltaSetAggregator for one scalar (asap-planner-rs/src/planner/sql.rs:124)

With zero aggregated labels, the planner allocates a multi-population CountMinSketch (width x depth) plus a key tracker for a population that is always one empty key. When spatial_output is empty, mapping to a single-population operator (Sum, or MinMax for exact) would be much cheaper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Acknowledged and deferred. This PR is scoped to parser acceptance and safe execution of exact global configurations; choosing a cheaper single-population physical plan is a planner optimization and should be addressed separately.

- name: sum
query: SELECT SUM(value) FROM data WHERE time BETWEEN DATEADD(s, -1, NOW()) AND NOW()
expected: unsupported
expected: supported

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[test-coverage] Newly supported cases don't check result values (applies to all ~30 flipped cases; runner at asap-query-engine/tests/sql_query_support.rs:90)

The runner only checks that an execution context gets built. Nothing ingests data and asserts that global SUM/COUNT/MIN/MAX/QUANTILE/COUNT(DISTINCT) combines all series. If the empty-grouping path returned one row per series or an empty result, every case would still pass. The parser tests only assert labels.is_empty().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Fixed in c9bc666 with global_sum_returns_one_unlabeled_result. It inserts two global SUM windows, executes the full SQL pipeline, and asserts one empty-label row with value 8.0; this catches both duplicate-per-series and empty-result failures.

)
.expect("nested global aggregates parse");

// Matching and execution may reject nested queries, but parsing retains both layers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[test-coverage] No negative-path test for nested global aggregates

This test only shows that parsing keeps both layers. Nothing asserts that the matcher/planner then rejects the shape with NestedQueryUnsupported, so loosening the n > 1 check in query_info_to_pattern would let these flow into execution with time_info UNUSED and no test would fail. The "may reject" comment also leaves the expected outcome unstated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Fixed in c9bc666. test_nested_global_aggregates_remain_unsupported_by_matcher now parses the nested global aggregate and asserts QueryError::NestedQueryUnsupported, making the accepted parser shape and rejected matcher shape explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sql: aggregation without GROUP BY is rejected

1 participant