Skip to content

Commit 74ca25d

Browse files
refactor(planner, query-engine): remove QueryPatternType for PromQL path (#518)
* Restricted OneTemporalOneSpatial patterns to only collapsable ones * made get_statistics_to_compute independent of QueryPatternType * made build_query_requirements_promql independent of QueryPatternType * Removed remaining QueryPatternType from asap-query-engine * Removed QueryPatternType from asap-planner * removed enum * added check * fix * fix clippy
1 parent a420865 commit 74ca25d

19 files changed

Lines changed: 688 additions & 566 deletions

File tree

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use promql_utilities::ast_matching::PromQLMatchResult;
22
use promql_utilities::data_model::KeyByLabelNames;
3-
use promql_utilities::query_logics::enums::{QueryPatternType, Statistic};
3+
use promql_utilities::query_logics::enums::Statistic;
44
use promql_utilities::query_logics::parsing::{
55
get_metric_and_spatial_filter, get_spatial_aggregation_output_labels, get_statistics_to_compute,
66
};
@@ -46,13 +46,12 @@ pub struct QueryRequirements {
4646
pub fn build_query_requirements_promql(
4747
query: &str,
4848
match_result: &PromQLMatchResult,
49-
pattern_type: QueryPatternType,
5049
metric_schema: &PromQLSchema,
5150
data_ingestion_interval_ms: u64,
5251
) -> Option<QueryRequirements> {
5352
let (metric, spatial_filter) = get_metric_and_spatial_filter(match_result);
5453

55-
let statistics = get_statistics_to_compute(pattern_type, match_result)
54+
let statistics = get_statistics_to_compute(match_result)
5655
.map_err(|err| {
5756
warn!(
5857
query = %query,
@@ -63,28 +62,33 @@ pub fn build_query_requirements_promql(
6362
})
6463
.ok()?;
6564

66-
let data_range_ms = match pattern_type {
67-
QueryPatternType::OnlySpatial => data_ingestion_interval_ms,
65+
let has_temporal_function = match_result.tokens.contains_key("function");
66+
let has_aggregation = match_result.tokens.contains_key("aggregation");
67+
68+
let data_range_ms = if has_temporal_function {
6869
// promql-parser supports a literal `ms` duration suffix (e.g. `[500ms]`),
6970
// so .num_seconds() would truncate sub-second ranges to 0.
70-
_ => match_result
71+
match_result
7172
.get_range_duration()
72-
.map(|d| d.num_milliseconds() as u64)?,
73+
.map(|d| d.num_milliseconds() as u64)?
74+
} else {
75+
// OnlySpatial (no temporal component): the query has no range of its
76+
// own, so its data range is exactly one scrape interval.
77+
data_ingestion_interval_ms
7378
};
7479

7580
let all_labels = metric_schema
7681
.get_labels(&metric)
7782
.cloned()
7883
.unwrap_or_else(KeyByLabelNames::empty);
7984

80-
let grouping_labels = match pattern_type {
85+
let grouping_labels = if has_aggregation {
86+
// OnlySpatial and (collapsable, see #508) OneTemporalOneSpatial encode
87+
// their output labels in the AST's `by (...)` / `without (...)` clause.
88+
get_spatial_aggregation_output_labels(match_result, &all_labels)
89+
} else {
8190
// OnlyTemporal preserves all labels.
82-
QueryPatternType::OnlyTemporal => all_labels,
83-
// OnlySpatial and OneTemporalOneSpatial encode their output labels in
84-
// the AST's `by (...)` / `without (...)` clause.
85-
QueryPatternType::OnlySpatial | QueryPatternType::OneTemporalOneSpatial => {
86-
get_spatial_aggregation_output_labels(match_result, &all_labels)
87-
}
91+
all_labels
8892
};
8993

9094
Some(QueryRequirements {

asap-common/dependencies/rs/promql_utilities/src/query_logics/enums.rs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,6 @@ use std::fmt;
33
use std::str::FromStr;
44
use tracing::debug;
55

6-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7-
pub enum QueryPatternType {
8-
OnlyTemporal,
9-
OnlySpatial,
10-
OneTemporalOneSpatial,
11-
}
12-
13-
impl std::fmt::Display for QueryPatternType {
14-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15-
debug!("Formatting QueryPatternType: {:?}", self);
16-
match self {
17-
QueryPatternType::OnlyTemporal => write!(f, "only_temporal"),
18-
QueryPatternType::OnlySpatial => write!(f, "only_spatial"),
19-
QueryPatternType::OneTemporalOneSpatial => write!(f, "one_temporal_one_spatial"),
20-
}
21-
}
22-
}
23-
246
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
257
pub enum QueryTreatmentType {
268
Exact,
@@ -69,6 +51,22 @@ impl std::fmt::Display for Statistic {
6951

7052
#[allow(clippy::should_implement_trait)]
7153
impl Statistic {
54+
/// Returns `true` for statistics whose result requires approximate
55+
/// pre-aggregation, regardless of whether they were reached via a
56+
/// temporal function (`PromQLFunction::is_approximate`) or a spatial
57+
/// aggregation operator (`AggregationOperator::is_approximate`) — for
58+
/// every `Statistic` reachable from either origin, the two origins agree.
59+
pub fn is_approximate(self) -> bool {
60+
matches!(
61+
self,
62+
Statistic::Count
63+
| Statistic::Sum
64+
| Statistic::Cardinality
65+
| Statistic::Quantile
66+
| Statistic::Topk
67+
)
68+
}
69+
7270
pub fn from_str(s: &str) -> Option<Self> {
7371
debug!("Parsing Statistic from string: {}", s);
7472
match s.to_lowercase().as_str() {

asap-common/dependencies/rs/promql_utilities/src/query_logics/parsing.rs

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@ use tracing::debug;
44
use crate::ast_matching::promql_pattern::AggregationModifierType;
55
use crate::ast_matching::PromQLMatchResult;
66
use crate::data_model::KeyByLabelNames;
7-
use crate::query_logics::enums::{AggregationOperator, QueryPatternType, Statistic};
7+
use crate::query_logics::enums::{AggregationOperator, PromQLFunction, Statistic};
8+
use crate::query_logics::logics::get_is_collapsable;
89

910
#[derive(Debug, Clone, PartialEq, Eq)]
1011
pub enum StatisticExtractionError {
11-
MissingStatistic { pattern_type: QueryPatternType },
12+
MissingStatistic,
1213
UnsupportedStatistic { statistic: String },
1314
}
1415

1516
impl std::fmt::Display for StatisticExtractionError {
1617
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1718
match self {
18-
Self::MissingStatistic { pattern_type } => {
19+
Self::MissingStatistic => {
1920
write!(
2021
f,
21-
"No statistic found for query pattern type {pattern_type:?}"
22+
"No temporal function or aggregation operation found in match result"
2223
)
2324
}
2425
Self::UnsupportedStatistic { statistic } => {
@@ -72,28 +73,66 @@ pub fn get_metric_and_spatial_filter(match_result: &PromQLMatchResult) -> (Strin
7273
(metric_name, spatial_filter)
7374
}
7475

75-
/// Get statistics to compute based on pattern type and tokens.
76+
/// Get statistics to compute from a matched query's tokens.
77+
///
78+
/// Explicitly handles the three reachable shapes:
79+
/// - Only a temporal function (`"function"` token, no `"aggregation"`): the
80+
/// statistic comes from the function name.
81+
/// - Only a spatial aggregation (`"aggregation"` token, no `"function"`): the
82+
/// statistic comes from the aggregation operator.
83+
/// - Both (a spatial aggregation wrapping a temporal function): only ever
84+
/// reachable via a pattern already narrowed to a collapsable `(function,
85+
/// op)` pair (see `get_is_collapsable`, and #508's pattern-narrowing fix
86+
/// that makes non-collapsable combinations fail to match at all) — asserted
87+
/// below rather than silently trusted. The statistic still comes from the
88+
/// *function*, never the outer op: e.g. `count_over_time` + `sum` needs a
89+
/// `Count` accumulator, not a `Sum` one — summing per-series counts gives
90+
/// the group's total count, so the outer op only describes how per-series
91+
/// results combine, never which statistic must be precomputed.
92+
///
7693
/// Returns a typed error if the matched statistic/function name is not
7794
/// recognized, so callers can decide whether to skip or fail the query.
7895
pub fn get_statistics_to_compute(
79-
pattern_type: QueryPatternType,
8096
match_result: &PromQLMatchResult,
8197
) -> Result<Vec<Statistic>, StatisticExtractionError> {
82-
debug!("Computing statistics for pattern type {:?}", pattern_type);
83-
let statistic_to_compute: Option<String> = match pattern_type {
84-
QueryPatternType::OnlyTemporal | QueryPatternType::OneTemporalOneSpatial => {
85-
match_result.get_function_name().map(|function_name| {
86-
let name = function_name.to_lowercase();
87-
name.split('_').next().unwrap_or(&name).to_string()
88-
})
89-
}
90-
QueryPatternType::OnlySpatial => match_result
98+
let has_function = match_result.tokens.contains_key("function");
99+
let has_aggregation = match_result.tokens.contains_key("aggregation");
100+
debug!("Computing statistics (has_function={has_function}, has_aggregation={has_aggregation})");
101+
102+
let function_statistic = |match_result: &PromQLMatchResult| {
103+
match_result.get_function_name().map(|function_name| {
104+
let name = function_name.to_lowercase();
105+
name.split('_').next().unwrap_or(&name).to_string()
106+
})
107+
};
108+
109+
let statistic_to_compute: Option<String> = if has_function && has_aggregation {
110+
debug_assert!(
111+
match_result
112+
.get_function_name()
113+
.and_then(|f| f.parse::<PromQLFunction>().ok())
114+
.zip(
115+
match_result
116+
.get_aggregation_op()
117+
.and_then(|o| o.parse::<AggregationOperator>().ok())
118+
)
119+
.is_some_and(|(f, o)| get_is_collapsable(f, o)),
120+
"a match with both function and aggregation tokens must be collapsable \
121+
(patterns are narrowed to only collapsable pairs, see #508)"
122+
);
123+
function_statistic(match_result)
124+
} else if has_function {
125+
function_statistic(match_result)
126+
} else if has_aggregation {
127+
match_result
91128
.get_aggregation_op()
92-
.map(|agg| agg.to_lowercase()),
129+
.map(|agg| agg.to_lowercase())
130+
} else {
131+
None
93132
};
94133

95134
let Some(statistic_to_compute) = statistic_to_compute else {
96-
return Err(StatisticExtractionError::MissingStatistic { pattern_type });
135+
return Err(StatisticExtractionError::MissingStatistic);
97136
};
98137

99138
debug!("Found statistic to compute: {}", statistic_to_compute);
@@ -194,9 +233,7 @@ mod tests {
194233

195234
#[test]
196235
fn unsupported_matched_temporal_statistic_returns_typed_error() {
197-
let err =
198-
get_statistics_to_compute(QueryPatternType::OnlyTemporal, &temporal_match("stddev"))
199-
.unwrap_err();
236+
let err = get_statistics_to_compute(&temporal_match("stddev")).unwrap_err();
200237

201238
assert_eq!(
202239
err,
@@ -208,17 +245,9 @@ mod tests {
208245

209246
#[test]
210247
fn missing_statistic_returns_typed_error() {
211-
let err = get_statistics_to_compute(
212-
QueryPatternType::OnlyTemporal,
213-
&PromQLMatchResult::with_tokens(HashMap::new()),
214-
)
215-
.unwrap_err();
248+
let err =
249+
get_statistics_to_compute(&PromQLMatchResult::with_tokens(HashMap::new())).unwrap_err();
216250

217-
assert_eq!(
218-
err,
219-
StatisticExtractionError::MissingStatistic {
220-
pattern_type: QueryPatternType::OnlyTemporal
221-
}
222-
);
251+
assert_eq!(err, StatisticExtractionError::MissingStatistic);
223252
}
224253
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,10 @@ fn extract_requirements(
170170
let ast = promql_parser::parser::parse(query).ok()?;
171171
let patterns = build_patterns();
172172

173-
let (pattern_type, match_result) = patterns.iter().find_map(|(pt, pat)| {
173+
let match_result = patterns.iter().find_map(|pat| {
174174
let r = pat.matches(&ast);
175175
if r.matches {
176-
Some((*pt, r))
176+
Some(r)
177177
} else {
178178
None
179179
}
@@ -182,7 +182,6 @@ fn extract_requirements(
182182
build_query_requirements_promql(
183183
query,
184184
&match_result,
185-
pattern_type,
186185
metric_schema,
187186
data_ingestion_interval_ms,
188187
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ mod tests {
126126
);
127127

128128
let mut seen_ids: StdHashMap<u64, ()> = StdHashMap::new();
129-
for (id, _) in solution.deployed_configs.iter() {
129+
for id in solution.deployed_configs.keys() {
130130
assert!(
131131
seen_ids.insert(*id, ()).is_none(),
132132
"duplicate aggregation_id"

0 commit comments

Comments
 (0)