Add Sparkbar aggregate function combinator#101352
Conversation
|
Workflow [PR], commit [46800c2] Summary: ❌
AI ReviewSummaryThis PR adds the Final VerdictStatus: ✅ Approve |
Introduces the `-Sparkbar` combinator that applies any aggregate function independently to each x-axis bucket and renders the per-bucket results as a Unicode sparkbar string. Usage: sumSparkbar(width, begin_x, end_x)(x_col, value_col) countSparkbar(width, begin_x, end_x)(x_col) uniqSparkbar(width, begin_x, end_x)(x_col, key_col) The first argument is the x-axis (bucket key) column; remaining arguments are forwarded to the nested aggregate function. The explicit range [begin_x, end_x] is required because bucketing must happen at aggregation time before the per-bucket states are accumulated. Supported key column types: UInt8–UInt64, Int8–Int64, Date, Date32, DateTime, DateTime64, Enum, Interval.
0d6db9a to
783a4a1
Compare
…ions - Fix error test annotations: wrong parameter count gives `NUMBER_OF_ARGUMENTS_DOESNT_MATCH` (42), not `SYNTAX_ERROR` - Remove empty-parens test case for "no key argument" since `f(params)()` is rejected by the parser with `SYNTAX_ERROR` before reaching our code - Replace non-triggerable nested-type test with unsupported x-axis type test - Add `.reference` file generated via `clickhouse-test --record` - Remove unused `BAD_ARGUMENTS` declaration from `.cpp` (it is used in `.h`)
The error text now lists all accepted key types: native integer, Date, Date32, DateTime, DateTime64, Enum, and Interval.
Strip Nullable wrapper with removeNullable before the isNumber check, so compositions like avgOrNullSparkbar are accepted.
Compute integer deltas before Float64 conversion to avoid catastrophic cancellation for large Int64/DateTime64 key values near ±1e18. Update documentation: the result can be an empty string when all buckets are empty or zero, rather than a fixed-width space-padded string.
DateTime64 literals are stored as Decimal64 fields, not UInt64. Move DateTime64 into its own branch and extract tick count via FieldVisitorConvertToNumber<Int64>. Fix "Let's you" -> "Lets you" in -Resample and -Sparkbar docs sections.
…binator columns are backed by , which implements but not . The previous branch for unsigned types called , causing a NOT_IMPLEMENTED exception at runtime. Fix: always read the x-axis key via and cast to . This is safe for all supported types: - (Date, DateTime, native UInt*): tick values fit in Int64 - / : is the natural accessor - : underlying stores the tick count as Int64 Also add a functional test covering x-axis parameters and update the file accordingly.
…tion DateTime64 parameters are stored in Field as DecimalField<DateTime64>, where getValue() returns the raw tick count. The previous code used FieldVisitorConvertToNumber<Int64>, which divides by the scale multiplier (e.g. 1000 for scale=3), turning ticks into seconds. In add(), getInt returns raw ticks, so begin_x/end_x and key were in different units and almost all rows were dropped. Fix: extract parameters via safeGet<DecimalField<DateTime64>>().getValue() to preserve raw ticks regardless of scale.
Pre-epoch DateTime64 timestamps have negative raw tick counts. The previous code used UInt64 as Key for DateTime64, so negative begin_x/end_x were cast to large unsigned values, causing the begin_x >= end_x constructor check to throw BAD_ARGUMENTS for valid cross-epoch ranges. Fix: use Int64 as Key for DateTime64 (same as Int*), keeping tick counts signed end-to-end. The UInt64 subtraction trick in add() is unaffected: after the key >= begin_x bounds check, both deltas are mathematically non-negative, so two's complement wraparound gives correct results. Add a cross-epoch test (begin_x=-2000ms, end_x=+2000ms) to prevent regression.
|
📊 Cloud Performance Report ✅ AI verdict: no significant changes detected. K_source=6 K_base=30 flagged=0/65 clickbench🟢 No significant changes tpch_adapted_1_official🟢 No significant changes Debug info
|
Initialize the temporary `tmp` variable in `transformAggregateFunction` so the `cppcoreguidelines-init-variables` clang-tidy check passes (the `arm_tidy` build treated it as an error). Also mirror `AggregateFunctionResample` by overriding `canMergeStateFromDifferentVariant` and `mergeStateFromDifferentVariant`, delegating per bucket to the nested function. Without this, merging a sparkbar state produced in a different aggregation/window variant of a nested function that supports cross-variant merge (such as `cramersV` or `theilsU`) would be rejected before any bucket could be merged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Picked up this PR via
Verified locally on an ARM build: the object compiles clean, the full test |
The DateTime64 branch rescaled the `begin_x`/`end_x` parameters down to the column scale, rounding `begin_x` up and `end_x` down. When the column scale was coarser than the parameter scale, a valid fine-scale range could collapse to `begin_x >= end_x`, making the constructor throw `BAD_ARGUMENTS` even though the range contained a representable column tick (and a truly sub-tick range threw instead of producing an empty bar). Bucket entirely at the finest scale among the column and the two bounds instead. All three quantities are then expressed in the same unit and obtained by exact multiplication (never division or rounding), so a range valid at fine scale can never collapse, single-tick ranges are aggregated, and sub-tick ranges render an empty bar. Keys read from the column are rescaled up to the working scale in `add` via a per-instance multiplier (1 for every non-DateTime64 key type). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Picked this up to advance it:
Built cleanly and |
The existing 04322_sparkbar_combinator test never exercises the cross-variant merge path (mergeStateFromDifferentVariant) that -Sparkbar implements. A nested-function state produced in a window context (...SparkbarState(...) OVER ()) stores a different aggregate function variant than the one used by ...SparkbarMerge, so finalizing it must go through mergeStateFromDifferentVariant for every per-bucket sub-state. Add 04329_sparkbar_combinator_state_variant, mirroring 04079_crosstab_window_state_variant_cast.sql, using cramersV as the nested function (it has a distinct window variant). The structural (width, begin_x, end_x) parameters are repeated on -Merge, as required for a combinator whose state layout depends on them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: the per-bucket index was computed in `Float64`, which loses precision once the range or the offset exceeds the 53-bit exact integer range, silently mis-bucketing valid `UInt64`/`Int64`/`DateTime64` keys near a bucket boundary. For example `countSparkbar(2, 0, 9007199254753337)` placed the key `4503599627376668` in bucket 1, although exact arithmetic puts it in bucket 0 (`2 * offset = range - 1 < range`), because `Float64(9007199254753337)` rounds to its nearest even double `9007199254753336`. Compute the bucket index with exact integer arithmetic instead. The deltas are already taken in `UInt64` (so the sign of `begin_x`/`end_x` and large magnitudes are handled by modular subtraction); the `offset * width` product is taken in 128 bits because it can exceed `UInt64`, then divided by the range. The quotient is at most `width` (reached only when `key == end_x`) and is clamped to the last bucket, matching the previous `std::min` clamp. Bucket assignments are unchanged for every existing test (all ranges are below 2^53); only the >2^53 boundary cases change, and they now match exact integer arithmetic. Add `04330_sparkbar_combinator_large_range_bucketing` covering `Int64` and `UInt64` keys at the exact bucket boundary of a >2^53 range, so reverting to a `Float64` index computation fails the test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After merging `master`, the test number 04330 collides with several existing tests (`04330_arrow_oob_declared_length`, `04330_show_policies_format_clause`, `04330_tuple_element_aggregation_disabled_by_default`). Renumber to 04342, the next free prefix, to keep test numbers unique. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`master` added a `Documentation` parameter to `registerCombinator` and gave every combinator an embedded description and syntax that are exposed through the new `system.aggregate_function_combinators` table. After merging `master`, `-Sparkbar` was the only combinator left without it. Add documentation matching that convention, mirroring the `-Resample` combinator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review "Request changes" verdict (exact 64-bit bucket indexing) and refreshed the branch against
Verified the changed combinator TU compiles against merged |
The embedded `system.aggregate_function_combinators.syntax` string for `-Sparkbar` documented only `Sparkbar(width, begin_x, end_x)`, omitting the optional nested aggregate parameters that the implementation forwards (e.g. the quantile level in `quantileSparkbar(0.9, width, begin_x, end_x)`). The Markdown docs already describe this optional prefix, so generated/system-table help pointed users to a form that cannot express parametric nested aggregates. Add the optional forwarded-parameter prefix to the embedded syntax so it matches the documented feature and `docs/.../combinators.md`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After merging `master`, the test prefixes `04322` and `04329` collided with several tests added on `master` (e.g. `04322_limit_by_monotonic_desc_key`, `04329_or_join_packed_key_merge`). Renumber the two colliding sparkbar tests to free prefixes: - `04322_sparkbar_combinator` -> `04344_sparkbar_combinator` - `04329_sparkbar_combinator_state_variant` -> `04345_sparkbar_combinator_state_variant` `04342_sparkbar_combinator_large_range_bucketing` was already unique and is left unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Picked this up via Merged Renumbered two colliding tests. After the merge, the prefixes
CI analysis — all failures are pre-existing and unrelated to this PR. This PR only adds an isolated new aggregate combinator (plus one registration line), and none of the failures touch that path:
No build was run locally: the merge is additive and the interfaces the combinator overrides are unchanged from the previously-built/tested merge-base, so CI will validate the build and tests. |
|
Addressed the AI Review's sole remaining (metadata) note: the PR body said The remaining CI reds are unrelated fuzzer flakes, not caused by this additive combinator:
|
Master renamed the virtual aggregate-function merge entry point: the public `merge` is now a non-virtual wrapper that asserts the source and destination states do not alias and forwards to a new pure-virtual `mergeImpl`. Overrides must now be done on `mergeImpl`, mirroring how `AggregateFunctionResample` was migrated. Rename the combinator's override accordingly; the body still calls the public `merge` on the nested function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Master grew several tests with the 04342 prefix, so move 04342_sparkbar_combinator_large_range_bucketing to the free 04346 prefix to keep a unique test number (adjacent to 04344/04345 sparkbar tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merged
Net diff remains purely additive (799 insertions, 0 deletions); the only existing files touched are the 6-line registration in |
LLVM Coverage ReportChanged lines: Changed C/C++ lines covered by tests: 153/182 (84.07%) | Lost baseline coverage: none · Uncovered code |

