Implement PromQL functions sum_over_time(), avg_over_time() and count_over_time() - #112353
Implement PromQL functions sum_over_time(), avg_over_time() and count_over_time()#112353valerypetrov wants to merge 82 commits into
Conversation
|
Workflow [PR], commit [d76d224] AI ReviewSummaryThis PR adds PromQL Findings
Tests
Final Verdict
|
…er_time() Add a new AggregateFunctionTimeseriesSumCountAvg aggregate function that computes a shared (sum, count) accumulator over the sliding grid window and projects it to sum, avg or count depending on a compile-time Kind selector, mirroring the existing bool-template idiom used by AggregateFunctionTimeseriesExtrapolatedValue. The (sum, count) Summary is a trivially invertible monoid, so it reuses AggregateFunctionTimeseriesSlidingSum's fast running-sum path instead of the two-stacks/recompute paths needed by non-invertible aggregates. Register timeSeriesSumToGrid, timeSeriesAvgToGrid and timeSeriesCountToGrid in AggregateFunctionTimeseriesHelpers.cpp, and wire sum_over_time, avg_over_time and count_over_time into applyFunctionOverRange.cpp's impl_map with drop_metric_name=true (these are value-transforming aggregations, like rate/increase/delta, not last_over_time's passthrough). Add SQL-level test 04628_timeseries_sum_avg_count_over_time and PromQL regression tests in test_evaluation.py covering all three functions plus the empty-window/no-samples case.
00cc2e9 to
0beac3a
Compare
…input value type timeSeriesCountToGrid projected its result as the input value column's ValueType, so a Float32 `value` column made count_over_time return Array(Nullable(Float32)): counts above 16777216 rounded, and even small counts were constrained to Float32 precision. This contradicted the function's documented Array(Nullable(Float64)) result. AggregateFunctionTimeseriesBase now lets Traits optionally declare a ResultType distinct from ValueType (defaulting to ValueType for every other timeseries aggregate, so their behavior is unchanged), and the Count specialization of AggregateFunctionTimeseriesSumCountAvgTraits uses it to always project to Float64. Sum and Avg keep projecting to ValueType as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on and fix Float32 date-function precision Two Float32 TimeSeries-table precision gaps remained after the previous commit, both flagged by review on this PR: - count_over_time() was only fixed at the aggregate-function layer. applyFunctionOverRange() still handed its result to a VECTOR_GRID, and finalizeSQL()/getResultColumns() unconditionally cast/declared every VECTOR_GRID's value column as context.scalar_data_type (the table's sample type), rounding the new exact Float64 count back down to Float32 before it reached the client. SQLQueryPiece gains an optional value_data_type override (nullptr by default, so every other function is unaffected); applyFunctionOverRange()'s ImplInfo sets it to Float64 only for count_over_time(), and finalizeSQL()'s two VECTOR_GRID branches now consult it. getResultColumns() had no SQLQueryPiece to consult at all, so Converter::getResultColumns() now builds one via the same tree walk getSQL() uses, but only trusts the override when the top-level piece's own value column is actually produced with StoreMethod::VECTOR_GRID - matching exactly the condition finalizeSQL() now checks, so the declared and actual result types can never disagree for any composition. - Date/time functions (minute(), hour(), day_of_week(), etc.) still computed their result from an evaluation-time argument that flowed through context.scalar_data_type, which can be Float32 and only has ~128 seconds of precision at today's epoch magnitude - enough to flip the returned calendar component near a boundary (e.g. minute(vector(time())) at 1770582700, which rounds to 1770582656). This is the same bug already fixed on the unrelated promql/date-funcs-0arg branch, but that fix relied on a 0-argument date-function feature and a makeTimeQueryPiece()/Native() split this branch doesn't have. Ported the same idea instead: fromFunctionTimeNative() is a sibling of fromFunctionTime() that keeps the evaluation time typed context.timestamp_data_type; applyDateTimeFunction() detects the vector(time()) argument shape at the AST level and swaps in the native-precision piece before delegating to applySimpleFunction(). time() itself is untouched and still legitimately returns a scalar/float value. Added Float32-table PromQL regressions for both: an end-to-end count_over_time() check (16777217 raw samples, verified via both the SQL table-function path and the standalone dynamic-table HTTP API path) and a minute(vector(time())) check at a non-128-aligned timestamp, both via instant and range queries. Verified against the built binary: count_over_time's Float64 precision and the date-function fix work end-to-end; sum_over_time/avg_over_time/rate/ increase/delta/last_over_time remain unaffected (spot-checked and via the unchanged 04628_timeseries_sum_avg_count_over_time.sql reference); the standalone compliance harness still reports the unchanged baseline of 402 pass / 11 fail / 124 unsupported / 2 ref_mismatch (539 total). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ppers The previous fix only preserved count_over_time()'s exact Float64 result through the direct top-level VECTOR_GRID finalization path. Once the result flowed through a wrapper, the override was silently dropped and the value was cast back down to the table's sample type: - applySimpleFunction() (abs(), sin(), date/time functions, etc.) built its result as a fresh SQLQueryPiece and never copied value_data_type from its VECTOR_GRID argument, so abs(count_over_time(...)) lost the override entirely. - finalizeSQL()'s SINGLE_SCALAR/SCALAR_GRID branches (used by scalar() and by vector()-wrapped/subqueried scalars) unconditionally cast to context.scalar_data_type, ignoring value_data_type even when set. - Converter::getResultColumns() only honored value_data_type for INSTANT_VECTOR/RANGE_VECTOR results produced via VECTOR_GRID, so a top-level scalar(count_over_time(...)) query would still advertise the table's sample type as its column type. Fix all three: applySimpleFunction() now copies value_data_type from its (at most one) VECTOR_GRID argument; every SINGLE_SCALAR/SCALAR_GRID finalization branch now applies the same value_data_type-or-fallback pattern already used by the VECTOR_GRID branches; and the getResultColumns() override condition now matches store method (VECTOR_GRID/SINGLE_SCALAR/ SCALAR_GRID) instead of gating on ResultType, keeping it in sync with finalizeSQL(). Verified end-to-end on a Float32 TimeSeries table that scalar(), abs(), and vector(scalar(...)) wrapping (including through a subquery) all now return the exact Float64 count instead of rounding it back to Float32, while sum_over_time()/avg_over_time() and unrelated applySimpleFunction() users (rate(), bool-modifier comparisons) are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three more places silently dropped count_over_time()'s Float64 override back to the table's sample type: - applyFunctionOverRange() gated the override on `has_group`, but `has_group` only reflects whether the *argument* was grouped, not the type of the aggregate function actually being built. count_over_time() over a scalar/subquery-sourced range vector (e.g. count_over_time(vector(1)[...])) produced a SCALAR_GRID/ungrouped piece and lost the override entirely. - applySimpleFunction() only copied value_data_type in its VECTOR_GRID branch. A scalar()-wrapped count_over_time() result reaching this function as a SINGLE_SCALAR/SCALAR_GRID/CONST_SCALAR argument (e.g. as an operand of a math/comparison operator) lost the override again. - The binary-operator files that build a fresh VECTOR_GRID SQLQueryPiece (applySimpleBinaryOperator, applyBinaryOperatorOr/And/Unless) never copied value_data_type from either operand, so combining two count_over_time() results (or mixing one with an ordinary metric) via +, or, and, or unless lost the override too. Fixed each based on which operand(s) actually determine the output value: arithmetic operators and `or` may draw from either operand, so they merge value_data_type via a new mergeValueDataType() helper (falling back to the default type on a genuine conflict, which can't happen today since Float64 is the only override in use); `and`/`unless` only ever forward the left operand's own values, so they take left's value_data_type unconditionally. Comparison operators without the bool modifier needed special handling: they act as a filter that keeps exactly one operand's value unmodified (whichever side is/contains the instant vector), so applyComparisonOperator() now captures that side's value_data_type before calling applySimpleBinaryOperator() and applies it to the result afterwards - otherwise a plain metric compared against a count_over_time() result would wrongly inherit Float64 from the *discarded* operand. Audited the remaining files that reference these store methods (applyAggregationOperatorQuantile, applyHistogramQuantile, applyLabelManipulationFunction, applyLimitAggregationOperator, applyOffset, applyOneArgumentAggregationOperator, applyUnaryOperator, dropMetricName, fromFunctionTime, toVectorGrid): all but applyHistogramQuantile and fromFunctionTime already preserve value_data_type correctly because they mutate their argument/result in place or copy it wholesale, and doing so is the right call - e.g. sum/min/max/quantile/topk of count_over_time() results should stay exact Float64. applyHistogramQuantile() computes a genuinely new interpolated estimate (not a copy of its input), so it correctly keeps defaulting to the table's sample type; fromFunctionTime() takes no arguments and can never receive a count_over_time()-derived value at all. Verified end-to-end on a Float32 TimeSeries table: all newly-fixed cases now return exact Float64, all four previously-fixed cases (bare count_over_time, scalar(), abs(), vector(scalar())) still work, ordinary (non-count_over_time) uses of every touched file are unaffected (regular +, comparisons with and without bool, or/and/unless, unary minus, topk, sum/quantile by(...), label_replace, offset/@), and quantile/sum aggregation of count_over_time() correctly stays exact Float64 through aggregation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e docs
Two issues found by review, both confirmed against current code:
1. test_date_time_functions_float32_precision()'s instant- and range-query
assertions used execute_query_in_prometheus()/execute_range_query_in_prometheus(),
which hit the real Prometheus reader/receiver services. Per
configs/prometheus.xml, those services' /api/v1/query and /api/v1/query_range
handlers (my_rule_5/my_rule_6) are hardcoded to `default.prometheus`, never
`prometheus_f32` - so neither assertion ever touched the Float32 table this
test exists to cover. Worse, since the query is vector(time())-based (no
series selector), real Prometheus can evaluate it entirely client-side
without even issuing a remote-read, so these assertions may not have
exercised ClickHouse's PromQL engine at all. The test would have stayed
green even if the Float32 date-function fix were completely reverted.
Fixed both assertions using the same dynamic-table HTTP API pattern
test_count_over_time_float32_precision() already uses: hit ClickHouse's
own /dynamic_table/api/v1/query handler (my_rule_10) directly, with the
table supplied via the `table` query parameter so it can actually target
`prometheus_f32`. No equivalent range-query handler existed, so added
my_rule_12 (/dynamic_table/api/v1/query_range) mirroring my_rule_10. This
is a low-risk, purely additive config change: PrometheusRequestHandler.cpp
dispatches Instant vs. Range by the trailing path segment of the actual
request URL (uri_path.ends_with("/query_range") vs "/query"), not by
anything baked into the handler's configured <url>, so the new rule can't
affect any existing handler's behavior. Chose this over dropping the
range-query HTTP-API assertion as redundant with the SQL-level check below
it, since covering both the HTTP API and SQL surfaces (like the
count_over_time precision test already does) is more thorough for the
same low cost.
Audited every other execute_query_in_prometheus()/execute_range_query_in_prometheus()
call in the file for the same table-mismatch bug: only this test function
had it. test_count_over_time_float32_precision() already used the correct
dynamic-table pattern throughout.
2. AggregateFunctionTimeseriesHelpers.cpp's ReturnedValue doc strings for
timeSeriesSumToGrid/timeSeriesAvgToGrid claimed a fixed
`Array(Nullable(Float64))` result. That's only true for
timeSeriesCountToGrid - per AggregateFunctionTimeseriesSumCountAvg.h's
ResultType, Sum/Avg deliberately keep projecting to the input `value`
column's own type (confirmed correct and already covered by
04628_timeseries_sum_avg_count_over_time.sql, which explicitly checks
Float32 input yielding Array(Nullable(Float32)) for both). Fixed the docs,
not the behavior: reworded both descriptions to "of the same type as
`value`" (matching the existing "of the same type as `x`" convention used
in FunctionsRound.cpp/changeDate.cpp) and changed their `types` list to
`Array(Nullable(Float*))` (matching the `Float*` wildcard convention
AggregateFunctionQuantileExactWeighted.cpp etc. use for the same
type-preserving pattern).
Verification:
- Built standalone via ninja; `SELECT returned_value FROM system.functions`
confirms the new Sum/Avg doc text renders correctly at runtime, and
timeSeriesCountToGrid's doc is untouched.
- Ran 04628_timeseries_sum_avg_count_over_time.sql directly and diffed
against its .reference file: zero differences, confirming the doc fix is
behavior-neutral.
- Docker isn't available in this sandbox (as in prior rounds on this
branch), so replicated the fixed integration-test assertions by hand
against a standalone clickhouse-server using the exact same
prometheus_test_utils helpers the test calls: both the instant- and
range-query assertions pass against a live `prometheus_f32` table
(minute=31 via both endpoints, matching the date-function fix), and
pointing the same query at a nonexistent table returns an error rather
than silently succeeding - confirming the assertions are genuinely keyed
off the `table` parameter and therefore do exercise `prometheus_f32`,
unlike the assertions they replace.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pers around time() The Float32-precision fast path for date/time functions (minute(), hour(), etc.) previously only fired for the exact vector(time()) AST shape. But applyFunctionScalar() and applyFunctionVector() are value-preserving no-ops for their argument's underlying value, and applyUnaryOperator()'s '+' case is too, so wrapper forms like scalar(vector(time())) or vector(scalar(vector(time()))) bypassed the fast path and fell back to the lossy context.scalar_data_type cast, which can be Float32 on a TimeSeries table with Float32 samples. Generalize asVectorOfTime() into findTimeCallThroughScalarVectorWrappers(), which recursively peels off any nesting of scalar(...), vector(...), and unary '+' around a bare time() call before falling back to the generic (possibly lossy) conversion path. Unary '-' is intentionally not peeled, since it actually negates the value rather than passing it through unchanged.
…ppers findTimeCallThroughScalarVectorWrappers() already peeled scalar()/vector()/ unary '+' wrappers around time() before falling back to the lossy context.scalar_data_type cast. Extend it to also peel an Offset node (the "@ <timestamp>" / "offset <duration>" AST wrapper): applyOffset.cpp's offsetEvaluationTime()/setEvaluationTime() are value-preserving no-ops for CONST_SCALAR/SINGLE_SCALAR/SCALAR_GRID pieces (only start_time/end_time/step bookkeeping changes, never scalar_value/select_query), and NodeEvaluationRangeGetter pre-computes each node's evaluation range in a separate upfront pass, already folding the @/offset adjustment into the range it assigns to the wrapped inner expression - so looking up the range for the innermost time() node directly still yields the correctly shifted start_time/end_time. Verified empirically that, per the current PromQL grammar/parser, an Offset node is only ever constructed directly around an InstantSelector, RangeSelector, or Subquery - never directly around a Function or UnaryOperator - so this branch is presently unreachable via any valid PromQL syntax (queries like `vector(time()) @ 123` fail to parse). It's kept as a defensive, provably non-regressing generalization consistent with the existing scalar()/vector()/unary-'+' peeling, documented inline. No new regression test was added for this specific path since no valid query exercises it; all pre-existing regression tests (Float32 date/time precision, count_over_time/sum_over_time/avg_over_time, value_data_type propagation) and SQL-level PromQL parser tests were re-verified to still pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 429/448 (95.76%) · Uncovered code |
|
Please condense all the comments in the code to a single (max two) lines. |
No worries, let me fix that. Thank you for the review! |
Build profile diff (arm_release)Comparing Binary sizes
Only the stripped binary is compared: the official master build keeps debug symbols while PR builds strip them, so the other binaries differ by construction. Object file sizes
|
| Object file | Master | PR | Δ |
|---|---|---|---|
src/AggregateFunctions/CMakeFiles/clickhouse_aggregate_functions.dir/TimeSeries/AggregateFunctionTi… |
4.90 MiB | 6.31 MiB | +1.41 MiB (+28.72%) |
716 more object files are built by the master warmup baseline only (it builds every object-file target, a pull request build only clickhouse-bundle) and not compared.
Compile time of recompiled translation units
48 translation units recompiled, 114 s compile time in total, 48 of them have a recent master baseline.
Resolves a conflict in getResultColumns.cpp where master renamed the
PQT alias to PrometheusQueryTree while this branch added the
value_data_type_override parameter for sum_over_time/avg_over_time/
count_over_time result typing; kept both changes.
Also fixes two spots left over by git's automatic (non-conflicting)
merge in fromFunctionTime.{h,cpp} and applyDateTimeFunction.cpp that
still referenced the now-removed PQT alias after master dropped it,
replacing them with PrometheusQueryTree to match the rest of the
codebase.
These functions return Array(Nullable(Float64)), one value per grid point, so the added assertions must not apply tupleElement to an element: the tuples in this test's other expectations come from a query that zips the timestamps in itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
The nanosecond-precision test asserted that a carrier inside `topk`'s first argument leaves the result unrestricted, which is the assumption the review refuted: `k` decides how many series survive. That query's `k` is in fact far larger than the series count, so its own answer was right, but the walk cannot prove that and fails closed on what it cannot prove - the same treatment unary `-` already gets here. Also fixes the added nested-empty regression: a range selector applies only to a vector selector, so the inner argument has to be a subquery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
Review feedback from vitlibar: keep only what sum_over_time / avg_over_time / count_over_time need. Dropped the evaluation-time carrier feature entirely - applyDateTimeFunction's carrier walk, fromFunctionTime, the round_evaluation_time_down context flag and the Converter refactor that exported convertNode for it, along with its tests and the query_range handler they used. Those four source files are back to their master contents. Kept the value type override plumbing: it exists because count_over_time returns Float64 on a table whose samples are Float32, so it has to survive every operator, the empty-result short circuits and the result-column schema. That is this PR's own contract, not the removed feature's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
Review feedback from vitlibar: split the shared implementation and its traits into three, one per file, and use Kahan summation for the average. Each function now owns its summary, which is what the three actually differ in: - count keeps only a UInt64, exact under subtraction, so it alone stays invertible and its window is an O(1) running sum. - sum keeps a plain Float64 and has no unmerge, so its window is recomputed. - avg sums with Kahan-Babuska-Neumaier compensation. Each addition's rounding error is carried in a second accumulator and folded back in at the end, so samples far smaller than the running sum survive: beside 1e16, whose ulp is 2, a plain sum drops every following 1. The kind enum, the conditional ResultType, the constrained unmerge and the if-constexpr projection in getResult all disappear with the split. Test: a window holding 1e16 followed by four 1s must average (1e16 + 4)/5, which a plain Float64 sum cannot produce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
More of vitlibar's "remove anything else": these operators never receive a sum/avg/count_over_time result in any test, so carrying the Float64 override through them was speculative rather than required. Reverted to master: offset, and/or/unless, clamp, the limit and quantile aggregation operators, and the one-argument aggregation operator. What stays is the path the Float64 contract is actually tested on: count_over_time itself, `+`, `<`, abs(), scalar(), the empty-range short circuits, and the result schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
The previous commit removed the value type propagation from offset, and/or/ unless, clamp, and the limit/quantile/one-argument aggregation operators, but left the assertions that exercised it. Six of them expect Float64 where the reverted code now yields the table's Float32, and the group() case is inverted the other way: applyOneArgumentAggregationOperator copies the whole piece, so group() now inherits its operand's override instead of clearing it. Removed the and/unless, sum(), clamp, or, group() and count() assertions. What stays is the coverage for the paths that kept their propagation: count_over_time itself, abs(), scalar(), `+`, the comparison operators, and `> bool`, which still clears the override in applyComparisonOperator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same cleanup as ClickHouse#112353. timestamp() is the only producer of a value type override, and no test in this PR, in 0_stateless, in test_evaluation.py or in the compliance suite composes timestamp() with and/or/unless, an aggregation, quantile, limitk, histogram_quantile, label_replace, offset or a range function -- so carrying the override through those operators was speculative. Reverted to master: the and/or/unless operators, clamp, offset, the limit, quantile and one-argument aggregation operators, label manipulation, histogram quantile, and applyFunctionOverRange's empty and CONST_SCALAR branches. applySimpleBinaryOperator keeps the vector/vector site the tests cover and loses the empty one they do not. What stays is the path the tests exercise: applyFunctionTimestamp itself, applySimpleFunction, applyFunctionScalar, toVectorGrid, finalizeSQL and the result schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…) revert tryEvaluateOneArgumentMathFunction() had its only caller in applyDateTimeFunction.cpp's constant-folding carrier walk, which the previous commit removed entirely. Reverts applyOneArgumentMathFunction.cpp/.h to their master contents; isOneArgumentMathFunction() still dispatches these functions in applyFunction.cpp and is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`AggregateFunctionTimeseriesSamples` collapsed equal-timestamp duplicates into one sample keeping the largest value, so `timeSeriesCountToGrid` counted distinct timestamps instead of rows and `timeSeriesSumToGrid`/ `timeSeriesAvgToGrid` folded only the per-timestamp maximum. Aggregate functions should consider duplicates. The samples storage gains a `keep_duplicates` policy (default off, so changes, extrapolated-value and linear-regression aggregates are unaffected): `add` appends equal timestamps instead of collapsing them, the sorted invariant becomes non-decreasing, and `deduplicateSorted` is compiled out. The sum/avg/ count traits opt in, and their `FORMAT_VERSION` goes 1 -> 2 because post-change states can hold duplicate timestamps that an old binary would silently re-collapse on deserialize. The stateless test now pins the multiplicity: for the 7-sample input with two repeated timestamps on a single-point grid, count is 7, sum is 16.5 and avg is 16.5/7.
`timeSeriesSumToGrid` and `timeSeriesAvgToGrid` always recomputed the window per grid point, which gets expensive once a window holds many populated buckets. Their summaries are not invertible (a Float64 sum cannot be un-merged), so `SlidingSum` offers the two-stack monoid queue as the alternative removal strategy. Mirror the `AggregateFunctionTimeseriesLinearRegression` wiring: the traits declare the same `AVG_POPULATED_BPW_TO_ENABLE_TWO_STACKS` = 10 and `BPW_TO_FORCE_TWO_STACKS` = 20 thresholds (their `Summary::merge` is no pricier than regression's, so the same crossover applies), and `createAggregator` applies the same density estimate - enable two-stacks when the average populated buckets per window reaches the threshold or the window capacity hits the hard cap, reserving `min(buckets_per_window, num_populated_buckets)`. `timeSeriesCountToGrid` is unchanged: its summary is invertible, so `SlidingSum` keeps the O(1) running sum and never uses the queue.
AggregateFunctionTimeseriesBase reads FunctionImpl::FORMAT_VERSION, so the constant must live on the function class like every sibling declares it, not on the traits. Newer clang rejects the traits placement as a non-constant-expression read during Base's own initialization.
|
@vitlibar The two remaining aggregate items are in: buckets keep duplicate timestamps for sum/avg/count (bcd26ce, with FORMAT_VERSION bumped and the 04628 test pinning multiplicity), and sum/avg use the two-stack queue with the same thresholds as LinearRegression (ed3b82c). Could you take another look? |
Conflict in AggregateFunctionTimeseriesSamples.h: master extracted the duplicate-timestamp rule into timeseriesMaxValueForDuplicateTimestamp(), this branch made the collapse conditional on `keep_duplicates`. Kept both - the policy switch now calls the shared helper. Master also reworked the two-stacks API the sum/avg traits use, so the three new headers are adapted to it: FORMAT_VERSION moves into the traits (the base now reads Traits::FORMAT_VERSION), createAggregator() takes the stack size the base computes via getStackSizeForTwoStacks() instead of redoing the density math, and the redundant DateTime64Supported declarations are dropped along with the siblings'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Per nikitamikhaylov's review request on this PR. The three blocks left over two lines were added after the earlier sweep: the Samples class summary, the inherited-override note in applyFunctionOverRange, and three comments in the value-type regressions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Kahan-Babuska-Neumaier summary's merge is not commutative: which of the two sums is larger decides where the rounding error lands, and the added compensation folds into a different running total. The two-stacks queue combines bucket summaries in queue order, so on dense windows the reported average depended on the queue state. Drop the two-stacks thresholds from the avg traits so getStackSizeForTwoStacks always returns 0 and the window is recomputed in time order; timeSeriesSumToGrid keeps the shortcut, its merge is commutative. The regression pins the last grid point of a 25-bucket-per-window grid (the old forcing threshold) against the single-point grid over the same window, which combines its samples sequentially: values whose compensation is order-sensitive make the two orders differ by an ulp, and the dense grid now equals the sequential reference exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scope cut in 8495079 reverted the value type propagation from the one-argument aggregation operators, clamp and or, but count()'s own fixed type is required, like count_over_time()'s: a series count is exact only in Float64, which Float32 stops representing above 16777216. Recover just that part - the override replaces the argument's inherited override on the non-empty path and is reported on the compile-time-empty path too - and the two empty fast paths that drop an override the operand carried: clamp with constant max < min, and or with both sides compile-time empty, where the early return handed back one operand verbatim and the operand order decided the type. The general propagation through sum/quantile/topk stays reverted per vitlibar's scope cut. Restores the count(), clamp() and or assertions of test_evaluation.py that exercised exactly these paths, on the Float32-backed table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Update on the open items:
On the older |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
…rrides Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
PromQL Compliance ReportBaseline: S3 master
Uploaded compliance JSON for this PR commit Baseline updateThis run reports 81.26% vs master baseline 77.74% ( |
Both operators pass the left operand's values through, but neither copied its value_data_type: the empty fast paths and the VECTOR_GRID results dropped it, so a Float64 override reverted to the table's Float32 and exact counts above 2^24 rounded again. The override now follows the values, independent of whether the result contains rows. already carried it (it moves the surviving side), as does the arithmetic operator's empty path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
The non-empty or path built its result without copying either operand's value_data_type, so a Float64 grid narrowed back to the table type before reaching the client. My previous reply on this thread said or carried the override everywhere; that was only true of its empty fast paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
The two-stacks queue regroups bucket summaries, which needs an associative merge; Float64 addition is commutative but not associative, so opting sum_over_time in made the result depend on the queue's front/back split. With samples 1, 1, 1e16 in one window, a grid dense enough to force the queue reports 1e16 while the recompute path reports 1e16 + 2. Drop the thresholds and correct the comments that credited commutativity as sufficient. The new case in 04628 fails on the current build and passes with the queue disabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
Merging rounded per-bucket totals made the average depend on where the grid put the bucket boundaries, which is an internal detail and not part of avg_over_time. The window now keeps its buckets -- they outlive the aggregator, so they are referenced, not copied -- and getResult runs one Kahan-Babuska-Neumaier pass over their samples in timestamp order. The serialized state is unchanged: only buckets are written, and the aggregator lives and dies inside doInsertResultInto. The new 04628 case pins the property: 25 one-second buckets and 5 five-second buckets over the same 25 samples report ...99.4 and ...99.5 on the current build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
The three new registrations copied an older doc block that advertises only UInt32/DateTime timestamps and integer step/staleness, while the factory accepts the DateTime64 / fractional / decimal / string surface that timeSeriesRateToGrid in the same file already documents; 04628 exercises these functions with DateTime64(3) timestamps. Mirror that wording, and state that these three keep duplicate timestamps rather than collapsing them to the greatest value like their neighbours. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcH1uX3QLcLes3XfidQqsC
Related: #57545
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
PromQL: implemented functions
sum_over_time(),avg_over_time()andcount_over_time().Description
Implements three PromQL range-vector functions previously reported as not implemented.
sum_over_time,avg_over_timeandcount_over_timeare three different projections of the same(sum, count)accumulator, so they share one new header (AggregateFunctionTimeseriesSumCountAvg.h) with a single invertibleSummary{sum, count}monoid (merge adds both fields, unmerge subtracts them), plugged into the existing genericAggregateFunctionTimeseriesSlidingSumsliding-window fast path — the same infrastructure already proven byrate/increase/delta. No changes were needed to the generic grid/bucket machinery (AggregateFunctionTimeseriesBase.h) or tocheckArgumentTypes(all three take a single range-vector argument, the existing default).Registered
timeSeriesSumToGrid/timeSeriesAvgToGrid/timeSeriesCountToGridinAggregateFunctionTimeseriesHelpers.cppand added the threeimpl_mapentries inapplyFunctionOverRange.cppwithdrop_metric_name = true, matchingrate/increase/delta's convention.Verified against Prometheus 3.5.0.
Testing: new SQL-level test (
04628_timeseries_sum_avg_count_over_time.sql) plus cases intests/integration/test_prometheus_protocols/test_evaluation.pycovering all three functions, an empty-window case, a window narrower than the step (grid points with no sample must be absent/NULL, not zero), and a nestedavg_over_time(rate(...)[2m:10s])case. This flips 19 queries of the PromQL compliance suite from unsupported to passed.Workflow [PR]
Sync PR [sync-upstream/pr/112353]