@@ -357,7 +357,7 @@ use asap_types::post_asap::{
357357use asap_types:: pre_asap:: agg_intent:: { agg_is_mergeable, AggIntent } ;
358358use asap_types:: pre_asap:: cse:: { share_common_subtrees, structural_hash, HashCache } ;
359359use asap_types:: pre_asap:: expr_ir:: ColumnRef ;
360- use asap_types:: pre_asap:: query_expr:: { QueryExpr , QueryExprError , Reduction } ;
360+ use asap_types:: pre_asap:: query_expr:: { BinaryOpKind , QueryExpr , QueryExprError , Reduction } ;
361361use asap_types:: pre_asap:: schema:: Schema ;
362362use asap_types:: types:: AccuracyTarget ;
363363use asap_types:: workload:: { QueryRecurrence , QueryWorkload , RepeatedDemand } ;
@@ -1263,6 +1263,17 @@ impl<'a> SketchAlgorithmStrategy<'a> {
12631263 /// differs — see [`realize_child_with`]).
12641264 fn propose_with ( & self , root : & Rc < QueryExpr > , intent_override : Option < & AggIntent > ) -> Proposals {
12651265 let mut proposals = Proposals :: default ( ) ;
1266+ if intent_override. is_none ( ) && is_supported_exact_binary ( root) {
1267+ if let Ok ( Some ( node) ) = realize_exact_binary ( root, self . models ) {
1268+ proposals. candidates . push ( ReplacementSubDAG {
1269+ replacement : Replacement :: Summary ( node) ,
1270+ strategy : "SketchAlgorithmStrategy" ,
1271+ provenance : ReplacementProvenance :: SummaryImplementation ,
1272+ rationale : "preserve exact PromQL arithmetic over independently realized summary operands" . into ( ) ,
1273+ } ) ;
1274+ }
1275+ return proposals;
1276+ }
12661277 let Some ( declared) = bindable_intent ( root) else {
12671278 return proposals;
12681279 } ;
@@ -1412,7 +1423,7 @@ fn aggregate_child(node: &QueryExpr) -> Option<&Rc<QueryExpr>> {
14121423
14131424impl ReplacementStrategy for SketchAlgorithmStrategy < ' _ > {
14141425 fn matches ( & self , target : & TargetSubDAG < ' _ > ) -> bool {
1415- bindable_intent ( target. root ) . is_some ( )
1426+ bindable_intent ( target. root ) . is_some ( ) || is_supported_exact_binary ( target . root )
14161427 }
14171428
14181429 fn replacements ( & self , target : & TargetSubDAG < ' _ > ) -> Vec < ReplacementSubDAG > {
@@ -1527,6 +1538,9 @@ pub(crate) fn realize_child_with(
15271538 models : Models < ' _ > ,
15281539 end_to_end_target : Option < & AccuracyTarget > ,
15291540) -> Result < Rc < SummaryNode > , ImplementError > {
1541+ if let Some ( composed) = realize_exact_binary ( root, models) ? {
1542+ return Ok ( composed) ;
1543+ }
15301544 let overridden = end_to_end_target. and_then ( |target| {
15311545 let declared = bindable_intent ( root) ?;
15321546 match accuracy_target ( declared) {
@@ -1560,6 +1574,88 @@ pub(crate) fn realize_child_with(
15601574 }
15611575}
15621576
1577+ /// Preserve an exact arithmetic root while allowing each vector operand to
1578+ /// select its own summary implementation. If either vector arm cannot be
1579+ /// accelerated, return `None` so the caller keeps the whole query exact;
1580+ /// mixed raw/summary snapshots are never constructed.
1581+ fn realize_exact_binary (
1582+ root : & Rc < QueryExpr > ,
1583+ models : Models < ' _ > ,
1584+ ) -> Result < Option < Rc < SummaryNode > > , ImplementError > {
1585+ let QueryExpr :: BinaryOp {
1586+ op,
1587+ lhs,
1588+ rhs,
1589+ vector_match,
1590+ } = root. as_ref ( )
1591+ else {
1592+ return Ok ( None ) ;
1593+ } ;
1594+ if !matches ! ( op, BinaryOpKind :: Arithmetic ( _) | BinaryOpKind :: Pow ) || vector_match. is_some ( ) {
1595+ return Ok ( None ) ;
1596+ }
1597+
1598+ let lhs_scalar = is_promql_scalar ( lhs) ;
1599+ let rhs_scalar = is_promql_scalar ( rhs) ;
1600+ if lhs_scalar && rhs_scalar {
1601+ return Ok ( None ) ;
1602+ }
1603+
1604+ let lhs_node = realize_binary_operand ( lhs, models) ?;
1605+ let rhs_node = realize_binary_operand ( rhs, models) ?;
1606+ let lhs_accelerated = lhs_scalar || !matches ! ( lhs_node. expr, SummaryExpr :: KeepPreAsap ( _) ) ;
1607+ let rhs_accelerated = rhs_scalar || !matches ! ( rhs_node. expr, SummaryExpr :: KeepPreAsap ( _) ) ;
1608+ if !lhs_accelerated || !rhs_accelerated {
1609+ return Ok ( None ) ;
1610+ }
1611+
1612+ Ok ( Some ( Rc :: new ( SummaryNode {
1613+ expr : SummaryExpr :: ExactBinary {
1614+ lhs : lhs_node,
1615+ rhs : rhs_node,
1616+ op : op. clone ( ) ,
1617+ vector_match : vector_match. clone ( ) ,
1618+ } ,
1619+ schema : lift ( & root. output_schema ( ) ?) ,
1620+ guarantee : Some ( ResultGuarantee :: exact ( "ExactBinary" ) ) ,
1621+ } ) ) )
1622+ }
1623+
1624+ fn is_supported_exact_binary ( root : & QueryExpr ) -> bool {
1625+ matches ! (
1626+ root,
1627+ QueryExpr :: BinaryOp {
1628+ op: BinaryOpKind :: Arithmetic ( _) | BinaryOpKind :: Pow ,
1629+ vector_match: None ,
1630+ ..
1631+ }
1632+ )
1633+ }
1634+
1635+ fn is_promql_scalar ( expr : & QueryExpr ) -> bool {
1636+ matches ! (
1637+ expr,
1638+ QueryExpr :: PromqlScalarBridge ( _) | QueryExpr :: Literal ( _)
1639+ )
1640+ }
1641+
1642+ fn realize_binary_operand (
1643+ operand : & Rc < QueryExpr > ,
1644+ models : Models < ' _ > ,
1645+ ) -> Result < Rc < SummaryNode > , ImplementError > {
1646+ if is_promql_scalar ( operand) {
1647+ return Ok ( Rc :: new ( SummaryNode {
1648+ expr : SummaryExpr :: KeepPreAsap ( Rc :: clone ( operand) ) ,
1649+ schema : SummarySchema {
1650+ fields : Vec :: new ( ) ,
1651+ time_index : None ,
1652+ } ,
1653+ guarantee : Some ( ResultGuarantee :: exact ( "PromQL scalar" ) ) ,
1654+ } ) ) ;
1655+ }
1656+ realize_child_with ( operand, models, None )
1657+ }
1658+
15631659/// `intent` with its `AccuracyTarget` replaced by `target` — a no-op for an
15641660/// intent that carries none (see [`accuracy_target`]).
15651661fn override_accuracy ( intent : & AggIntent , target : & AccuracyTarget ) -> AggIntent {
0 commit comments