Skip to content

Commit c33afe2

Browse files
zz_yclaude
andcommitted
test(lower): composition of counter-derivatives with outer/nested funcs (#44)
Pin how the new counter-derivative intents compose with the rest of the lowering — they are per-series/label-preserving, so they reuse the general nesting path with no special-casing: - outer cross-series stat over a derivative nests two levels and threads the outer group key + any scalar param (`avg by (dc)(predict_linear(...))`) - `topk(k, deriv(...))` is generic Sort+Limit, not a heavy-hitter TopK - as a binary-op operand and under `sum(rate(...) + changes(...))` - counter-derivative over a sub-query stays cleanly rejected (a #42 follow-up) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 29b107a commit c33afe2

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

crates/lower/tests/promql_conformance.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,3 +812,77 @@ fn aggregation_over_counter_derivative_keeps_labels() {
812812
assert!(intents(&qe).iter().any(|i| matches!(i, AggIntent::Changes)));
813813
let _ = child;
814814
}
815+
816+
#[test]
817+
fn outer_stat_over_counter_derivative_nests_two_levels() {
818+
// A cross-series stat over a counter-derivative is a genuine two-level
819+
// reduction: the derivative runs per series (inner), the stat aggregates
820+
// across series (outer). They must not collapse into one node — and a
821+
// grouped outer (`avg by (dc)`) must resolve its key against the labels the
822+
// inner reduction preserved, threading any scalar param (predict horizon).
823+
let qe = ok("avg by (dc) (predict_linear(m[3h], 3600))");
824+
let QueryExpr::Aggregate { by, aggs, child, .. } = &qe else {
825+
panic!("expected outer Aggregate, got {qe:?}");
826+
};
827+
assert!(!by.is_empty(), "outer `avg by (dc)` groups on a label");
828+
assert!(matches!(aggs.as_slice(), [AggIntent::Avg { .. }]));
829+
let QueryExpr::Aggregate { by: inner_by, aggs: inner_aggs, .. } = child.as_ref() else {
830+
panic!("expected inner per-series Aggregate, got {child:?}");
831+
};
832+
assert!(inner_by.is_empty(), "inner derivative stays per-series");
833+
assert_eq!(
834+
inner_aggs.as_slice(),
835+
std::slice::from_ref(&AggIntent::PredictLinear { seconds: 3600.0 })
836+
);
837+
}
838+
839+
#[test]
840+
fn topk_over_counter_derivative_is_generic_sort_limit() {
841+
// `topk(k, deriv(...))` ranks the per-series derivative values — a generic
842+
// `Sort + Limit`, NOT a heavy-hitter `TopK` (that's only `count_over_time`).
843+
let qe = ok("topk(3, deriv(m[5m]))");
844+
let QueryExpr::Limit { n, child, .. } = &qe else {
845+
panic!("expected Limit, got {qe:?}");
846+
};
847+
assert_eq!(*n, 3);
848+
assert!(matches!(child.as_ref(), QueryExpr::Sort { .. }));
849+
assert!(intents(&qe).iter().any(|i| matches!(i, AggIntent::Deriv)));
850+
assert!(
851+
!intents(&qe).iter().any(|i| matches!(i, AggIntent::TopK { .. })),
852+
"counter-derivative topk is generic ranking, not a heavy-hitter sketch"
853+
);
854+
}
855+
856+
#[test]
857+
fn counter_derivative_composes_in_binary_ops() {
858+
// As a vector operand: `delta(a[5m]) / delta(b[5m])` is a BinaryOp of two
859+
// per-series Delta reductions.
860+
let ratio = ok("delta(a[5m]) / delta(b[5m])");
861+
let QueryExpr::BinaryOp { op, lhs, rhs, .. } = &ratio else {
862+
panic!("expected BinaryOp, got {ratio:?}");
863+
};
864+
assert_eq!(*op, BinaryOpKind::Arith(ArithOp::Div));
865+
assert!(matches!(lhs.as_ref(), QueryExpr::Aggregate { aggs, .. } if aggs.as_slice() == [AggIntent::Delta]));
866+
assert!(matches!(rhs.as_ref(), QueryExpr::Aggregate { aggs, .. } if aggs.as_slice() == [AggIntent::Delta]));
867+
868+
// Under an aggregate over a binary op mixing a counter-derivative with
869+
// another per-series function: `sum(rate(m[5m]) + changes(m[5m]))`.
870+
let mixed = ok("sum(rate(m[5m]) + changes(m[5m]))");
871+
let QueryExpr::Aggregate { aggs, child, .. } = &mixed else {
872+
panic!("expected Aggregate, got {mixed:?}");
873+
};
874+
assert!(matches!(aggs.as_slice(), [AggIntent::Sum { .. }]));
875+
assert!(matches!(child.as_ref(), QueryExpr::BinaryOp { .. }));
876+
assert!(intents(&mixed).iter().any(|i| matches!(i, AggIntent::Rate)));
877+
assert!(intents(&mixed).iter().any(|i| matches!(i, AggIntent::Changes)));
878+
}
879+
880+
#[test]
881+
fn counter_derivative_over_a_subquery_is_rejected__GAP() {
882+
// Unlike `*_over_time` (issue #42), the counter-derivative functions do not
883+
// yet accept a sub-query argument — only a bare matrix selector. This is
884+
// valid PromQL and rejects cleanly (never mislowered); wiring them into the
885+
// sub-query path is a follow-up to #42/#44.
886+
let _ = rejected("changes(rate(m[5m])[1h:])");
887+
let _ = rejected("delta(sum(m)[5m:])");
888+
}

0 commit comments

Comments
 (0)