11//! Evidence-backed physical-plan costing at the planner selection boundary.
22
3- use std:: rc:: Rc ;
3+ use std:: { cell :: RefCell , rc:: Rc } ;
44
55use asap_types:: post_asap:: { SketchAlgorithm , SummaryExpr , SummaryNode } ;
6- use asap_types:: pre_asap:: AggIntent ;
6+ use asap_types:: pre_asap:: { AggIntent , QueryExpr } ;
77
88use crate :: analytical_cost:: {
99 estimate_physical_dag_comparison, AnalyticalCostError , PhysicalDagComparisonEstimate ,
@@ -17,6 +17,18 @@ use crate::analytical_statistics::ComparisonScope;
1717use crate :: cost_model:: { Cost , CostModel , DefaultCostModel } ;
1818use crate :: replacement:: { Replacement , ReplacementSubDAG , TargetSubDAG } ;
1919
20+ /// One immutable generation of deployment evidence for a planner target.
21+ ///
22+ /// The opaque version lets a provider bind every subsequent query and summary
23+ /// lookup to the same catalog/runtime snapshot. A cost-model instance retains
24+ /// this value for the target, so a caller that needs fresher evidence creates a
25+ /// new model instead of mixing generations in one ranking decision.
26+ #[ derive( Debug , Clone , PartialEq ) ]
27+ pub struct PlannerEvidenceSnapshot {
28+ pub version : String ,
29+ pub scope : ComparisonScope ,
30+ }
31+
2032/// Deployment evidence needed to price one planner alternative.
2133///
2234/// The planner lowers raw queries and logical rewrites itself. A deployment
@@ -26,21 +38,23 @@ use crate::replacement::{Replacement, ReplacementSubDAG, TargetSubDAG};
2638/// choices; that binder must return the complete summary DAG, including any
2739/// embedded `KeepPreAsap` work.
2840pub trait PlannerPhysicalPlanProvider {
29- fn comparison_scope (
41+ /// Atomically captures the comparison scope and evidence generation.
42+ fn capture_evidence_snapshot (
3043 & self ,
3144 target : & TargetSubDAG < ' _ > ,
32- ) -> Result < ComparisonScope , AnalyticalCostError > ;
45+ ) -> Result < PlannerEvidenceSnapshot , AnalyticalCostError > ;
3346
3447 fn query_node_evidence (
3548 & self ,
49+ snapshot : & PlannerEvidenceSnapshot ,
3650 request : PhysicalNodeRequest < ' _ > ,
3751 ) -> Result < PhysicalNodeEvidence , AnalyticalCostError > ;
3852
3953 fn summary_physical_dag (
4054 & self ,
55+ snapshot : & PlannerEvidenceSnapshot ,
4156 summary : & Rc < SummaryNode > ,
4257 target : & TargetSubDAG < ' _ > ,
43- scope : & ComparisonScope ,
4458 ) -> Result < PhysicalDag , AnalyticalCostError > ;
4559}
4660
@@ -61,6 +75,14 @@ pub struct PlannerCandidateEstimate {
6175pub struct AnalyticalPlannerCostModel < ' a > {
6276 provider : & ' a dyn PlannerPhysicalPlanProvider ,
6377 calibration : ResourceCalibration ,
78+ target_evidence : RefCell < Vec < CachedTargetEvidence > > ,
79+ }
80+
81+ struct CachedTargetEvidence {
82+ root : Rc < QueryExpr > ,
83+ consumer_count : usize ,
84+ snapshot : PlannerEvidenceSnapshot ,
85+ raw : PhysicalDag ,
6486}
6587
6688impl < ' a > AnalyticalPlannerCostModel < ' a > {
@@ -72,49 +94,76 @@ impl<'a> AnalyticalPlannerCostModel<'a> {
7294 Ok ( Self {
7395 provider,
7496 calibration,
97+ target_evidence : RefCell :: new ( Vec :: new ( ) ) ,
7598 } )
7699 }
77100
101+ fn target_evidence (
102+ & self ,
103+ target : & TargetSubDAG < ' _ > ,
104+ ) -> Result < ( PlannerEvidenceSnapshot , PhysicalDag ) , AnalyticalCostError > {
105+ if let Some ( cached) = self . target_evidence . borrow ( ) . iter ( ) . find ( |cached| {
106+ Rc :: ptr_eq ( & cached. root , target. root ) && cached. consumer_count == target. consumer_count
107+ } ) {
108+ return Ok ( ( cached. snapshot . clone ( ) , cached. raw . clone ( ) ) ) ;
109+ }
110+
111+ let snapshot = self . provider . capture_evidence_snapshot ( target) ?;
112+ if snapshot. version . is_empty ( ) {
113+ return Err ( AnalyticalCostError :: MissingOrStale (
114+ "planner_evidence_snapshot.version" ,
115+ ) ) ;
116+ }
117+ snapshot. scope . validate ( ) ?;
118+ let evidence = QueryEvidence {
119+ provider : self . provider ,
120+ snapshot : & snapshot,
121+ } ;
122+ let raw = lower_query_physical_dag ( target. root , & snapshot. scope , & evidence) ?;
123+ self . target_evidence
124+ . borrow_mut ( )
125+ . push ( CachedTargetEvidence {
126+ root : Rc :: clone ( target. root ) ,
127+ consumer_count : target. consumer_count ,
128+ snapshot : snapshot. clone ( ) ,
129+ raw : raw. clone ( ) ,
130+ } ) ;
131+ Ok ( ( snapshot, raw) )
132+ }
133+
78134 pub fn estimate_candidate (
79135 & self ,
80136 candidate : & ReplacementSubDAG ,
81137 target : & TargetSubDAG < ' _ > ,
82138 ) -> Result < PlannerCandidateEstimate , AnalyticalCostError > {
83- struct QueryEvidence < ' a > ( & ' a dyn PlannerPhysicalPlanProvider ) ;
84- impl PhysicalNodeEvidenceProvider for QueryEvidence < ' _ > {
85- fn evidence (
86- & self ,
87- request : PhysicalNodeRequest < ' _ > ,
88- ) -> Result < PhysicalNodeEvidence , AnalyticalCostError > {
89- self . 0 . query_node_evidence ( request)
90- }
91- }
92-
93- let scope = self . provider . comparison_scope ( target) ?;
94- let evidence = QueryEvidence ( self . provider ) ;
95- let raw = lower_query_physical_dag ( target. root , & scope, & evidence) ?;
139+ let ( snapshot, raw) = self . target_evidence ( target) ?;
140+ let scope = & snapshot. scope ;
141+ let evidence = QueryEvidence {
142+ provider : self . provider ,
143+ snapshot : & snapshot,
144+ } ;
96145 let replacement = match & candidate. replacement {
97- Replacement :: Rewrite ( query) => lower_query_physical_dag ( query, & scope, & evidence) ?,
146+ Replacement :: Rewrite ( query) => lower_query_physical_dag ( query, scope, & evidence) ?,
98147 Replacement :: Summary ( summary) => match & summary. expr {
99148 SummaryExpr :: KeepPreAsap ( query) => {
100- lower_query_physical_dag ( query, & scope, & evidence) ?
149+ lower_query_physical_dag ( query, scope, & evidence) ?
101150 }
102151 _ => self
103152 . provider
104- . summary_physical_dag ( summary , target , & scope ) ?,
153+ . summary_physical_dag ( & snapshot , summary , target ) ?,
105154 } ,
106155 } ;
107156 let resources = estimate_physical_dag_comparison (
108157 PhysicalDagEstimateRequest {
109158 nodes : & raw. nodes ,
110159 root : & raw. root ,
111- scope : & scope ,
160+ scope,
112161 statistics : & raw,
113162 } ,
114163 PhysicalDagEstimateRequest {
115164 nodes : & replacement. nodes ,
116165 root : & replacement. root ,
117- scope : & scope ,
166+ scope,
118167 statistics : & replacement,
119168 } ,
120169 ) ?;
@@ -128,6 +177,20 @@ impl<'a> AnalyticalPlannerCostModel<'a> {
128177 }
129178}
130179
180+ struct QueryEvidence < ' a > {
181+ provider : & ' a dyn PlannerPhysicalPlanProvider ,
182+ snapshot : & ' a PlannerEvidenceSnapshot ,
183+ }
184+
185+ impl PhysicalNodeEvidenceProvider for QueryEvidence < ' _ > {
186+ fn evidence (
187+ & self ,
188+ request : PhysicalNodeRequest < ' _ > ,
189+ ) -> Result < PhysicalNodeEvidence , AnalyticalCostError > {
190+ self . provider . query_node_evidence ( self . snapshot , request)
191+ }
192+ }
193+
131194impl CostModel for AnalyticalPlannerCostModel < ' _ > {
132195 fn candidate_cost_covers_complete_plan ( & self ) -> bool {
133196 true
@@ -163,6 +226,7 @@ impl CostModel for AnalyticalPlannerCostModel<'_> {
163226#[ cfg( test) ]
164227mod tests {
165228 use super :: * ;
229+ use std:: cell:: Cell ;
166230 use std:: collections:: HashMap ;
167231
168232 use asap_types:: pre_asap:: { Column , DataType , QueryExpr , Reduction , Schema , Source } ;
@@ -244,9 +308,20 @@ mod tests {
244308 struct TestProvider {
245309 summary_available : bool ,
246310 candidate_scan_bytes : u64 ,
311+ snapshot_calls : Cell < u64 > ,
312+ raw_evidence_calls : Cell < u64 > ,
247313 }
248314
249315 impl TestProvider {
316+ fn new ( summary_available : bool , candidate_scan_bytes : u64 ) -> Self {
317+ Self {
318+ summary_available,
319+ candidate_scan_bytes,
320+ snapshot_calls : Cell :: new ( 0 ) ,
321+ raw_evidence_calls : Cell :: new ( 0 ) ,
322+ }
323+ }
324+
250325 fn summary_dag ( & self , scope : & ComparisonScope ) -> PhysicalDag {
251326 let scan_statistics = statistics (
252327 self . candidate_scan_bytes ,
@@ -321,17 +396,25 @@ mod tests {
321396 }
322397
323398 impl PlannerPhysicalPlanProvider for TestProvider {
324- fn comparison_scope (
399+ fn capture_evidence_snapshot (
325400 & self ,
326401 _target : & TargetSubDAG < ' _ > ,
327- ) -> Result < ComparisonScope , AnalyticalCostError > {
328- Ok ( scope ( ) )
402+ ) -> Result < PlannerEvidenceSnapshot , AnalyticalCostError > {
403+ self . snapshot_calls . set ( self . snapshot_calls . get ( ) + 1 ) ;
404+ Ok ( PlannerEvidenceSnapshot {
405+ version : "test-snapshot-1" . into ( ) ,
406+ scope : scope ( ) ,
407+ } )
329408 }
330409
331410 fn query_node_evidence (
332411 & self ,
412+ snapshot : & PlannerEvidenceSnapshot ,
333413 request : PhysicalNodeRequest < ' _ > ,
334414 ) -> Result < PhysicalNodeEvidence , AnalyticalCostError > {
415+ assert_eq ! ( snapshot. version, "test-snapshot-1" ) ;
416+ self . raw_evidence_calls
417+ . set ( self . raw_evidence_calls . get ( ) + 1 ) ;
335418 let ( physical_id, mut statistics) = match request. operator {
336419 PhysicalOperator :: Scan => (
337420 "raw-scan" ,
@@ -357,12 +440,13 @@ mod tests {
357440
358441 fn summary_physical_dag (
359442 & self ,
443+ snapshot : & PlannerEvidenceSnapshot ,
360444 _summary : & Rc < SummaryNode > ,
361445 _target : & TargetSubDAG < ' _ > ,
362- scope : & ComparisonScope ,
363446 ) -> Result < PhysicalDag , AnalyticalCostError > {
447+ assert_eq ! ( snapshot. version, "test-snapshot-1" ) ;
364448 self . summary_available
365- . then ( || self . summary_dag ( scope) )
449+ . then ( || self . summary_dag ( & snapshot . scope ) )
366450 . ok_or ( AnalyticalCostError :: MissingOrStale ( "summary_physical_plan" ) )
367451 }
368452 }
@@ -384,10 +468,7 @@ mod tests {
384468 & crate :: replacement:: default_strategies ( ) ,
385469 ) ;
386470 let planned_root = Rc :: clone ( & space. roots [ 0 ] . 1 ) ;
387- let provider = TestProvider {
388- summary_available : true ,
389- candidate_scan_bytes : 800 ,
390- } ;
471+ let provider = TestProvider :: new ( true , 800 ) ;
391472 let model = AnalyticalPlannerCostModel :: new ( & provider, calibration ( ) ) . unwrap ( ) ;
392473
393474 let selected = space. global_selection ( & model) ;
@@ -405,10 +486,7 @@ mod tests {
405486 & crate :: replacement:: default_strategies ( ) ,
406487 ) ;
407488 let planned_root = Rc :: clone ( & space. roots [ 0 ] . 1 ) ;
408- let provider = TestProvider {
409- summary_available : false ,
410- candidate_scan_bytes : 800 ,
411- } ;
489+ let provider = TestProvider :: new ( false , 800 ) ;
412490 let model = AnalyticalPlannerCostModel :: new ( & provider, calibration ( ) ) . unwrap ( ) ;
413491
414492 let selected = space. global_selection ( & model) ;
@@ -422,27 +500,28 @@ mod tests {
422500 fn candidate_with_incomplete_scope_is_unavailable ( ) {
423501 struct WrongScope ( TestProvider ) ;
424502 impl PlannerPhysicalPlanProvider for WrongScope {
425- fn comparison_scope (
503+ fn capture_evidence_snapshot (
426504 & self ,
427505 target : & TargetSubDAG < ' _ > ,
428- ) -> Result < ComparisonScope , AnalyticalCostError > {
429- self . 0 . comparison_scope ( target)
506+ ) -> Result < PlannerEvidenceSnapshot , AnalyticalCostError > {
507+ self . 0 . capture_evidence_snapshot ( target)
430508 }
431509
432510 fn query_node_evidence (
433511 & self ,
512+ snapshot : & PlannerEvidenceSnapshot ,
434513 request : PhysicalNodeRequest < ' _ > ,
435514 ) -> Result < PhysicalNodeEvidence , AnalyticalCostError > {
436- self . 0 . query_node_evidence ( request)
515+ self . 0 . query_node_evidence ( snapshot , request)
437516 }
438517
439518 fn summary_physical_dag (
440519 & self ,
520+ snapshot : & PlannerEvidenceSnapshot ,
441521 summary : & Rc < SummaryNode > ,
442522 target : & TargetSubDAG < ' _ > ,
443- scope : & ComparisonScope ,
444523 ) -> Result < PhysicalDag , AnalyticalCostError > {
445- let mut dag = self . 0 . summary_physical_dag ( summary , target , scope ) ?;
524+ let mut dag = self . 0 . summary_physical_dag ( snapshot , summary , target ) ?;
446525 dag. nodes [ 0 ] . source_coverage . as_mut ( ) . unwrap ( ) . snapshot_id = "other" . into ( ) ;
447526 Ok ( dag)
448527 }
@@ -451,10 +530,7 @@ mod tests {
451530 let root = query ( ) ;
452531 let candidates = crate :: replacement:: SketchAlgorithmStrategy :: default_cost_model ( )
453532 . replacements ( & TargetSubDAG :: new ( & root) ) ;
454- let provider = WrongScope ( TestProvider {
455- summary_available : true ,
456- candidate_scan_bytes : 800 ,
457- } ) ;
533+ let provider = WrongScope ( TestProvider :: new ( true , 800 ) ) ;
458534 let model = AnalyticalPlannerCostModel :: new ( & provider, calibration ( ) ) . unwrap ( ) ;
459535 assert_eq ! (
460536 model. candidate_cost( & candidates[ 0 ] , & TargetSubDAG :: new( & root) ) ,
@@ -470,13 +546,31 @@ mod tests {
470546 & crate :: replacement:: default_strategies ( ) ,
471547 ) ;
472548 let planned_root = Rc :: clone ( & space. roots [ 0 ] . 1 ) ;
473- let provider = TestProvider {
474- summary_available : true ,
475- candidate_scan_bytes : 100_000 ,
476- } ;
549+ let provider = TestProvider :: new ( true , 100_000 ) ;
477550 let model = AnalyticalPlannerCostModel :: new ( & provider, calibration ( ) ) . unwrap ( ) ;
478551
479552 let selected = space. global_selection ( & model) ;
480553 assert ! ( selected. for_target( & planned_root) . unwrap( ) . chosen. is_none( ) ) ;
481554 }
555+
556+ #[ test]
557+ fn sibling_candidates_share_one_scope_and_raw_baseline ( ) {
558+ let root = query ( ) ;
559+ let candidates = crate :: replacement:: SketchAlgorithmStrategy :: default_cost_model ( )
560+ . replacements ( & TargetSubDAG :: new ( & root) ) ;
561+ assert ! ( candidates. len( ) >= 2 ) ;
562+ let provider = TestProvider :: new ( true , 800 ) ;
563+ let model = AnalyticalPlannerCostModel :: new ( & provider, calibration ( ) ) . unwrap ( ) ;
564+ let target = TargetSubDAG :: new ( & root) ;
565+
566+ model. estimate_candidate ( & candidates[ 0 ] , & target) . unwrap ( ) ;
567+ model. estimate_candidate ( & candidates[ 1 ] , & target) . unwrap ( ) ;
568+
569+ assert_eq ! ( provider. snapshot_calls. get( ) , 1 ) ;
570+ assert_eq ! (
571+ provider. raw_evidence_calls. get( ) ,
572+ 2 ,
573+ "the scan and aggregate evidence for the raw baseline are captured once"
574+ ) ;
575+ }
482576}
0 commit comments