Skip to content

Commit a420865

Browse files
refactor(query-engine): dedup instant/range PromQL arm-building and parsing in promql.rs (#517)
1 parent 60b2224 commit a420865

3 files changed

Lines changed: 425 additions & 104 deletions

File tree

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

Lines changed: 178 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ use std::collections::HashMap;
2525
use std::time::Instant;
2626
use tracing::{debug, warn};
2727

28+
/// Detects whether either side of a PromQL binary expression is a scalar
29+
/// (numeric literal), returning the scalar value, the other (vector) arm,
30+
/// and whether the scalar was on the left. Shared by instant and range
31+
/// binary-expression handling.
32+
fn detect_scalar_arm<'a>(
33+
lhs: &'a promql_parser::parser::Expr,
34+
rhs: &'a promql_parser::parser::Expr,
35+
) -> Option<(f64, &'a promql_parser::parser::Expr, bool)> {
36+
use promql_parser::parser::Expr;
37+
match (lhs, rhs) {
38+
(_, Expr::NumberLiteral(nl)) => Some((nl.val, lhs, false)),
39+
(Expr::NumberLiteral(nl), _) => Some((nl.val, rhs, true)),
40+
_ => None,
41+
}
42+
}
43+
2844
impl SimpleEngine {
2945
/// Calculates start timestamp for PromQL queries
3046
fn calculate_start_timestamp_promql(
@@ -346,11 +362,37 @@ impl SimpleEngine {
346362
})
347363
}
348364

