@@ -8,7 +8,8 @@ use crate::analytical_cost::{
88 AnalyticalCostError , ExecutionMultiplicity , PhysicalDagNode , PhysicalOperator ,
99} ;
1010use crate :: analytical_statistics:: {
11- ComparisonScope , EdgeStatistics , OperatorStatistics , OperatorStatisticsProvider , SourceCoverage ,
11+ ComparisonScope , EdgeStatistics , OperatorStatistics , OperatorStatisticsProvider ,
12+ PromqlBinaryOperandMode , SourceCoverage ,
1213} ;
1314
1415/// A lowered physical DAG and the node whose output is the query result.
@@ -605,6 +606,19 @@ pub fn lower_query_physical_dag(
605606 "PromQL binary inputs do not match child outputs" ,
606607 ) ) ;
607608 }
609+ let operand_mode = match ( left_scalar, right_scalar) {
610+ ( false , false ) => PromqlBinaryOperandMode :: VectorVector ,
611+ ( false , true ) => PromqlBinaryOperandMode :: VectorScalar ,
612+ ( true , false ) => PromqlBinaryOperandMode :: ScalarVector ,
613+ ( true , true ) => unreachable ! ( "scalar/scalar was rejected above" ) ,
614+ } ;
615+ if require_promql_statistics ( statistics, 2 ) ?. binary_operand_mode
616+ != Some ( operand_mode)
617+ {
618+ return Err ( AnalyticalCostError :: InconsistentOperatorStatistics (
619+ "PromQL binary operand mode disagrees with its logical operands" ,
620+ ) ) ;
621+ }
608622 if left_scalar || right_scalar {
609623 if vector_match. is_some ( )
610624 || statistics. hash_join_build_side . is_some ( )
@@ -895,7 +909,10 @@ fn require_operator_statistics(
895909 return invalid ( "only Scan may charge source bytes" ) ;
896910 }
897911 if matches ! ( operator, PhysicalOperator :: PromqlScalarLeaf ) {
898- require_promql_statistics ( statistics, 0 ) ?;
912+ let promql = require_promql_statistics ( statistics, 0 ) ?;
913+ if promql. output_series != 0 || statistics. output . rows != promql. evaluation_steps {
914+ return invalid ( "PromQL scalar leaf must emit one scalar row per evaluation step" ) ;
915+ }
899916 return Ok ( ( ) ) ;
900917 }
901918 let input = statistics. inputs . first ( ) . copied ( ) . ok_or (
@@ -982,7 +999,43 @@ fn require_operator_statistics(
982999 }
9831000 }
9841001 PhysicalOperator :: PromqlVectorBinary => {
985- require_promql_statistics ( statistics, 2 ) ?;
1002+ let promql = require_promql_statistics ( statistics, 2 ) ?;
1003+ let mode = promql
1004+ . binary_operand_mode
1005+ . ok_or ( AnalyticalCostError :: MissingOrStale (
1006+ "promql_binary_operand_mode" ,
1007+ ) ) ?;
1008+ match mode {
1009+ PromqlBinaryOperandMode :: VectorVector => {
1010+ if statistics. hash_join_build_side . is_none ( ) || statistics. key_bytes . is_none ( ) {
1011+ return Err ( AnalyticalCostError :: MissingOrStale (
1012+ "vector_binary_label_match_statistics" ,
1013+ ) ) ;
1014+ }
1015+ }
1016+ PromqlBinaryOperandMode :: VectorScalar => {
1017+ if promql. input_series [ 1 ] != 0
1018+ || statistics. inputs [ 1 ] . rows != promql. evaluation_steps
1019+ || statistics. hash_join_build_side . is_some ( )
1020+ || statistics. key_bytes . is_some ( )
1021+ {
1022+ return invalid (
1023+ "vector/scalar binary evidence has an invalid scalar edge or label-match state" ,
1024+ ) ;
1025+ }
1026+ }
1027+ PromqlBinaryOperandMode :: ScalarVector => {
1028+ if promql. input_series [ 0 ] != 0
1029+ || statistics. inputs [ 0 ] . rows != promql. evaluation_steps
1030+ || statistics. hash_join_build_side . is_some ( )
1031+ || statistics. key_bytes . is_some ( )
1032+ {
1033+ return invalid (
1034+ "scalar/vector binary evidence has an invalid scalar edge or label-match state" ,
1035+ ) ;
1036+ }
1037+ }
1038+ }
9861039 }
9871040 PhysicalOperator :: PromqlInfoEnrich => {
9881041 let promql = require_promql_statistics ( statistics, 2 ) ?;
@@ -1441,6 +1494,7 @@ mod tests {
14411494 window_samples_per_series : None ,
14421495 subquery_steps : None ,
14431496 scalar_ops_per_row : None ,
1497+ binary_operand_mode : None ,
14441498 } ) ;
14451499 statistics
14461500 }
@@ -2333,10 +2387,15 @@ mod tests {
23332387 let empty_scope = scope ( vec ! [ ] ) ;
23342388 let scalar = Rc :: new ( QueryExpr :: promql_scalar ( 5.0 ) ) ;
23352389 let scalar_statistics = with_promql ( statistics ( vec ! [ ] , edge ( 10 , 80 ) ) , vec ! [ ] , 0 , 10 ) ;
2336- let provided = HashMap :: from ( [ ( "query-0" . into ( ) , evidence ( scalar_statistics) ) ] ) ;
2390+ let mut provided = HashMap :: from ( [ ( "query-0" . into ( ) , evidence ( scalar_statistics) ) ] ) ;
23372391 let dag = lower_query_physical_dag ( & scalar, & empty_scope, & scripted ( & provided) ) . unwrap ( ) ;
23382392 assert_eq ! ( dag. nodes[ 0 ] . operator, PhysicalOperator :: PromqlScalarLeaf ) ;
23392393 assert ! ( estimate_physical_dag( & dag. nodes, & dag. root, & empty_scope, & dag. evidence) . is_ok( ) ) ;
2394+ provided. get_mut ( "query-0" ) . unwrap ( ) . statistics . output = edge ( 9 , 72 ) ;
2395+ assert ! ( matches!(
2396+ lower_query_physical_dag( & scalar, & empty_scope, & scripted( & provided) ) ,
2397+ Err ( AnalyticalCostError :: InconsistentOperatorStatistics ( _) )
2398+ ) ) ;
23402399
23412400 let timestamp = Rc :: new ( QueryExpr :: EvalTimestamp ) ;
23422401 let timestamp_statistics = with_promql ( statistics ( vec ! [ ] , edge ( 10 , 80 ) ) , vec ! [ ] , 0 , 10 ) ;
@@ -2549,12 +2608,14 @@ mod tests {
25492608 ) ;
25502609 vector_statistics. source_scan_bytes = 8_000 ;
25512610 let scalar = with_promql ( statistics ( vec ! [ ] , edge ( 10 , 80 ) ) , vec ! [ ] , 0 , 10 ) ;
2552- let binary = with_promql (
2611+ let mut binary = with_promql (
25532612 statistics ( vec ! [ edge( 1_000 , 16_000 ) , edge( 10 , 80 ) ] , edge ( 1_000 , 16_000 ) ) ,
25542613 vec ! [ 100 , 0 ] ,
25552614 100 ,
25562615 10 ,
25572616 ) ;
2617+ binary. promql . as_mut ( ) . unwrap ( ) . binary_operand_mode =
2618+ Some ( PromqlBinaryOperandMode :: VectorScalar ) ;
25582619 let provided = HashMap :: from ( [
25592620 ( "query-1" . into ( ) , evidence ( vector_statistics) ) ,
25602621 ( "query-2" . into ( ) , evidence ( scalar) ) ,
@@ -2649,6 +2710,8 @@ mod tests {
26492710 ) ;
26502711 binary. key_bytes = Some ( 32 ) ;
26512712 binary. hash_join_build_side = Some ( HashJoinBuildSide :: Right ) ;
2713+ binary. promql . as_mut ( ) . unwrap ( ) . binary_operand_mode =
2714+ Some ( PromqlBinaryOperandMode :: VectorVector ) ;
26522715 let provided = HashMap :: from ( [
26532716 ( "query-1" . into ( ) , evidence ( left) ) ,
26542717 ( "query-2" . into ( ) , evidence ( right) ) ,
@@ -2659,6 +2722,32 @@ mod tests {
26592722 assert_eq ! ( estimate. cpu_ops, 3_400.0 ) ;
26602723 assert_eq ! ( estimate. scan_bytes, 12_000 ) ;
26612724
2725+ let mut empty = provided. clone ( ) ;
2726+ for id in [ "query-1" , "query-2" ] {
2727+ let evidence = empty. get_mut ( id) . unwrap ( ) ;
2728+ evidence. statistics . inputs [ 0 ] = edge ( 0 , 0 ) ;
2729+ evidence. statistics . output = edge ( 0 , 0 ) ;
2730+ let promql = evidence. statistics . promql . as_mut ( ) . unwrap ( ) ;
2731+ promql. input_series [ 0 ] = 0 ;
2732+ promql. output_series = 0 ;
2733+ evidence. output_buffer_bytes = 0 ;
2734+ }
2735+ let evidence = empty. get_mut ( "query-0" ) . unwrap ( ) ;
2736+ evidence. statistics . inputs = vec ! [ edge( 0 , 0 ) , edge( 0 , 0 ) ] ;
2737+ evidence. statistics . output = edge ( 0 , 0 ) ;
2738+ let promql = evidence. statistics . promql . as_mut ( ) . unwrap ( ) ;
2739+ promql. input_series = vec ! [ 0 , 0 ] ;
2740+ promql. output_series = 0 ;
2741+ evidence. output_buffer_bytes = 0 ;
2742+ let empty_dag = lower_query_physical_dag ( & root, & scope, & scripted ( & empty) ) . unwrap ( ) ;
2743+ assert ! ( estimate_physical_dag(
2744+ & empty_dag. nodes,
2745+ & empty_dag. root,
2746+ & scope,
2747+ & empty_dag. evidence
2748+ )
2749+ . is_ok( ) ) ;
2750+
26622751 let mut missing = provided;
26632752 missing
26642753 . get_mut ( "query-0" )
0 commit comments