Skip to content

Commit 0a50f72

Browse files
refactor(planner): dedup logic for extracting PromQL QueryRequirements (#430)
1 parent 2acac13 commit 0a50f72

3 files changed

Lines changed: 83 additions & 113 deletions

File tree

asap-common/dependencies/rs/asap_types/src/query_requirements.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
use promql_utilities::ast_matching::PromQLMatchResult;
12
use promql_utilities::data_model::KeyByLabelNames;
2-
use promql_utilities::query_logics::enums::Statistic;
3+
use promql_utilities::query_logics::enums::{QueryPatternType, Statistic};
4+
use promql_utilities::query_logics::parsing::{
5+
get_metric_and_spatial_filter, get_spatial_aggregation_output_labels, get_statistics_to_compute,
6+
};
7+
use tracing::warn;
8+
9+
use crate::promql_schema::PromQLSchema;
10+
use crate::utils::normalize_spatial_filter;
311

412
/// What a query needs in order to be answered by a stored aggregation.
513
#[derive(Debug, Clone)]
@@ -28,3 +36,60 @@ pub struct QueryRequirements {
2836
/// constrain the sketch weighting); matching ignores it when `None`.
2937
pub topk_count_events: Option<bool>,
3038
}
39+
40+
/// Build `QueryRequirements` from an already-pattern-matched PromQL query.
41+
///
42+
/// Shared by the query engine's capability-matching fallback and the planner's
43+
/// AQE extractor, which independently parse and pattern-match a query before
44+
/// calling this. `query` is used only for diagnostics on the unsupported-stats
45+
/// warning.
46+
pub fn build_query_requirements_promql(
47+
query: &str,
48+
match_result: &PromQLMatchResult,
49+
pattern_type: QueryPatternType,
50+
metric_schema: &PromQLSchema,
51+
) -> Option<QueryRequirements> {
52+
let (metric, spatial_filter) = get_metric_and_spatial_filter(match_result);
53+
54+
let statistics = get_statistics_to_compute(pattern_type, match_result)
55+
.map_err(|err| {
56+
warn!(
57+
query = %query,
58+
error = %err,
59+
"skipping matched query with unsupported statistics"
60+
);
61+
err
62+
})
63+
.ok()?;
64+
65+
let data_range_ms = match pattern_type {
66+
QueryPatternType::OnlySpatial => None,
67+
_ => match_result
68+
.get_range_duration()
69+
.map(|d| d.num_seconds() as u64 * 1000),
70+
};
71+
72+
let all_labels = metric_schema
73+
.get_labels(&metric)
74+
.cloned()
75+
.unwrap_or_else(KeyByLabelNames::empty);
76+
77+
let grouping_labels = match pattern_type {
78+
// OnlyTemporal preserves all labels.
79+
QueryPatternType::OnlyTemporal => all_labels,
80+
// OnlySpatial and OneTemporalOneSpatial encode their output labels in
81+
// the AST's `by (...)` / `without (...)` clause.
82+
QueryPatternType::OnlySpatial | QueryPatternType::OneTemporalOneSpatial => {
83+
get_spatial_aggregation_output_labels(match_result, &all_labels)
84+
}
85+
};
86+
87+
Some(QueryRequirements {
88+
metric,
89+
statistics,
90+
data_range_ms,
91+
grouping_labels,
92+
spatial_filter_normalized: normalize_spatial_filter(&spatial_filter),
93+
topk_count_events: None,
94+
})
95+
}

asap-planner-rs/src/optimizer/aqe_extractor.rs

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
use std::collections::HashMap;
22

3-
use asap_types::query_requirements::QueryRequirements;
4-
use asap_types::utils::normalize_spatial_filter;
3+
use asap_types::query_requirements::{build_query_requirements_promql, QueryRequirements};
54
use asap_types::PromQLSchema;
65
use promql_utilities::data_model::KeyByLabelNames;
7-
use promql_utilities::query_logics::enums::{QueryPatternType, Statistic};
8-
use promql_utilities::query_logics::parsing::{
9-
get_metric_and_spatial_filter, get_spatial_aggregation_output_labels, get_statistics_to_compute,
10-
};
6+
use promql_utilities::query_logics::enums::Statistic;
117
use tracing::warn;
128

139
use crate::planner::patterns::build_patterns;
@@ -160,12 +156,6 @@ fn decompose_to_leaves(query: &str) -> Vec<String> {
160156

161157
/// Try to extract `QueryRequirements` from a single leaf PromQL query string.
162158
/// Returns `None` if the query cannot be parsed or does not match any pattern.
163-
///
164-
/// TODO: this duplicates `build_query_requirements_promql` in
165-
/// `asap-query-engine/src/engines/simple_engine/promql.rs:614`. That function
166-
/// is a private `&self` method tied to `SimplePromQLEngine`. The shared logic
167-
/// should be extracted into a free function in `asap_types::query_requirements`
168-
/// and called from both sites.
169159
fn extract_requirements(query: &str, metric_schema: &PromQLSchema) -> Option<QueryRequirements> {
170160
let ast = promql_parser::parser::parse(query).ok()?;
171161
let patterns = build_patterns();
@@ -179,52 +169,7 @@ fn extract_requirements(query: &str, metric_schema: &PromQLSchema) -> Option<Que
179169
}
180170
})?;
181171

