@@ -25,6 +25,22 @@ use std::collections::HashMap;
2525use std:: time:: Instant ;
2626use 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+
2844impl 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