@@ -23,9 +23,9 @@ use asap_types::ir::schema::FieldDataType;
2323use asap_types:: types:: AccuracyTarget ;
2424use asap_types:: workload:: {
2525 AccuracyRequirement , DataArrival , DataDistribution , DataWorkload , DurationMs , Evidence ,
26- EvidenceSource , LatencyRequirement , PlanningWorkload , Predictability , Query , QueryLanguage ,
27- QueryRequirements , QueryTimeScope , QueryWorkload , Rate , RepeatedDemand , RepeatingEntry ,
28- RepetitionInterval , TimeSelection ,
26+ EvidenceSource , LatencyRequirement , MetricType , PlanningWorkload , Predictability , Query ,
27+ QueryLanguage , QueryRequirements , QueryTimeScope , QueryWorkload , Rate , RepeatedDemand ,
28+ RepeatingEntry , RepetitionInterval , TimeSelection ,
2929} ;
3030
3131/// Adapters from the Phase C stage APIs to the shapes these tests were written
@@ -331,6 +331,8 @@ fn example1_workload() -> PlanningWorkload {
331331 ingestion_rate : declared ( Rate ( 1_000_000.0 / 15.0 ) ) ,
332332 input_cardinality : declared ( 1_000_000 ) ,
333333 distribution : declared ( DataDistribution :: Zipf ) ,
334+ // `http_requests_total` is a counter: its samples are never negative.
335+ metric_types : [ ( "http_requests_total" . into ( ) , MetricType :: Counter ) ] . into ( ) ,
334336 } ) ,
335337 }
336338}
@@ -504,12 +506,31 @@ fn relational(payload: &LogicalASAPOperatorPayload) -> Option<String> {
504506 }
505507}
506508
509+ /// Example 1 with `http_requests_total` declared `metric_type`, or undeclared.
510+ fn example1_workload_with ( metric_type : Option < MetricType > ) -> PlanningWorkload {
511+ let mut workload = example1_workload ( ) ;
512+ let data = workload. data_workload . as_mut ( ) . expect ( "data workload" ) ;
513+ data. metric_types = metric_type
514+ . map ( |t| [ ( "http_requests_total" . to_string ( ) , t) ] . into ( ) )
515+ . unwrap_or_default ( ) ;
516+ workload
517+ }
518+
507519fn pipeline ( ) -> (
508520 PlanningWorkload ,
509521 Vec < LogicalCandidate > ,
510522 Vec < PhysicalCandidate > ,
511523) {
512- let workload = example1_workload ( ) ;
524+ pipeline_for ( example1_workload ( ) )
525+ }
526+
527+ fn pipeline_for (
528+ workload : PlanningWorkload ,
529+ ) -> (
530+ PlanningWorkload ,
531+ Vec < LogicalCandidate > ,
532+ Vec < PhysicalCandidate > ,
533+ ) {
513534 let logical = stage1_logical_asap ( & workload, & stage0_logical ( & workload) ) ;
514535 let physical = stage2_physical ( & workload, & logical) ;
515536 ( workload, logical, physical)
@@ -887,21 +908,18 @@ fn compile_in_runtime(p: &PhysicalCandidate) -> Result<(), String> {
887908 . map_err ( |e| format ! ( "{} ({}): {e}" , p. id, p. label) )
888909}
889910
890- /// Runtime capability check (added by the implementer, not part of the
891- /// spec): the physical planner compiles every candidate Stage 3 finds valid,
892- /// and rejects the invalid ones (Count-Min over weights not proven
893- /// non-negative) for the same reason Stage 3 gives.
894- #[ test]
895- fn stage2_runtime_compiles_exactly_the_candidates_stage3_finds_valid ( ) {
896- let ( workload, _, physical) = pipeline ( ) ;
911+ /// The physical planner compiles every candidate Stage 3 finds valid, and
912+ /// rejects the invalid ones (Count-Min over weights not proven non-negative)
913+ /// for the same reason Stage 3 gives. Returns the number of invalid ones.
914+ fn assert_runtime_agrees_with_stage3 ( workload : PlanningWorkload ) -> usize {
915+ let ( workload, _, physical) = pipeline_for ( workload) ;
897916 let selection = stage3_select ( & workload, & physical, PlanningModels :: builtin ( ) ) ;
898917 let invalid: BTreeMap < _ , _ > = selection
899918 . rejected
900919 . iter ( )
901920 . filter ( |r| !r. valid )
902921 . map ( |r| ( r. id . as_str ( ) , r. reason . as_str ( ) ) )
903922 . collect ( ) ;
904- assert_eq ! ( invalid. len( ) , 24 , "the Count-Min + heap candidates" ) ;
905923 for p in & physical {
906924 let compiled = compile_in_runtime ( p) ;
907925 match invalid. get ( p. id . as_str ( ) ) {
@@ -920,6 +938,28 @@ fn stage2_runtime_compiles_exactly_the_candidates_stage3_finds_valid() {
920938 }
921939 }
922940 }
941+ invalid. len ( )
942+ }
943+
944+ /// Runtime capability check (added by the implementer, not part of the
945+ /// spec): with `http_requests_total` declared a counter, every candidate,
946+ /// Count-Min + heap included, is valid in Stage 3 and compiles.
947+ #[ test]
948+ fn stage2_runtime_compiles_every_candidate_over_a_declared_counter ( ) {
949+ assert_eq ! ( assert_runtime_agrees_with_stage3( example1_workload( ) ) , 0 ) ;
950+ }
951+
952+ /// Without the counter declaration, or with a gauge, the Count-Min + heap
953+ /// candidates are invalid in Stage 3 and the runtime rejects them.
954+ #[ test]
955+ fn stage2_count_min_needs_a_counter_declaration ( ) {
956+ for metric_type in [ None , Some ( MetricType :: Gauge ) ] {
957+ assert_eq ! (
958+ assert_runtime_agrees_with_stage3( example1_workload_with( metric_type) ) ,
959+ 24 ,
960+ "{metric_type:?}: the Count-Min + heap candidates"
961+ ) ;
962+ }
923963}
924964
925965/// A CountSketch+heap top-k readout compiles: the IR's derived readout schema
@@ -1030,8 +1070,8 @@ fn stage3_charges_each_node_once() {
10301070}
10311071
10321072/// Sharing the input never costs more than reading it separately, for the
1033- /// same local choices. Count-Min + heap is invalid here and has no cost
1034- /// (Hydra: see `stage1_q2_summary_families_are_heap_sketches_and_hydra`).
1073+ /// same local choices (Hydra: see
1074+ /// `stage1_q2_summary_families_are_heap_sketches_and_hydra`).
10351075#[ test]
10361076fn stage3_shared_input_is_not_costlier ( ) {
10371077 let ( workload, _, physical) = pipeline ( ) ;
@@ -1046,7 +1086,9 @@ fn stage3_shared_input_is_not_costlier() {
10461086 . collect ( ) ;
10471087 for option in [
10481088 Q2Option :: Exact ,
1089+ Q2Option :: CountMinHeapPerJob ,
10491090 Q2Option :: CountSketchHeapPerJob ,
1091+ Q2Option :: WholeCountMinHeapPerJob ,
10501092 Q2Option :: WholeCountSketchHeapPerJob ,
10511093 ] {
10521094 assert ! (
0 commit comments