365+
/// Recursively unwraps `Paren`, then structurally resolves a leaf PromQL
366+
/// arm (i.e. not `Binary` or `NumberLiteral`) to its `QueryConfig` and
367+
/// base `QueryExecutionContext`. Shared leaf-resolution step for both
368+
/// `build_arm_logical_plan` (instant) and `build_arm_range_context` (range).
369+
///
370+
/// Returns `None` for `Binary` arms (caller handles recursion) and
371+
/// `NumberLiteral` arms (caller handles scalars).
372+
fn resolve_arm_leaf_context(
373+
&self,
374+
arm_ast: &promql_parser::parser::Expr,
375+
time: f64,
376+
) -> Option<(QueryExecutionContext, Vec<String>)> {
377+
use promql_parser::parser::Expr;
378+
379+
match arm_ast {
380+
Expr::NumberLiteral(_) | Expr::Binary(_) => None,
381+
Expr::Paren(paren) => self.resolve_arm_leaf_context(&paren.expr, time),
382+
other => {
383+
let config = self.find_query_config_promql_structural(other)?;
384+
let ctx = self.build_query_execution_context_from_ast(other, &config, time)?;
385+
let label_names = ctx.metadata.query_output_labels.labels.clone();
386+
Some((ctx, label_names))
387+
}
388+
}
389+
}
390+
349391
/// Recursively builds a DataFusion logical plan for one arm of a binary
350392
/// arithmetic expression.
351393
///
352-
/// - Leaf arm (supported PromQL pattern): look up config structurally, build
353-
/// context, return its `to_logical_plan()` together with the output label names.
394+
/// - Leaf arm (supported PromQL pattern): resolved via `resolve_arm_leaf_context`,
395+
/// returning its `to_logical_plan()` together with the output label names.
354396
/// - Binary arm: recursively build both sub-arms and combine with
355397
/// `build_binary_vector_plan`.
356398
/// - Scalar literal: returns `None` (handled by the caller separately).
@@ -374,11 +416,8 @@ impl SimpleEngine {
374416
.ok()?;
375417
Some((combined, lhs_labels))
376418
}
377-
other => {
378-
// Leaf pattern: structural config lookup + context + plan
379-
let config = self.find_query_config_promql_structural(other)?;
380-
let ctx = self.build_query_execution_context_from_ast(other, &config, time)?;
381-
let label_names = ctx.metadata.query_output_labels.labels.clone();
419+
_ => {
420+
let (ctx, label_names) = self.resolve_arm_leaf_context(arm_ast, time)?;
382421
let plan = ctx.to_logical_plan().ok()?;
383422
Some((plan, label_names))
384423
}
@@ -408,13 +447,7 @@ impl SimpleEngine {
408447
let rhs = binary.rhs.as_ref();
409448
let op = &binary.op;
410449

411-
// Scalar case: either side may be a numeric literal
412-
let scalar_case: Option<(f64, &Expr, bool)> = match (lhs, rhs) {
413-
(_, Expr::NumberLiteral(nl)) => Some((nl.val, lhs, false)),
414-
(Expr::NumberLiteral(nl), _) => Some((nl.val, rhs, true)),
415-
_ => None,
416-
};
417-
if let Some((scalar, vector_arm, scalar_on_left)) = scalar_case {
450+
if let Some((scalar, vector_arm, scalar_on_left)) = detect_scalar_arm(lhs, rhs) {
418451
let (vector_plan, label_names) = self.build_arm_logical_plan(vector_arm, time)?;
419452
let combined =
420453
build_scalar_plan(vector_plan, scalar, op, scalar_on_left, label_names.clone())
@@ -469,7 +502,13 @@ impl SimpleEngine {
469502
}
470503
}
471504

472-
/// Recursively builds a range execution context for one arm of a binary arithmetic expression.
505+
/// Recursively builds a range execution context for one arm of a binary
506+
/// arithmetic expression.
507+
///
508+
/// Leaf resolution (Paren-unwrap + structural config lookup) is shared
509+
/// with `build_arm_logical_plan` via `resolve_arm_leaf_context`. Note this
510+
/// does not support nested `Binary` arms (e.g. `(a+b)*c` over a range) —
511+
/// tracked separately in #516.
473512
fn build_arm_range_context(
474513
&self,
475514
arm_ast: &promql_parser::parser::Expr,
@@ -479,63 +518,57 @@ impl SimpleEngine {
479518
) -> Option<(RangeQueryExecutionContext, Vec<String>)> {
480519
use promql_parser::parser::Expr;
481520

482-
match arm_ast {
483-
Expr::NumberLiteral(_) => None, // caller handles scalars
484-
Expr::Paren(paren) => self.build_arm_range_context(&paren.expr, start, end, step),
485-
other => {
486-
let config = self.find_query_config_promql_structural(other)?;
487-
let base_context =
488-
self.build_query_execution_context_from_ast(other, &config, end)?;
489-
let label_names = base_context.metadata.query_output_labels.labels.clone();
521+
if matches!(arm_ast, Expr::NumberLiteral(_)) {
522+
return None; // caller handles scalars
523+
}
490524

491-
let start_ms = Self::convert_query_time_to_data_time(start);
492-
let end_ms = Self::convert_query_time_to_data_time(end);
493-
let step_ms = (step * 1000.0) as u64;
525+
let (base_context, label_names) = self.resolve_arm_leaf_context(arm_ast, end)?;
494526

495-
let tumbling_window_ms = self
496-
.streaming_config
497-
.read()
498-
.unwrap()
499-
.get_aggregation_config(base_context.agg_info.aggregation_id_for_value)
500-
.map(|c| c.window_size_ms)?;
501-
502-
self.validate_range_query_params(start_ms, end_ms, step_ms, tumbling_window_ms)
503-
.map_err(|e| {
504-
warn!("Range arm query validation failed: {}", e);
505-
e
506-
})
507-
.ok()?;
527+
let start_ms = Self::convert_query_time_to_data_time(start);
528+
let end_ms = Self::convert_query_time_to_data_time(end);
529+
let step_ms = (step * 1000.0) as u64;
508530

509-
let lookback_ms = base_context.store_plan.values_query.end_timestamp
510-
- base_context.store_plan.values_query.start_timestamp;
511-
512-
let buckets_per_step = (step_ms / tumbling_window_ms) as usize;
513-
let lookback_bucket_count = (lookback_ms / tumbling_window_ms) as usize;
514-
515-
let mut extended_store_plan = base_context.store_plan.clone();
516-
extended_store_plan.values_query.start_timestamp =
517-
start_ms.saturating_sub(lookback_ms);
518-
extended_store_plan.values_query.end_timestamp = end_ms;
519-
extended_store_plan.values_query.is_exact_query = false;
520-
521-
let range_context = RangeQueryExecutionContext {
522-
base: QueryExecutionContext {
523-
store_plan: extended_store_plan,
524-
..base_context
525-
},
526-
range_params: RangeQueryParams {
527-
start: start_ms,
528-
end: end_ms,
529-
step: step_ms,
530-
},
531-
buckets_per_step,
532-
lookback_bucket_count,
533-
tumbling_window_ms,
534-
};
535-
536-
Some((range_context, label_names))
537-
}
538-
}
531+
let tumbling_window_ms = self
532+
.streaming_config
533+
.read()
534+
.unwrap()
535+
.get_aggregation_config(base_context.agg_info.aggregation_id_for_value)
536+
.map(|c| c.window_size_ms)?;
537+
538+
self.validate_range_query_params(start_ms, end_ms, step_ms, tumbling_window_ms)
539+
.map_err(|e| {
540+
warn!("Range arm query validation failed: {}", e);
541+
e
542+
})
543+
.ok()?;
544+
545+
let lookback_ms = base_context.store_plan.values_query.end_timestamp
546+
- base_context.store_plan.values_query.start_timestamp;
547+
548+
let buckets_per_step = (step_ms / tumbling_window_ms) as usize;
549+
let lookback_bucket_count = (lookback_ms / tumbling_window_ms) as usize;
550+
551+
let mut extended_store_plan = base_context.store_plan.clone();
552+
extended_store_plan.values_query.start_timestamp = start_ms.saturating_sub(lookback_ms);
553+
extended_store_plan.values_query.end_timestamp = end_ms;
554+
extended_store_plan.values_query.is_exact_query = false;
555+
556+
let range_context = RangeQueryExecutionContext {
557+
base: QueryExecutionContext {
558+
store_plan: extended_store_plan,
559+
..base_context
560+
},
561+
range_params: RangeQueryParams {
562+
start: start_ms,
563+
end: end_ms,
564+
step: step_ms,
565+
},
566+
buckets_per_step,
567+
lookback_bucket_count,
568+
tumbling_window_ms,
569+
};
570+
571+
Some((range_context, label_names))
539572
}
540573

541574
/// Handles a binary arithmetic PromQL expression for range queries.
@@ -561,13 +594,7 @@ impl SimpleEngine {
561594
let rhs = binary.rhs.as_ref();
562595
let op = &binary.op;
563596

564-
// Scalar case: either side may be a numeric literal
565-
let scalar_case: Option<(f64, &Expr, bool)> = match (lhs, rhs) {
566-
(_, Expr::NumberLiteral(nl)) => Some((nl.val, lhs, false)),
567-
(Expr::NumberLiteral(nl), _) => Some((nl.val, rhs, true)),
568-
_ => None,
569-
};
570-
if let Some((scalar, vector_arm, scalar_on_left)) = scalar_case {
597+
if let Some((scalar, vector_arm, scalar_on_left)) = detect_scalar_arm(lhs, rhs) {
571598
let (ctx, labels) = self.build_arm_range_context(vector_arm, start, end, step)?;
572599
let results = self.execute_range_query_pipeline(&ctx).ok()?;
573600
let combined: Vec<RangeVectorElement> = results
@@ -869,21 +896,27 @@ impl SimpleEngine {
869896
let query_start_time = Instant::now();
870897
debug!("Handling query: {} at time {}", query, time);
871898

899+
let ast = match promql_parser::parser::parse(&query) {
900+
Ok(ast) => ast,
901+
Err(e) => {
902+
warn!("Failed to parse PromQL query '{}': {}", query, e);
903+
return None;
904+
}
905+
};
906+
872907
// Check for binary arithmetic before attempting single-query dispatch.
873908
// Binary expressions won't have a matching query_config, so we handle them here.
874-
if let Ok(ast) = promql_parser::parser::parse(&query) {
875-
if matches!(&ast, promql_parser::parser::Expr::Binary(_)) {
876-
let result = self.handle_binary_expr_promql(&ast, time);
877-
let total_query_duration = query_start_time.elapsed();
878-
debug!(
879-
"Binary arithmetic query handling took: {:.2}ms",
880-
total_query_duration.as_secs_f64() * 1000.0
881-
);
882-
return result;
883-
}
909+
if matches!(&ast, promql_parser::parser::Expr::Binary(_)) {
910+
let result = self.handle_binary_expr_promql(&ast, time);
911+
let total_query_duration = query_start_time.elapsed();
912+
debug!(
913+
"Binary arithmetic query handling took: {:.2}ms",
914+
total_query_duration.as_secs_f64() * 1000.0
915+
);
916+
return result;
884917
}
885918

886-
let context = self.build_query_execution_context_promql(query, time)?;
919+
let context = self.build_query_execution_context_from_parsed(&ast, &query, time)?;
887920

888921
debug!(
889922
"Querying store for metric: {}, aggregation_id: {}, range: [{}, {}]",
@@ -974,8 +1007,6 @@ impl SimpleEngine {
9741007
query: String,
9751008
time: f64,
9761009
) -> Option<QueryExecutionContext> {
977-
let query_time = Self::convert_query_time_to_data_time(time);
978-
9791010
// Parse PromQL AST using promql-parser crate
9801011
let parse_start_time = Instant::now();
9811012
let ast = match promql_parser::parser::parse(&query) {
@@ -993,9 +1024,23 @@ impl SimpleEngine {
9931024
}
9941025
};
9951026

1027+
self.build_query_execution_context_from_parsed(&ast, &query, time)
1028+
}
1029+
1030+
/// Variant of `build_query_execution_context_promql` that accepts an
1031+
/// already-parsed AST, avoiding a redundant re-parse when the caller
1032+
/// (e.g. `handle_query_promql`) has already parsed `query` once.
1033+
fn build_query_execution_context_from_parsed(
1034+
&self,
1035+
ast: &promql_parser::parser::Expr,
1036+
query: &str,
1037+
time: f64,
1038+
) -> Option<QueryExecutionContext> {
1039+
let query_time = Self::convert_query_time_to_data_time(time);
1040+
9961041
let pattern_match_start_time = Instant::now();
9971042

998-
let found_match = self.find_matching_controller_pattern(&ast, &query);
1043+
let found_match = self.find_matching_controller_pattern(ast, query);
9991044

10001045
let (query_pattern_type, match_result) = match found_match {
10011046
Some((pt, result)) => {
@@ -1017,7 +1062,7 @@ impl SimpleEngine {
10171062
let query_context_start_time = Instant::now();
10181063

10191064
// Resolve aggregation: try pre-configured query_configs first, fall back to capability matching.
1020-
let agg_info: AggregationIdInfo = if let Some(config) = self.find_query_config(&query) {
1065+
let agg_info: AggregationIdInfo = if let Some(config) = self.find_query_config(query) {
10211066
self.get_aggregation_id_info(&config)
10221067
.map_err(|e| {
10231068
warn!("{}", e);
@@ -1036,7 +1081,7 @@ impl SimpleEngine {
10361081
_ => &empty_schema,
10371082
};
10381083
let requirements = build_query_requirements_promql(
1039-
&query,
1084+
query,
10401085
&match_result,
10411086
query_pattern_type,
10421087
metric_schema,
@@ -1072,10 +1117,32 @@ impl SimpleEngine {
10721117
start: f64,
10731118
end: f64,
10741119
step: f64,
1120+
) -> Option<RangeQueryExecutionContext> {
1121+
let ast = match promql_parser::parser::parse(&query) {
1122+
Ok(ast) => ast,
1123+
Err(e) => {
1124+
warn!("Failed to parse PromQL query '{}': {}", query, e);
1125+
return None;
1126+
}
1127+
};
1128+
1129+
self.build_range_query_execution_context_from_parsed(&ast, &query, start, end, step)
1130+
}
1131+
1132+
/// Variant of `build_range_query_execution_context_promql` that accepts an
1133+
/// already-parsed AST, avoiding a redundant re-parse when the caller
1134+
/// (e.g. `handle_range_query_promql`) has already parsed `query` once.
1135+
fn build_range_query_execution_context_from_parsed(
1136+
&self,
1137+
ast: &promql_parser::parser::Expr,
1138+
query: &str,
1139+
start: f64,
1140+
end: f64,
1141+
step: f64,
10751142
) -> Option<RangeQueryExecutionContext> {
10761143
// First, build the base instant query context (reuse existing logic)
10771144
// Use 'end' as the reference time for parsing
1078-
let base_context = self.build_query_execution_context_promql(query, end)?;
1145+
let base_context = self.build_query_execution_context_from_parsed(ast, query, end)?;
10791146

10801147
// Convert to milliseconds
10811148
let start_ms = Self::convert_query_time_to_data_time(start);
@@ -1142,20 +1209,27 @@ impl SimpleEngine {
11421209
query, start, end, step
11431210
);
11441211

1145-
// Check for binary arithmetic before attempting single-query dispatch.
1146-
if let Ok(ast) = promql_parser::parser::parse(&query) {
1147-
if matches!(&ast, promql_parser::parser::Expr::Binary(_)) {
1148-
let result = self.handle_binary_expr_range_promql(&ast, start, end, step);
1149-
let total_duration = query_start_time.elapsed();
1150-
debug!(
1151-
"Binary arithmetic range query handling took: {:.2}ms",
1152-
total_duration.as_secs_f64() * 1000.0
1153-
);
1154-
return result;
1212+
let ast = match promql_parser::parser::parse(&query) {
1213+
Ok(ast) => ast,
1214+
Err(e) => {
1215+
warn!("Failed to parse PromQL query '{}': {}", query, e);
1216+
return None;
11551217
}
1218+
};
1219+
1220+
// Check for binary arithmetic before attempting single-query dispatch.
1221+
if matches!(&ast, promql_parser::parser::Expr::Binary(_)) {
1222+
let result = self.handle_binary_expr_range_promql(&ast, start, end, step);
1223+
let total_duration = query_start_time.elapsed();
1224+
debug!(
1225+
"Binary arithmetic range query handling took: {:.2}ms",
1226+
total_duration.as_secs_f64() * 1000.0
1227+
);
1228+
return result;
11561229
}
11571230

1158-
let context = self.build_range_query_execution_context_promql(query, start, end, step)?;
1231+
let context =
1232+
self.build_range_query_execution_context_from_parsed(&ast, &query, start, end, step)?;
11591233

11601234
// Execute range query pipeline
11611235
let results: Vec<RangeVectorElement> = self

0 commit comments

Comments
 (0)