Closes: #59832
Adds the
-Sparkbarcombinator that applies any numeric-result aggregate function independently to each x-axis bucket and renders the per-bucket results as a Unicode sparkbar string.The combinator works with any aggregate function that returns a numeric type. The first argument is the x-axis (bucket key) column; remaining arguments are forwarded to the nested function. An explicit range
[begin_x, end_x]is required because bucketing happens at aggregation time.Supported key column types:
UInt8–UInt64,Int8–Int64,Date,Date32,DateTime,DateTime64,Enum,Interval.Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Added the
-Sparkbaraggregate function combinator. It applies numeric-result aggregate functions such assum,count, anduniqto values grouped into x-axis buckets and renders the results as a Unicode sparkbar string. Example:sumSparkbar(20, toDate('2022-01-01'), toDate('2024-12-31'))(Date, value).Documentation entry for user-facing changes
The combinator follows the same parameter convention as the existing
sparkbarfunction:{fn}Sparkbar(width, begin_x, end_x)(x_col, ...). Documentation should be added todocs/en/sql-reference/aggregate-functions/combinators.md.Note
Medium Risk
Adds a new aggregate combinator that changes aggregation state layout and parameter/type validation, which could impact query correctness/performance for affected functions. Risk is mitigated by strict argument checks and dedicated stateless tests across key types (including
DateTime64scaling).Overview
Adds a new
-Sparkbaraggregate function combinator that buckets rows by a first “x-axis” argument over an explicit[min_x, max_x]range, runs the nested aggregate per bucket, and returns a Unicode sparkbarString.The implementation registers the new combinator, enforces numeric nested-result types and supported key types (including
DateTime64scale alignment), and introduces stateless tests plus user docs describing syntax, parameters, and examples.Reviewed by Cursor Bugbot for commit 8ae26e0. Bugbot is set up for automated code reviews on this repo. Configure here.