182-
let (metric, spatial_filter) = get_metric_and_spatial_filter(&match_result);
183-
let statistics = get_statistics_to_compute(pattern_type, &match_result)
184-
.map_err(|err| {
185-
warn!(
186-
query = %query,
187-
error = %err,
188-
"aqe_extractor: skipping matched leaf query with unsupported statistics"
189-
);
190-
err
191-
})
192-
.ok()?;
193-
194-
let data_range_ms = match pattern_type {
195-
QueryPatternType::OnlySpatial => None,
196-
_ => match_result
197-
.get_range_duration()
198-
.map(|d| d.num_seconds() as u64 * 1000),
199-
};
200-
201-
let grouping_labels = match pattern_type {
202-
// OnlyTemporal preserves all labels — look them up in the schema.
203-
// If the metric is unknown, fall back to empty (dedup still works; cost
204-
// model will treat it as a zero-group-count sketch).
205-
QueryPatternType::OnlyTemporal => metric_schema
206-
.get_labels(&metric)
207-
.cloned()
208-
.unwrap_or_else(KeyByLabelNames::empty),
209-
// OnlySpatial and OneTemporalOneSpatial encode their output labels in
210-
// the AST's `by (...)` / `without (...)` clause.
211-
QueryPatternType::OnlySpatial | QueryPatternType::OneTemporalOneSpatial => {
212-
let all_labels = metric_schema
213-
.get_labels(&metric)
214-
.cloned()
215-
.unwrap_or_else(KeyByLabelNames::empty);
216-
get_spatial_aggregation_output_labels(&match_result, &all_labels)
217-
}
218-
};
219-
220-
Some(QueryRequirements {
221-
metric,
222-
statistics,
223-
data_range_ms,
224-
grouping_labels,
225-
spatial_filter_normalized: normalize_spatial_filter(&spatial_filter),
226-
topk_count_events: None,
227-
})
172+
build_query_requirements_promql(query, &match_result, pattern_type, metric_schema)
228173
}
229174

230175
#[cfg(test)]

asap-query-engine/src/engines/simple_engine/promql.rs

Lines changed: 14 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use super::{
1010
};
1111
use crate::data_model::{AggregationIdInfo, KeyByLabelValues, QueryConfig, SchemaConfig};
1212
use crate::engines::query_result::{QueryResult, RangeVectorElement};
13-
use asap_types::query_requirements::QueryRequirements;
14-
use asap_types::utils::normalize_spatial_filter;
13+
use asap_types::query_requirements::build_query_requirements_promql;
14+
use asap_types::PromQLSchema;
1515
use promql_utilities::ast_matching::PromQLMatchResult;
1616
use promql_utilities::data_model::KeyByLabelNames;
1717
use promql_utilities::get_is_collapsable;
@@ -614,56 +614,6 @@ impl SimpleEngine {
614614
Some((output_labels, QueryResult::matrix(combined)))
615615
}
616616

617-
/// Extract QueryRequirements from a parsed PromQL match result.
618-
/// Used as the fallback path when no query_configs entry is found.
619-
fn build_query_requirements_promql(
620-
&self,
621-
match_result: &PromQLMatchResult,
622-
query_pattern_type: QueryPatternType,
623-
) -> Option<QueryRequirements> {
624-
let (metric, spatial_filter) = get_metric_and_spatial_filter(match_result);
625-
626-
let statistics = get_statistics_to_compute(query_pattern_type, match_result)
627-
.map_err(|err| {
628-
warn!("{}", err);
629-
err
630-
})
631-
.ok()?;
632-
633-
let data_range_ms = match query_pattern_type {
634-
QueryPatternType::OnlySpatial => None,
635-
_ => match_result
636-
.get_range_duration()
637-
.map(|d| d.num_seconds() as u64 * 1000),
638-
};
639-
640-
let all_labels = match &self.inference_config.read().unwrap().schema {
641-
SchemaConfig::PromQL(schema) => schema
642-
.get_labels(&metric)
643-
.cloned()
644-
.unwrap_or_else(KeyByLabelNames::empty),
645-
_ => KeyByLabelNames::empty(),
646-
};
647-
648-
let grouping_labels = match query_pattern_type {
649-
QueryPatternType::OnlyTemporal => all_labels,
650-
QueryPatternType::OnlySpatial | QueryPatternType::OneTemporalOneSpatial => {
651-
get_spatial_aggregation_output_labels(match_result, &all_labels)
652-
}
653-
};
654-
655-
Some(QueryRequirements {
656-
metric,
657-
statistics,
658-
data_range_ms,
659-
grouping_labels,
660-
spatial_filter_normalized: normalize_spatial_filter(&spatial_filter),
661-
// PromQL top-k does not constrain the sketch weighting; leave the
662-
// count/sum discriminator unset so matching does not over-filter.
663-
topk_count_events: None,
664-
})
665-
}
666-
667617
// /// Try to extract sketch query components from a PromQL query string.
668618
// ///
669619
// /// Attempts the standard AST parser first. If that fails (e.g. for custom
@@ -1085,8 +1035,18 @@ impl SimpleEngine {
10851035
"No query_config entry for PromQL query '{}'. Attempting capability-based matching.",
10861036
query
10871037
);
1088-
let requirements =
1089-
self.build_query_requirements_promql(&match_result, query_pattern_type)?;
1038+
let inference_config = self.inference_config.read().unwrap();
1039+
let empty_schema = PromQLSchema::new();
1040+
let metric_schema = match &inference_config.schema {
1041+
SchemaConfig::PromQL(schema) => schema,
1042+
_ => &empty_schema,
1043+
};
1044+
let requirements = build_query_requirements_promql(
1045+
&query,
1046+
&match_result,
1047+
query_pattern_type,
1048+
metric_schema,
1049+
)?;
10901050
self.streaming_config
10911051
.read()
10921052
.unwrap()

0 commit comments

Comments
 (0)