Skip to content

Commit 14d032c

Browse files
zaoxingclaude
andcommitted
Merge origin/main into 476-rate-increase-functions-do-no-counter-reset-correction
Resolve conflicts from main's promql.rs dedup + QueryPatternType removal (#511/#515/#517/#518) against the rate()/increase() counter-reset correction + extrapolation work: - promql.rs: keep main's refactored build_query_kwargs_promql signature (&Statistic, match_result); thread `timestamps` through it and retain the Rate|Increase range-boundary arm; drop now-unused imports (QueryPatternType, get_is_collapsable, AggregationOperator, PromQLFunction, get_spatial_aggregation_output_labels, get_statistics_to_compute). - simple_engine/mod.rs: keep RANGE_*_MS_KWARG imports (per-step range boundaries), drop removed QueryPatternType. - tests: keep the single-pop Increase extrapolation test (arroyo serde now makes it deserializable) over main's obsolete not_implemented assertion; keep all new rate/increase test modules alongside main's range_query_arithmetic_tests. Verified: workspace builds; 560 lib tests pass (0 failures); fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 parents dc60e4c + 1796f2a commit 14d032c

33 files changed

Lines changed: 1833 additions & 1337 deletions

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
@@ -13,24 +13,6 @@ pub const RANGE_START_MS_KWARG: &str = "range_start_ms";
1313
/// `rate()`/`increase()` evaluation. See [`RANGE_START_MS_KWARG`].
1414
pub const RANGE_END_MS_KWARG: &str = "range_end_ms";
1515

16-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17-
pub enum QueryPatternType {
18-
OnlyTemporal,
19-
OnlySpatial,
20-
OneTemporalOneSpatial,
21-
}
22-
23-
impl std::fmt::Display for QueryPatternType {
24-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25-
debug!("Formatting QueryPatternType: {:?}", self);
26-
match self {
27-
QueryPatternType::OnlyTemporal => write!(f, "only_temporal"),
28-
QueryPatternType::OnlySpatial => write!(f, "only_spatial"),
29-
QueryPatternType::OneTemporalOneSpatial => write!(f, "one_temporal_one_spatial"),
30-
}
31-
}
32-
}
33-
3416
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3517
pub enum QueryTreatmentType {
3618
Exact,
@@ -79,6 +61,22 @@ impl std::fmt::Display for Statistic {
7961

8062
#[allow(clippy::should_implement_trait)]
8163
impl Statistic {
64+
/// Returns `true` for statistics whose result requires approximate
65+
/// pre-aggregation, regardless of whether they were reached via a
66+
/// temporal function (`PromQLFunction::is_approximate`) or a spatial
67+
/// aggregation operator (`AggregationOperator::is_approximate`) — for
68+
/// every `Statistic` reachable from either origin, the two origins agree.
69+
pub fn is_approximate(self) -> bool {
70+
matches!(
71+
self,
72+
Statistic::Count
73+
| Statistic::Sum
74+
| Statistic::Cardinality
75+
| Statistic::Quantile
76+
| Statistic::Topk
77+
)
78+
}
79+
8280
pub fn from_str(s: &str) -> Option<Self> {
8381
debug!("Parsing Statistic from string: {}", s);
8482
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
}

0 commit comments

Comments
 (0)