Summary
topk(k, <bare instant selector>) by (labels) — e.g. topk(3, http_requests_total) by (job) ("top-3 series per job") — does not lower correctly. This is a regression surfaced reviewing PR #18 (which removed the L3 Partition node and routed per-group ranking onto Sort.partition_by).
Repro
topk(3, http_requests_total) by (job)
→ column resolution failed: column `job` not found in schema (have: ["sum"])
Root cause
In crates/lower/src/promql.rs, build's Outer::TopK else-branch (the non-heavy-hitter path) defaults a bare selector argument (inner.func == None) to an implicit cross-series AggFunc::Sum:
let func = match &inner.func {
Some(f) => inner_func(f),
None => AggFunc::Sum, // ← here
};
let base = windowed_aggregate(inner, vec![], func);
That reducing Sum collapses every label (including job) into a single sum column, so when the converter then resolves Sort.partition_by = [job] positionally against the child schema (lower.rs), job no longer exists → resolution error.
It is also semantically wrong independent of the by failure: PromQL topk(3, http_requests_total) ranks the raw instant-vector samples — it does not sum them.
Expected L3 shape
A bare selector should rank its own (label-preserving) samples:
topk(3, http_requests_total) by (job)
→ Limit { n: 3 } → Sort { keys: [value desc], partition_by: [job] } → Scan(http_requests_total)
This is exactly the shape PR #18's Sort.partition_by was designed for — no Partition node needed. Range-vector-function arguments (topk(3, rate(m[5m])) by (job), topk(3, avg_over_time(m[5m])) by (job)) already work because they produce a label-preserving per-series reduction.
Fix
In the Outer::TopK else-branch, when the argument is a bare instant selector, rank over the filtered_source directly (label-preserving) instead of wrapping it in a Sum. Only a range-vector-function argument gets the per-series reduction.
Tests
topk(3, http_requests_total) by (job) → Limit → Sort{value desc, partition_by:[job]} → Scan (job preserved).
topk(3, http_requests_total) (no by) → ranks raw samples, no implicit Sum.
- Regression:
topk(k, count_over_time(...)) stays heavy-hitter; bottomk(...) stays generic Sort+Limit.
Follow-up to PR #18.
Summary
topk(k, <bare instant selector>) by (labels)— e.g.topk(3, http_requests_total) by (job)("top-3 series perjob") — does not lower correctly. This is a regression surfaced reviewing PR #18 (which removed the L3Partitionnode and routed per-group ranking ontoSort.partition_by).Repro
Root cause
In
crates/lower/src/promql.rs,build'sOuter::TopKelse-branch (the non-heavy-hitter path) defaults a bare selector argument (inner.func == None) to an implicit cross-seriesAggFunc::Sum:That reducing
Sumcollapses every label (includingjob) into a singlesumcolumn, so when the converter then resolvesSort.partition_by = [job]positionally against the child schema (lower.rs),jobno longer exists → resolution error.It is also semantically wrong independent of the
byfailure: PromQLtopk(3, http_requests_total)ranks the raw instant-vector samples — it does not sum them.Expected L3 shape
A bare selector should rank its own (label-preserving) samples:
This is exactly the shape PR #18's
Sort.partition_bywas designed for — noPartitionnode needed. Range-vector-function arguments (topk(3, rate(m[5m])) by (job),topk(3, avg_over_time(m[5m])) by (job)) already work because they produce a label-preserving per-series reduction.Fix
In the
Outer::TopKelse-branch, when the argument is a bare instant selector, rank over thefiltered_sourcedirectly (label-preserving) instead of wrapping it in aSum. Only a range-vector-function argument gets the per-series reduction.Tests
topk(3, http_requests_total) by (job)→Limit → Sort{value desc, partition_by:[job]} → Scan(job preserved).topk(3, http_requests_total)(noby) → ranks raw samples, no implicitSum.topk(k, count_over_time(...))stays heavy-hitter;bottomk(...)stays generic Sort+Limit.Follow-up to PR #18.