diff --git a/TODO.md b/TODO.md index 066efe414..01379747e 100644 --- a/TODO.md +++ b/TODO.md @@ -86,6 +86,15 @@ stopped polluting the per-data-point attribute set with the encoding string (which had broken the per-series snapshot cache key). +- **Inference-YAML pattern coverage.** Expanded + `asap-query-engine/examples/promql/inference_config.yaml` (and the + SQL twin) with multi-quantile / wider-range / rate / increase / + topk entries; closes + [ASAPCollector PROGRESS.md follow-up #4](https://github.com/ProjectASAP/ASAPCollector/blob/main/PROGRESS.md#open-follow-ups-not-e2e-blockers) + ("Inference config breadth"). New `tests/inference_yaml_pattern_coverage.rs` + pins each family's YAML → `find_query_config` → `query_statistic` + routing. + ## All-five-sketch query path verification (2026-04-30) Each sketch type now has a runtime-verified PromQL → backend path diff --git a/asap-query-engine/examples/promql/inference_config.yaml b/asap-query-engine/examples/promql/inference_config.yaml index 4aedd0137..24fd878b6 100644 --- a/asap-query-engine/examples/promql/inference_config.yaml +++ b/asap-query-engine/examples/promql/inference_config.yaml @@ -6,8 +6,163 @@ metrics: - label_1 cleanup_policy: name: read_based +# Each (query, aggregation_id) pair binds a PromQL pattern to a precompute +# plan. The exact-string match in `find_query_config` requires the request +# query to canonicalize to one of the listed strings — wider-range / wider- +# shape variants (e.g. `[2m]`, `[5m]`, `rate(...)`, multi-quantile) need +# their own entries here, otherwise the request falls through to capability +# matching and (failing that) the cold tier. See +# `tests/inference_yaml_pattern_coverage.rs` for the runtime contract. queries: +# ── Spatial quantile (existing canonical pattern + multi-quantile) ───── +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile by (label_0) (0.5, fake_metric) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile by (label_0) (0.9, fake_metric) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile by (label_0) (0.95, fake_metric) - aggregations: - aggregation_id: 1 read_count_threshold: 1 query: quantile by (label_0) (0.99, fake_metric) +# ── quantile_over_time: multi-quantile × wider ranges ────────────────── +# Routes to `Statistic::Quantile`; supported by DDSketch / KLL accumulators. +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.5, fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.9, fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.95, fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.99, fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.5, fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.9, fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.95, fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.99, fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.5, fake_metric[5m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.9, fake_metric[5m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.95, fake_metric[5m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: quantile_over_time(0.99, fake_metric[5m]) +# ── sum_over_time / count_over_time: wider ranges ────────────────────── +# Routes to `Statistic::Sum` / `Statistic::Count`; supported by DDSketch / +# CountSketch / CountMinSketch accumulators (no-key total-volume fallback). +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: sum_over_time(fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: sum_over_time(fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: sum_over_time(fake_metric[5m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: count_over_time(fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: count_over_time(fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: count_over_time(fake_metric[5m]) +# ── rate / increase: delta-capable sketches ──────────────────────────── +# Routes to `Statistic::Rate` / `Statistic::Increase`; supported by +# IncreaseAccumulator / MultipleIncreaseAccumulator. KLL has no delta and +# will return an error for these — pair this YAML with an Increase-typed +# streaming aggregation when serving rate/increase queries. +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: rate(fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: rate(fake_metric[2m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: rate(fake_metric[5m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: increase(fake_metric[1m]) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: increase(fake_metric[5m]) +# ── Cardinality / generic spatial aggregations ───────────────────────── +# `count` over an HLL-backed aggregation routes to `Statistic::Count`, +# which the HLL accumulator answers as a unique-cardinality estimate. +# `sum` / `avg` route to `Statistic::Sum` / `(Sum, Count)`. +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: count(fake_metric) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: sum(fake_metric) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: avg(fake_metric) +# ── Top-K (CountSketch heavy-hitter readout) ─────────────────────────── +# `topk(N, …)` routes to `Statistic::Topk`; CountSketch's `query_statistic` +# returns the row-mean total when no key is supplied (limitation: per-key +# top-K enumeration needs a paired SetAggregator on the agent — tracked +# upstream). +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: topk(5, fake_metric) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: topk(10, fake_metric) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: topk(50, fake_metric) diff --git a/asap-query-engine/examples/sql/inference_config.yaml b/asap-query-engine/examples/sql/inference_config.yaml index 378c6f6bf..a57e30904 100644 --- a/asap-query-engine/examples/sql/inference_config.yaml +++ b/asap-query-engine/examples/sql/inference_config.yaml @@ -5,7 +5,37 @@ tables: value_columns: [cpu_usage, memory_usage] cleanup_policy: name: read_based +# Each entry binds a SQL template (NOW()-based, structurally matched) to a +# precompute plan. SQL pattern matching is structural — incoming queries +# with absolute timestamps still match the NOW()-based template as long as +# aggregation, columns, GROUP BY, and the window duration agree. See +# `tests/inference_yaml_pattern_coverage.rs` for the runtime contract. queries: +# ── Quantile family (cpu_usage, multi-quantile, single window) ───────── +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, quantile(0.5)(cpu_usage) as p50 + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -11, NOW()) AND DATEADD(s, -10, NOW()) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, quantile(0.9)(cpu_usage) as p90 + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -11, NOW()) AND DATEADD(s, -10, NOW()) +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, quantile(0.95)(cpu_usage) as p95 + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -11, NOW()) AND DATEADD(s, -10, NOW()) - aggregations: - aggregation_id: 1 read_count_threshold: 1 @@ -14,3 +44,36 @@ queries: FROM metrics_table GROUP BY datacenter WHERE time BETWEEN DATEADD(s, -11, NOW()) AND DATEADD(s, -10, NOW()) +# ── Sum / Count / Avg over wider windows (cpu_usage) ─────────────────── +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, SUM(cpu_usage) as total + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -60, NOW()) AND NOW() +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, SUM(cpu_usage) as total + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, COUNT(cpu_usage) as samples + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -60, NOW()) AND NOW() +- aggregations: + - aggregation_id: 1 + read_count_threshold: 1 + query: | + SELECT datacenter, AVG(cpu_usage) as mean + FROM metrics_table + GROUP BY datacenter + WHERE time BETWEEN DATEADD(s, -60, NOW()) AND NOW() diff --git a/asap-query-engine/tests/inference_yaml_pattern_coverage.rs b/asap-query-engine/tests/inference_yaml_pattern_coverage.rs new file mode 100644 index 000000000..ee233948e --- /dev/null +++ b/asap-query-engine/tests/inference_yaml_pattern_coverage.rs @@ -0,0 +1,496 @@ +//! Inference-YAML PromQL pattern coverage. +//! +//! Verifies the warm-tier query coverage advertised in +//! `asap-query-engine/examples/promql/inference_config.yaml`: +//! +//! 1. The YAML parses and exposes every pattern family the engine claims +//! to serve (multi-quantile, wider ranges, rate/increase, sum/count +//! over time, spatial aggregations, top-K). +//! 2. For each family, an end-to-end PromQL query routes through +//! `find_query_config` → `parse_and_match_promql` → `query_statistic` +//! against a backing accumulator that supports the resolved +//! `Statistic`, and returns a non-empty result vector. +//! +//! This is the runtime contract referenced by the YAML's leading comment: +//! adding a new entry without a corresponding routing test risks shipping +//! warm-tier "promises" the engine can't keep. + +use std::collections::HashMap; +use std::sync::Arc; + +use promql_utilities::data_model::KeyByLabelNames; + +#[allow(dead_code)] +fn init_test_tracing() { + let _ = tracing_subscriber::fmt::try_init(); +} + +use query_engine_rust::data_model::{ + AggregationConfig, AggregationReference, AggregationType, CleanupPolicy, InferenceConfig, + KeyByLabelValues, PrecomputedOutput, PromQLSchema, QueryConfig, QueryLanguage, SchemaConfig, + StreamingConfig, WindowType, +}; +use query_engine_rust::engines::SimpleEngine; +use query_engine_rust::precompute_operators::{ + DDSketchAccumulator, DatasketchesKLLAccumulator, IncreaseAccumulator, SumAccumulator, +}; +use query_engine_rust::stores::SimpleMapStore; +use query_engine_rust::stores::Store; +use query_engine_rust::utils::file_io::read_inference_config; +use query_engine_rust::AggregateCore; + +const PROMQL_YAML: &str = "examples/promql/inference_config.yaml"; + +// ─── 1. YAML parses and covers every pattern family ──────────────────── + +#[test] +fn promql_inference_yaml_loads_all_pattern_families() { + let cfg = read_inference_config(PROMQL_YAML, QueryLanguage::promql) + .expect("inference_config.yaml must parse"); + + let queries: Vec<&str> = cfg + .query_configs + .iter() + .map(|q| q.query.as_str()) + .collect(); + + // Sanity: expansion landed (pre-PR baseline was 1 entry). + assert!( + queries.len() >= 20, + "expected substantial pattern expansion; got {} entries", + queries.len() + ); + + // Every PromQL string parses through promql_parser — i.e. no typos + // would silently fail-to-match against an incoming canonical-AST. + for q in &queries { + promql_parser::parser::parse(q) + .unwrap_or_else(|e| panic!("query `{q}` failed to parse: {e}")); + } + + // Per-family presence checks. Each family must contribute at least + // one wider-range / wider-shape variant beyond the [1m] / 0.5 + // baseline that PROGRESS.md flagged as the only landed shape. + let has = |needle: &str| queries.iter().any(|q| q.contains(needle)); + + // Multi-quantile (quantile_over_time) + assert!(has("quantile_over_time(0.9, fake_metric[1m])")); + assert!(has("quantile_over_time(0.95, fake_metric[1m])")); + assert!(has("quantile_over_time(0.99, fake_metric[1m])")); + // Wider ranges + assert!(has("quantile_over_time(0.5, fake_metric[2m])")); + assert!(has("quantile_over_time(0.5, fake_metric[5m])")); + // Rate / increase + assert!(has("rate(fake_metric[1m])")); + assert!(has("rate(fake_metric[5m])")); + assert!(has("increase(fake_metric[1m])")); + // Sum / count over wider ranges + assert!(has("sum_over_time(fake_metric[2m])")); + assert!(has("sum_over_time(fake_metric[5m])")); + assert!(has("count_over_time(fake_metric[1m])")); + // Spatial aggregations + assert!(has("count(fake_metric)")); + assert!(has("sum(fake_metric)")); + assert!(has("avg(fake_metric)")); + // Top-K + assert!(has("topk(5, fake_metric)")); + assert!(has("topk(10, fake_metric)")); + assert!(has("topk(50, fake_metric)")); + // Multi-quantile spatial + assert!(has("quantile by (label_0) (0.5, fake_metric)")); + assert!(has("quantile by (label_0) (0.99, fake_metric)")); +} + +// ─── 2. SQL YAML expansion parity ────────────────────────────────────── + +#[test] +fn sql_inference_yaml_loads_expanded_patterns() { + let cfg = read_inference_config("examples/sql/inference_config.yaml", QueryLanguage::sql) + .expect("sql inference_config.yaml must parse"); + + let queries: Vec<&str> = cfg + .query_configs + .iter() + .map(|q| q.query.as_str()) + .collect(); + + assert!( + queries.len() >= 6, + "SQL YAML did not expand: {} entries", + queries.len() + ); + + let has = |needle: &str| queries.iter().any(|q| q.contains(needle)); + assert!(has("quantile(0.5)(cpu_usage)")); + assert!(has("quantile(0.9)(cpu_usage)")); + assert!(has("quantile(0.95)(cpu_usage)")); + assert!(has("quantile(0.99)(cpu_usage)")); + assert!(has("SUM(cpu_usage)")); + assert!(has("COUNT(cpu_usage)")); + assert!(has("AVG(cpu_usage)")); +} + +// ─── 3. Per-family runtime routing tests ─────────────────────────────── +// +// Each test below exercises the full warm-tier path for one pattern +// family: the engine finds the YAML entry exactly, pattern-matches +// the request to a `Statistic`, and the chosen accumulator's +// `query_statistic` returns a finite scalar — which the engine then +// folds into a `QueryResult::Vector`. +// +// We keep the engine fixtures inline rather than reusing +// `crate::tests::test_utilities::engine_factories` because that module +// is `#[cfg(test)]`-only and not visible to integration tests in +// `tests/`. The fixture is small enough that this is fine. + +/// Single-population single-aggregation engine fixture. `agg_type` and +/// `acc` must agree (e.g. `DDSketch` + `DDSketchAccumulator`). +/// +/// `grouping_labels` MUST be non-empty for any non-spatial test — +/// `format_final_results` drops result rows whose key is `None`, so a +/// `None`-keyed insert produces a "result Some, vector empty" outcome +/// that's indistinguishable from a real warm-tier miss. +fn build_engine( + metric: &str, + schema_labels: &[&str], + agg_type: AggregationType, + grouping_labels: &[&str], + window_size: u64, + acc: Box, + promql_query: &str, +) -> SimpleEngine { + let schema_label_strs: Vec = schema_labels.iter().map(|s| s.to_string()).collect(); + let grouping_label_strs: Vec = grouping_labels.iter().map(|s| s.to_string()).collect(); + + let mut aggregation_configs = HashMap::new(); + aggregation_configs.insert( + 1u64, + AggregationConfig { + aggregation_id: 1, + aggregation_type: agg_type, + aggregation_sub_type: String::new(), + parameters: HashMap::new(), + grouping_labels: KeyByLabelNames::new(grouping_label_strs.clone()), + aggregated_labels: KeyByLabelNames::empty(), + rollup_labels: KeyByLabelNames::empty(), + original_yaml: String::new(), + window_size, + slide_interval: window_size, + window_type: WindowType::Tumbling, + spatial_filter: String::new(), + spatial_filter_normalized: String::new(), + metric: metric.to_string(), + num_aggregates_to_retain: None, + read_count_threshold: None, + table_name: None, + value_column: None, + }, + ); + let streaming_config = Arc::new(StreamingConfig { + aggregation_configs, + }); + let store = Arc::new(SimpleMapStore::new( + streaming_config.clone(), + CleanupPolicy::NoCleanup, + )); + + // Insert one window's worth of precomputed data covering the grid + // around `query_time = 1_000_000` ms used by the assertions below. + // Pre-populate two windows so wider ranges (e.g. [5m]) still find + // a covering pane via the closest-pane store query. + let window_ms = window_size * 1000; + for i in 0..2 { + let end_ts = 1_000_000_u64 - i * window_ms; + let start_ts = end_ts.saturating_sub(window_ms); + // Synthesize one label value per grouping label so the result + // row has a non-`None` key (see fn-level comment). + let key_labels: Vec = grouping_label_strs + .iter() + .enumerate() + .map(|(idx, _)| format!("v{idx}")) + .collect(); + let key = Some(KeyByLabelValues { labels: key_labels }); + let output = PrecomputedOutput::new(start_ts, end_ts, key, 1); + store + .insert_precomputed_output(output, acc.clone_boxed_core()) + .unwrap(); + } + + let promql_schema = + PromQLSchema::new().add_metric(metric.to_string(), KeyByLabelNames::new(schema_label_strs)); + let inference_config = InferenceConfig { + schema: SchemaConfig::PromQL(promql_schema), + query_configs: vec![QueryConfig::new(promql_query.to_string()) + .add_aggregation(AggregationReference::new(1, None))], + cleanup_policy: CleanupPolicy::NoCleanup, + }; + + SimpleEngine::new( + store, + inference_config, + streaming_config, + 1, + QueryLanguage::promql, + ) +} + +const QUERY_TIME_SEC: f64 = 1000.0; + +fn make_dd_acc(values: &[f64]) -> Box { + let mut acc = DDSketchAccumulator::new(0.01); + for &v in values { + acc.inner.update(v); + } + Box::new(acc) +} + +fn make_kll_acc(values: &[f64]) -> Box { + let mut acc = DatasketchesKLLAccumulator::new(200); + for &v in values { + acc.inner.update(v); + } + Box::new(acc) +} + +fn make_sum_acc(total: f64) -> Box { + Box::new(SumAccumulator::with_sum(total)) +} + +#[test] +fn quantile_over_time_multi_phi_routes_through_warm_tier() { + init_test_tracing(); + // KLL backs the canonical warm-tier quantile path. Same metric + // schema as the YAML so `find_query_config` exact-matches. + let acc = make_kll_acc(&[10.0, 20.0, 30.0, 40.0, 50.0]); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::DatasketchesKLL, + &["instance"], + 60, // 1m window + acc, + "quantile_over_time(0.5, fake_metric[1m])", + ); + + let result = engine + .handle_query_promql( + "quantile_over_time(0.5, fake_metric[1m])".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer p50 quantile_over_time"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "expected non-empty p50 result"); + let p50 = elements[0].value; + assert!( + p50.is_finite() && (5.0..=55.0).contains(&p50), + "p50 out of plausible range for [10..50]: {p50}" + ); +} + +#[test] +fn quantile_over_time_wider_range_routes_through_warm_tier() { + // [5m] entry must match — pre-PR this fell to capability matching. + let acc = make_dd_acc(&[1.0, 2.0, 3.0, 4.0, 5.0]); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::DDSketch, + &["instance"], + 300, // 5m window + acc, + "quantile_over_time(0.99, fake_metric[5m])", + ); + + let result = engine + .handle_query_promql( + "quantile_over_time(0.99, fake_metric[5m])".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer [5m] quantile_over_time"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "expected non-empty [5m] p99 result"); +} + +#[test] +fn rate_routes_to_increase_accumulator_warm_tier() { + // Rate / Increase requires an Increase-typed aggregation; KLL has no + // delta, so the YAML's `rate(fake_metric[…])` entries are paired + // with this accumulator type at runtime. We exercise that pairing. + let acc = IncreaseAccumulator::new( + query_engine_rust::Measurement::new(0.0), + 0, + query_engine_rust::Measurement::new(100.0), + 60_000, + ); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::Increase, + &["instance"], + 60, + Box::new(acc), + "rate(fake_metric[1m])", + ); + + let result = engine + .handle_query_promql( + "rate(fake_metric[1m])".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer rate(...[1m])"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "rate result should not be empty"); +} + +#[test] +fn increase_routes_to_increase_accumulator_warm_tier() { + let acc = IncreaseAccumulator::new( + query_engine_rust::Measurement::new(5.0), + 0, + query_engine_rust::Measurement::new(25.0), + 60_000, + ); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::Increase, + &["instance"], + 60, + Box::new(acc), + "increase(fake_metric[1m])", + ); + + let result = engine + .handle_query_promql( + "increase(fake_metric[1m])".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer increase(...[1m])"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "increase result should not be empty"); +} + +#[test] +fn sum_over_time_wider_range_routes_through_warm_tier() { + // SumAccumulator answers Statistic::Sum directly. [2m] entry was + // previously absent and would have fallen to capability matching. + let acc = make_sum_acc(420.0); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::Sum, + &["instance"], + 120, // 2m window + acc, + "sum_over_time(fake_metric[2m])", + ); + + let result = engine + .handle_query_promql( + "sum_over_time(fake_metric[2m])".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer sum_over_time(...[2m])"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "sum_over_time result should not be empty"); +} + +#[test] +fn count_over_time_routes_through_warm_tier() { + let acc = make_sum_acc(7.0); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::Sum, + &["instance"], + 60, + acc, + "count_over_time(fake_metric[1m])", + ); + + let result = engine + .handle_query_promql( + "count_over_time(fake_metric[1m])".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer count_over_time(...[1m])"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "count_over_time result should not be empty"); +} + +#[test] +fn spatial_sum_routes_through_warm_tier() { + let acc = make_sum_acc(100.0); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::Sum, + &["label_0"], + 1, + acc, + "sum(fake_metric)", + ); + + let result = engine + .handle_query_promql("sum(fake_metric)".to_string(), QUERY_TIME_SEC) + .expect("warm tier should answer sum(metric)"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "sum() result should not be empty"); +} + +#[test] +fn spatial_multi_quantile_routes_through_warm_tier() { + // p50 spatial — pre-PR only p99 had an entry, so this would + // previously have fallen to capability matching. + let acc = make_kll_acc(&[10.0, 20.0, 30.0, 40.0, 50.0]); + let engine = build_engine( + "fake_metric", + &["instance", "job", "label_0", "label_1"], + AggregationType::DatasketchesKLL, + &["label_0"], + 1, + acc, + "quantile by (label_0) (0.5, fake_metric)", + ); + + let result = engine + .handle_query_promql( + "quantile by (label_0) (0.5, fake_metric)".to_string(), + QUERY_TIME_SEC, + ) + .expect("warm tier should answer quantile by(...) (0.5, ...)"); + let (_, qr) = result; + let elements = match qr { + query_engine_rust::engines::QueryResult::Vector(iv) => iv.values, + other => panic!("expected vector, got {other:?}"), + }; + assert!(!elements.is_empty(), "spatial p50 result should not be empty"); +}