@@ -27,7 +27,6 @@ pub struct RawDagProgram {
2727 pub input : SummaryUpdate ,
2828 pub grouping : GroupingStrategy ,
2929 pub reduction : planner_types:: pre_asap:: Reduction ,
30- projected_column : Option < String > ,
3130 /// Planner's encoded precompute graph from the raw sample boundary to this
3231 /// output. Decoded graphs are not `Send`, so each execution decodes it.
3332 program : std:: sync:: Arc < [ u8 ] > ,
@@ -197,12 +196,7 @@ impl RawDagProgram {
197196 input : input. clone ( ) ,
198197 grouping : grouping. clone ( ) ,
199198 reduction : reduction. clone ( ) ,
200- projected_column : config
201- . effective_value_projection ( )
202- . column ( )
203- . map ( str:: to_owned) ,
204199 } ;
205- program. validate ( ) ?;
206200 if let Some ( old) = & selected {
207201 if old. family != program. family
208202 || old. input != program. input
@@ -222,6 +216,16 @@ impl RawDagProgram {
222216 selected. ok_or_else ( || "raw materialization has no selected post-ASAP DAG producer" . into ( ) )
223217 }
224218
219+ /// Heap readout decodes only the backend heap kernel, not Planner's weighted
220+ /// frequency state, so heaps stay on that kernel until it does.
221+ fn heap ( & self ) -> bool {
222+ matches ! ( & self . family, SummaryFamilyType :: Sketch ( kind, _) if matches!(
223+ kind. algorithm( ) ,
224+ planner_types:: post_asap:: SketchAlgorithm :: CmsWithHeap
225+ | planner_types:: post_asap:: SketchAlgorithm :: CountSketchWithHeap
226+ ) )
227+ }
228+
225229 /// Execute the Planner graph over one pane's samples as one typed batch.
226230 /// Returns `None` when the graph admits no population from them.
227231 pub fn build < ' a > (
@@ -230,21 +234,67 @@ impl RawDagProgram {
230234 pane : ( i64 , i64 ) ,
231235 max_bytes : usize ,
232236 ) -> Result < Option < Box < dyn AggregateCore > > , String > {
233- use futures:: StreamExt ;
234237 let samples = pane_batch ( samples) ;
235- // Stored heap readout decodes only the backend heap kernel, not
236- // Planner's weighted frequency state, so heaps stay on that kernel.
237- if matches ! ( & self . family, SummaryFamilyType :: Sketch ( kind, _) if matches!(
238- kind. algorithm( ) ,
239- planner_types:: post_asap:: SketchAlgorithm :: CmsWithHeap
240- | planner_types:: post_asap:: SketchAlgorithm :: CountSketchWithHeap
241- ) ) {
242- let mut updater = self . updater ( ) ?;
238+ if self . heap ( ) {
239+ let mut updater = self . heap_updater ( ) ?;
243240 for ( series, time, value) in samples {
244241 self . apply ( & mut * updater, series, value, time) ?;
245242 }
246243 return Ok ( Some ( updater. take_accumulator ( ) ) ) ;
247244 }
245+ // The router assigns one population per group, so one pane yields
246+ // at most one state.
247+ match self . execute ( samples, pane, max_bytes) ?. as_slice ( ) {
248+ [ ] => Ok ( None ) ,
249+ [ row] => match row. as_slice ( ) {
250+ [ _, _, Value :: Summary { state, .. } ] => {
251+ asap_summary_state:: physical:: from_physical ( state. as_ref ( ) )
252+ . map ( Some )
253+ . map_err ( |e| e. to_string ( ) )
254+ }
255+ _ => Err ( "raw precompute output is not a population state" . into ( ) ) ,
256+ } ,
257+ _ => Err ( "one routed group produced several populations" . into ( ) ) ,
258+ }
259+ }
260+
261+ /// Check that the Planner graph admits every sample, without storing a result.
262+ pub fn validate < ' a > (
263+ & self ,
264+ samples : impl IntoIterator < Item = ( & ' a str , i64 , f64 ) > ,
265+ max_bytes : usize ,
266+ ) -> Result < ( ) , String > {
267+ let samples = pane_batch ( samples) ;
268+ if self . heap ( ) {
269+ let updater = self . heap_updater ( ) ?;
270+ for ( _, _, value) in samples {
271+ self . validate_sample ( updater. as_ref ( ) , value) ?;
272+ }
273+ return Ok ( ( ) ) ;
274+ }
275+ if samples. is_empty ( ) {
276+ return Ok ( ( ) ) ;
277+ }
278+ let bounds = samples
279+ . iter ( )
280+ . fold ( ( i64:: MAX , i64:: MIN ) , |( lo, hi) , ( _, time, _) | {
281+ ( lo. min ( * time) , hi. max ( time. saturating_add ( 1 ) ) )
282+ } ) ;
283+ self . execute ( samples, bounds, max_bytes) . map ( |_| ( ) )
284+ }
285+
286+ /// The family's empty state, for a pane known to have no samples.
287+ pub fn empty_state ( & self ) -> Result < Box < dyn AggregateCore > , String > {
288+ Ok ( self . heap_updater ( ) ?. take_accumulator ( ) )
289+ }
290+
291+ fn execute < ' a > (
292+ & self ,
293+ samples : impl IntoIterator < Item = ( & ' a str , i64 , f64 ) > ,
294+ pane : ( i64 , i64 ) ,
295+ max_bytes : usize ,
296+ ) -> Result < Vec < Vec < Value > > , String > {
297+ use futures:: StreamExt ;
248298 let schema = precompute:: raw_sample_schema ( ) ;
249299 let rows = samples
250300 . into_iter ( )
@@ -272,7 +322,7 @@ impl RawDagProgram {
272322 } ,
273323 )
274324 . map_err ( |e| e. to_string ( ) ) ?;
275- let rows = futures:: executor:: block_on ( async {
325+ futures:: executor:: block_on ( async {
276326 let mut stream = graph. execute ( program. roots ( ) , context) ?. pop ( ) . ok_or (
277327 asap_physical_operators:: Error :: Invalid ( "missing output" . into ( ) ) ,
278328 ) ?;
@@ -282,58 +332,14 @@ impl RawDagProgram {
282332 }
283333 Ok :: < _ , asap_physical_operators:: Error > ( rows)
284334 } )
285- . map_err ( |e| e. to_string ( ) ) ?;
286- // The router assigns one population per group, so one pane yields
287- // at most one state.
288- match rows. as_slice ( ) {
289- [ ] => Ok ( None ) ,
290- [ row] => match row. as_slice ( ) {
291- [ _, _, Value :: Summary { state, .. } ] => {
292- asap_summary_state:: physical:: from_physical ( state. as_ref ( ) )
293- . map ( Some )
294- . map_err ( |e| e. to_string ( ) )
295- }
296- _ => Err ( "raw precompute output is not a population state" . into ( ) ) ,
297- } ,
298- _ => Err ( "one routed group produced several populations" . into ( ) ) ,
299- }
335+ . map_err ( |e| e. to_string ( ) )
300336 }
301337
302- pub fn updater ( & self ) -> Result < Box < dyn AccumulatorUpdater > , String > {
338+ fn heap_updater ( & self ) -> Result < Box < dyn AccumulatorUpdater > , String > {
303339 create_planner_accumulator ( & self . family , & self . input , & self . grouping )
304340 }
305341
306- fn validate ( & self ) -> Result < ( ) , String > {
307- match & self . input . weight {
308- SummaryInputExpr :: Column ( ColumnRef :: SampleValue ) | SummaryInputExpr :: Constant ( _) => { }
309- SummaryInputExpr :: Column (
310- ColumnRef :: Named ( name) | ColumnRef :: Qualified { name, .. } ,
311- ) if self . projected_column . as_ref ( ) == Some ( name) => { }
312- _ => return Err ( "raw DAG weight expression is unsupported" . into ( ) ) ,
313- }
314- fn item ( expr : & SummaryInputExpr ) -> bool {
315- match expr {
316- SummaryInputExpr :: Column ( ColumnRef :: Named ( _) | ColumnRef :: SampleValue ) => true ,
317- SummaryInputExpr :: Tuple ( items) => items. iter ( ) . all ( item) ,
318- SummaryInputExpr :: EntityIdentity (
319- planner_types:: post_asap:: EntityIdentity :: PromqlLabelSet { excluding } ,
320- ) => excluding. is_empty ( ) ,
321- _ => false ,
322- }
323- }
324- if self . input . item . as_ref ( ) . is_some_and ( |e| !item ( e) ) {
325- return Err ( "raw DAG item expression is unsupported" . into ( ) ) ;
326- }
327- self . updater ( ) . map ( |_| ( ) )
328- }
329-
330- /// Validate admission with the same weight semantics used during execution,
331- /// without mutating an accumulator or accepting part of a request.
332- pub fn validate_sample (
333- & self ,
334- updater : & dyn AccumulatorUpdater ,
335- value : f64 ,
336- ) -> Result < f64 , String > {
342+ fn validate_sample ( & self , updater : & dyn AccumulatorUpdater , value : f64 ) -> Result < f64 , String > {
337343 let weight = match & self . input . weight {
338344 SummaryInputExpr :: Constant ( c) => * c,
339345 // The worker retains one previous value per series across pane rotation.
@@ -347,7 +353,7 @@ impl RawDagProgram {
347353 Ok ( weight)
348354 }
349355
350- pub fn apply (
356+ fn apply (
351357 & self ,
352358 updater : & mut dyn AccumulatorUpdater ,
353359 series : & str ,
0 commit comments