1- //! Pass 1 local alternatives over the unified logical IR.
1+ //! Stage 1 (logical ASAP-aware optimization), Pass 1: per-computation
2+ //! candidate generation, as defined in
3+ //! `docs/design_docs/proposals/planner-layering.md`.
24//!
3- //! Alternatives are nominal realization descriptors attached to their original
4- //! target, not ranked plans or accuracy certificates. Summary alternatives are
5- //! unsized: Pass 2 may share a summary and tighten it to the strictest consumer,
6- //! so sizing happens later. Workload composition and physical planning consume
7- //! this inventory later; empirical models belong to selection. The legacy search
8- //! API remains until planner cutover.
5+ //! [`generate_per_computation_candidates`] takes the frontends' logical DAGs
6+ //! (`QueryRoot`s) and returns, for every eligible computation, its unranked
7+ //! candidates. A computation ([`ComputationTarget`]) is one measure of an
8+ //! `Aggregate` whose intent has a realization besides plain exact execution;
9+ //! nested aggregates and operators read by scalar roots are found too.
10+ //! Pass 1 does not consider sharing across computations; that is Pass 2.
11+ //!
12+ //! - Each target has exactly one exact option: the exact accumulator
13+ //! (`ExactAccumulator`, or `AvgAsSumCount` for `avg`) when it can be built,
14+ //! otherwise plain `Exact`. Whether it is materialized is decided in Stage 2.
15+ //! - Summary candidates are unsized; they are sized after Pass 2 sharing.
16+ //! - Candidates are pruned only when provably unable to meet the accuracy
17+ //! requirement, and each pruned candidate keeps its [`PruneReason`].
18+ //! - A measure filter or `HAVING` prunes nothing. The target records the
19+ //! measure filter (applied as `SummaryAgg.filter`) and one [`HavingEntry`]
20+ //! per consumer `Filter` directly above the aggregate: a key-only predicate
21+ //! applied by every consumer may go below the aggregate, anything else stays
22+ //! above the output (approximate outputs then get guarantee `None`).
23+ //!
24+ //! The input must be a logical DAG: ASAP operators, execution timing or a
25+ //! materialization are rejected. No cost or accuracy model is used; ranking
26+ //! and discarding valid candidates belong to Stage 3.
927use std:: collections:: { HashMap , HashSet } ;
1028use std:: rc:: Rc ;
1129
@@ -18,14 +36,14 @@ use thiserror::Error;
1836
1937use crate :: replacement:: { accuracy_budget, accuracy_target, summary_candidates} ;
2038
21- /// One local realization of a target measure.
39+ /// One Pass 1 candidate for a computation (one measure of an aggregate) .
2240///
2341/// Each target has exactly one exact option: `ExactAccumulator` or
2442/// `AvgAsSumCount` when the intent has one and it can be built, otherwise
2543/// `Exact`. The plain form would duplicate "accumulator, not materialized";
2644/// Stage 2/3 decide materialization and sharing.
2745#[ derive( Debug , Clone , PartialEq ) ]
28- pub enum LocalCandidate {
46+ pub enum ComputationCandidate {
2947 /// Plain exact execution of the original sub-DAG; the exact option only
3048 /// when the intent has no exact accumulator form or that form is pruned.
3149 Exact ,
@@ -51,31 +69,31 @@ pub enum PruneReason {
5169
5270/// Unranked candidates of one measure and the candidates pruned for it.
5371#[ derive( Debug , Clone , PartialEq ) ]
54- pub struct LocalChoices {
55- pub alternatives : Vec < LocalCandidate > ,
56- pub pruned : Vec < ( LocalCandidate , PruneReason ) > ,
72+ pub struct ComputationChoices {
73+ pub candidates : Vec < ComputationCandidate > ,
74+ pub pruned : Vec < ( ComputationCandidate , PruneReason ) > ,
5775}
5876
59- impl LocalChoices {
60- /// Prune matching candidates; a pruned exact accumulator form falls back to
77+ impl ComputationChoices {
78+ /// Prune matching candidates; a pruned exact accumulator form is replaced by
6179 /// plain `Exact`, so the target keeps one exact option.
62- fn prune ( & mut self , reason : PruneReason , applies : impl Fn ( & LocalCandidate ) -> bool ) {
63- let ( pruned, kept) : ( Vec < _ > , _ ) = std:: mem:: take ( & mut self . alternatives )
80+ fn prune ( & mut self , reason : PruneReason , applies : impl Fn ( & ComputationCandidate ) -> bool ) {
81+ let ( pruned, kept) : ( Vec < _ > , _ ) = std:: mem:: take ( & mut self . candidates )
6482 . into_iter ( )
65- . partition ( |choice| * choice != LocalCandidate :: Exact && applies ( choice) ) ;
66- self . alternatives = kept;
83+ . partition ( |choice| * choice != ComputationCandidate :: Exact && applies ( choice) ) ;
84+ self . candidates = kept;
6785 if pruned. iter ( ) . any ( |choice| {
6886 matches ! (
6987 choice,
70- LocalCandidate :: ExactAccumulator { .. } | LocalCandidate :: AvgAsSumCount
88+ ComputationCandidate :: ExactAccumulator { .. } | ComputationCandidate :: AvgAsSumCount
7189 )
7290 } ) {
73- self . alternatives . insert ( 0 , LocalCandidate :: Exact ) ;
91+ self . candidates . insert ( 0 , ComputationCandidate :: Exact ) ;
7492 }
7593 self . pruned . extend (
7694 pruned
7795 . into_iter ( )
78- . map ( |choice : LocalCandidate | ( choice, reason) ) ,
96+ . map ( |choice : ComputationCandidate | ( choice, reason) ) ,
7997 ) ;
8098 }
8199}
@@ -113,11 +131,11 @@ pub struct HavingEntry {
113131/// execution. The target retains source, grouping, filters, input expressions
114132/// and evaluation context.
115133#[ derive( Debug , Clone ) ]
116- pub struct LocalLogicalTarget {
134+ pub struct ComputationTarget {
117135 pub target : Rc < OperatorNode > ,
118136 /// Index into the aggregate's measures.
119137 pub measure : usize ,
120- pub choices : LocalChoices ,
138+ pub choices : ComputationChoices ,
121139 /// The measure's own filter; only rows that pass it update the state
122140 /// (`SummaryAgg.filter`).
123141 pub filter : Option < Predicate > ,
@@ -128,21 +146,21 @@ pub struct LocalLogicalTarget {
128146
129147/// Compact Pass 1 inventory; roots and nested producer dependencies are retained.
130148#[ derive( Debug , Clone ) ]
131- pub struct LocalLogicalCandidates < Id > {
149+ pub struct PerComputationCandidates < Id > {
132150 pub roots : Vec < ( Id , QueryRoot ) > ,
133- pub targets : Vec < LocalLogicalTarget > ,
151+ pub targets : Vec < ComputationTarget > ,
134152}
135153
136154#[ derive( Debug , Error ) ]
137- pub enum LogicalCandidateError {
155+ pub enum PerComputationCandidateError {
138156 #[ error( transparent) ]
139157 Structure ( #[ from] SchemaDerivationError ) ,
140158 #[ error( "logical candidate input already has assigned execution timing or materialization" ) ]
141159 AssignedPhysicalAnnotation ,
142160 #[ error( "logical candidate input already contains ASAP operators" ) ]
143161 ContainsAsap ,
144162 /// `root` (the root id's `Debug` form) and `measure` locate the target;
145- /// [`local_candidates_for_intent `] alone leaves them `None`.
163+ /// [`computation_candidates_for_intent `] alone leaves them `None`.
146164 #[ error(
147165 "approximate accuracy of root {}, measure {} requires epsilon and delta in (0, 1)" ,
148166 root. as_deref( ) . unwrap_or( "?" ) ,
@@ -158,9 +176,9 @@ pub enum LogicalCandidateError {
158176/// order, without ranking or empirical assessment. A summary is not a claim that
159177/// a deployment meets the measure's accuracy requirement. The one exact option
160178/// comes first: the exact accumulator form if the intent has one, else `Exact`.
161- pub fn local_candidates_for_intent (
179+ pub fn computation_candidates_for_intent (
162180 intent : & AggIntent ,
163- ) -> Result < LocalChoices , LogicalCandidateError > {
181+ ) -> Result < ComputationChoices , PerComputationCandidateError > {
164182 let exact = match intent {
165183 AggIntent :: Count { .. } => Some ( ( ExactKind :: Count , ExactParams :: Count ) ) ,
166184 AggIntent :: Sum { .. } => Some ( ( ExactKind :: Sum , ExactParams :: Sum ) ) ,
@@ -172,20 +190,20 @@ pub fn local_candidates_for_intent(
172190 _ => None ,
173191 } ;
174192 let exact = match exact {
175- Some ( ( kind, params) ) => LocalCandidate :: ExactAccumulator { kind, params } ,
176- None if matches ! ( intent, AggIntent :: Avg { .. } ) => LocalCandidate :: AvgAsSumCount ,
177- None => LocalCandidate :: Exact ,
193+ Some ( ( kind, params) ) => ComputationCandidate :: ExactAccumulator { kind, params } ,
194+ None if matches ! ( intent, AggIntent :: Avg { .. } ) => ComputationCandidate :: AvgAsSumCount ,
195+ None => ComputationCandidate :: Exact ,
178196 } ;
179- let mut choices = LocalChoices {
180- alternatives : vec ! [ exact] ,
197+ let mut choices = ComputationChoices {
198+ candidates : vec ! [ exact] ,
181199 pruned : vec ! [ ] ,
182200 } ;
183201 if let Some ( target) = accuracy_target ( intent) {
184202 if * target != AccuracyTarget :: Exact {
185203 let ( epsilon, delta) = accuracy_budget ( target) ;
186204 let unit = |value : f64 | value > 0.0 && value < 1.0 ;
187205 if !unit ( epsilon) || !unit ( delta) {
188- return Err ( LogicalCandidateError :: InvalidAccuracy {
206+ return Err ( PerComputationCandidateError :: InvalidAccuracy {
189207 root : None ,
190208 measure : None ,
191209 } ) ;
@@ -195,14 +213,14 @@ pub fn local_candidates_for_intent(
195213 if * target == AccuracyTarget :: Exact {
196214 choices. pruned . extend ( summaries. map ( |algorithm| {
197215 (
198- LocalCandidate :: Summary ( algorithm) ,
216+ ComputationCandidate :: Summary ( algorithm) ,
199217 PruneReason :: RequiresExact ,
200218 )
201219 } ) ) ;
202220 } else {
203221 choices
204- . alternatives
205- . extend ( summaries. map ( LocalCandidate :: Summary ) ) ;
222+ . candidates
223+ . extend ( summaries. map ( ComputationCandidate :: Summary ) ) ;
206224 }
207225 }
208226 Ok ( choices)
@@ -254,9 +272,9 @@ fn placement(below: bool) -> HavingPlacement {
254272/// and `HAVING` keep every candidate; the target records how they are applied.
255273/// A consumer `Filter`'s key-only `HAVING` is pushed below only when every
256274/// consumer of the aggregate applies the same predicate.
257- pub fn enumerate_local_logical_candidates < Id : std:: fmt:: Debug > (
275+ pub fn generate_per_computation_candidates < Id : std:: fmt:: Debug > (
258276 roots : Vec < ( Id , QueryRoot ) > ,
259- ) -> Result < LocalLogicalCandidates < Id > , LogicalCandidateError > {
277+ ) -> Result < PerComputationCandidates < Id > , PerComputationCandidateError > {
260278 let mut seen = HashSet :: new ( ) ;
261279 // Each aggregate with the first root that reaches it.
262280 let mut aggregates = Vec :: new ( ) ;
@@ -271,7 +289,7 @@ pub fn enumerate_local_logical_candidates<Id: std::fmt::Debug>(
271289 } ;
272290 for root in operators {
273291 if root. contains_asap ( ) {
274- return Err ( LogicalCandidateError :: ContainsAsap ) ;
292+ return Err ( PerComputationCandidateError :: ContainsAsap ) ;
275293 }
276294 consumers. entry ( Rc :: as_ptr ( root) ) . or_default ( ) . push ( None ) ;
277295 for node in OperatorNode :: reachable ( root) {
@@ -280,7 +298,7 @@ pub fn enumerate_local_logical_candidates<Id: std::fmt::Debug>(
280298 }
281299 if node. timing . is_some ( ) || node. materialization != Materialization :: NotMaterialized
282300 {
283- return Err ( LogicalCandidateError :: AssignedPhysicalAnnotation ) ;
301+ return Err ( PerComputationCandidateError :: AssignedPhysicalAnnotation ) ;
284302 }
285303 for child in node. children ( ) {
286304 consumers
@@ -338,24 +356,25 @@ pub fn enumerate_local_logical_candidates<Id: std::fmt::Debug>(
338356 } ) ;
339357 }
340358 for ( measure, intent) in measures. iter ( ) . enumerate ( ) {
341- let mut choices = local_candidates_for_intent ( intent) . map_err ( |error| match error {
342- LogicalCandidateError :: InvalidAccuracy { .. } => {
343- LogicalCandidateError :: InvalidAccuracy {
344- root : Some ( root. clone ( ) ) ,
345- measure : Some ( measure) ,
359+ let mut choices =
360+ computation_candidates_for_intent ( intent) . map_err ( |error| match error {
361+ PerComputationCandidateError :: InvalidAccuracy { .. } => {
362+ PerComputationCandidateError :: InvalidAccuracy {
363+ root : Some ( root. clone ( ) ) ,
364+ measure : Some ( measure) ,
365+ }
346366 }
347- }
348- error => error,
349- } ) ?;
350- if choices. alternatives == [ LocalCandidate :: Exact ] && choices. pruned . is_empty ( ) {
367+ error => error,
368+ } ) ?;
369+ if choices. candidates == [ ComputationCandidate :: Exact ] && choices. pruned . is_empty ( ) {
351370 continue ;
352371 }
353372 if matches ! ( intent, AggIntent :: Avg { .. } ) && nullable_avg_input ( & node, intent) {
354373 choices. prune ( PruneReason :: NullableAvgInput , |choice| {
355- * choice == LocalCandidate :: AvgAsSumCount
374+ * choice == ComputationCandidate :: AvgAsSumCount
356375 } ) ;
357376 }
358- targets. push ( LocalLogicalTarget {
377+ targets. push ( ComputationTarget {
359378 target : node. clone ( ) ,
360379 measure,
361380 choices,
@@ -364,7 +383,7 @@ pub fn enumerate_local_logical_candidates<Id: std::fmt::Debug>(
364383 } ) ;
365384 }
366385 }
367- Ok ( LocalLogicalCandidates { roots, targets } )
386+ Ok ( PerComputationCandidates { roots, targets } )
368387}
369388
370389#[ cfg( test) ]
@@ -374,22 +393,22 @@ mod tests {
374393 /// universal summary; plain exact execution is not offered.
375394 #[ test]
376395 fn approximate_count_keeps_one_exact_accumulator_and_universal_choices ( ) {
377- let choices = local_candidates_for_intent ( & AggIntent :: Count {
396+ let choices = computation_candidates_for_intent ( & AggIntent :: Count {
378397 accuracy : AccuracyTarget :: EpsilonDelta {
379398 epsilon : 0.05 ,
380399 delta : 0.01 ,
381400 } ,
382401 } )
383402 . unwrap ( )
384- . alternatives ;
385- assert ! ( !choices. contains( & LocalCandidate :: Exact ) ) ;
403+ . candidates ;
404+ assert ! ( !choices. contains( & ComputationCandidate :: Exact ) ) ;
386405 assert_eq ! (
387406 choices[ 0 ] ,
388- LocalCandidate :: ExactAccumulator {
407+ ComputationCandidate :: ExactAccumulator {
389408 kind: ExactKind :: Count ,
390409 params: ExactParams :: Count
391410 }
392411 ) ;
393- assert ! ( choices. contains( & LocalCandidate :: Summary ( SketchAlgorithm :: UnivMon ) ) ) ;
412+ assert ! ( choices. contains( & ComputationCandidate :: Summary ( SketchAlgorithm :: UnivMon ) ) ) ;
394413 }
395414}
0 commit comments