@@ -14,65 +14,111 @@ pub mod promql;
1414
1515use asap_types:: pre_asap:: resolve_root;
1616use asap_types:: pre_asap:: QueryExpr ;
17- use asap_types:: types:: AccuracyTarget ;
18- use asap_types:: workload:: { QueryLanguage , QueryWorkload } ;
17+ use asap_types:: workload:: { DurationMs , QueryLanguage , QueryWorkload } ;
1918
2019pub use error:: PromqlError ;
2120pub use histogram:: { HistogramCatalog , HistogramKind } ;
2221pub use promql:: PromqlLowerer ;
2322
24- /// Lower a single PromQL query string to the canonical, resolved `QueryExpr`.
23+ /// Lower every normalized PromQL workload entry to a plan-ready `QueryExpr`.
2524///
26- /// `accuracy` is threaded onto every approximate intent (`Count`, `Quantile`,
27- /// `Cardinality`, `TopK`). The returned tree carries a self-contained `Schema`
28- /// on its `Scan`; call [`QueryExpr::output_schema`] for any node's schema.
29- ///
30- /// `histogram_quantile` discrimination uses the structural heuristic; to drive
31- /// it from declared sample types instead, use [`lower_promql_with_histograms`].
32- pub fn lower_promql ( query : & str , accuracy : AccuracyTarget ) -> Result < QueryExpr , PromqlError > {
33- let unresolved = PromqlLowerer :: lower ( query, & accuracy) ?;
34- let resolved = resolve_root ( & unresolved) ?;
35- Ok ( resolved)
25+ /// PromQL workloads must declare a non-zero `data_ingestion_interval`; it is
26+ /// injected around each bare instant selector. Explicit range selectors keep
27+ /// their query-specified range.
28+ pub fn lower_promql_workload ( workload : & QueryWorkload ) -> Result < Vec < QueryExpr > , PromqlError > {
29+ if !matches ! ( workload. language, QueryLanguage :: PromQL ) {
30+ return Err ( PromqlError :: WrongLanguage ( format ! (
31+ "{:?}" ,
32+ workload. language
33+ ) ) ) ;
34+ }
35+ workload. validate ( ) ?;
36+ let DurationMs ( interval_ms) = workload
37+ . data_workload
38+ . as_ref ( )
39+ . expect ( "validated PromQL workload has data_workload" )
40+ . data_ingestion_interval
41+ . value
42+ . expect ( "validated PromQL workload has data_ingestion_interval" ) ;
43+ workload
44+ . entries ( )
45+ . map ( |entry| {
46+ let unresolved = PromqlLowerer :: lower_with_ingestion_interval (
47+ & entry. query . 0 ,
48+ & entry. requirements . accuracy . target ( ) ,
49+ std:: time:: Duration :: from_millis ( interval_ms) ,
50+ ) ?;
51+ Ok ( resolve_root ( & unresolved) ?)
52+ } )
53+ . collect ( )
3654}
3755
38- /// Like [`lower_promql`], but consults `histograms` to decide whether a
39- /// `histogram_quantile` argument is sketch-able (generic `Quantile`) or a
40- /// classic-bucket interpolation (`HistogramQuantile`) — a type-driven decision
41- /// instead of the structural heuristic (issue #79). Metrics absent from the
42- /// catalog still fall back to the heuristic.
43- pub fn lower_promql_with_histograms (
44- query : & str ,
45- accuracy : AccuracyTarget ,
46- histograms : HistogramCatalog ,
47- ) -> Result < QueryExpr , PromqlError > {
48- let _guard = histogram:: CatalogGuard :: install ( histograms) ;
49- lower_promql ( query, accuracy)
50- }
56+ #[ cfg( test) ]
57+ mod tests {
58+ use std:: time:: Duration ;
5159
52- /// Lower every PromQL batch entry in `workload` to a `QueryExpr`.
53- ///
54- /// One `Result` per entry — errors are per-query, not fatal for the batch.
55- /// Returns an empty `Vec` if `workload.query_batch` is absent or empty, and a
56- /// `WrongLanguage` error for every entry if the workload language is not PromQL.
57- pub fn lower_promql_batch ( workload : & QueryWorkload ) -> Vec < Result < QueryExpr , PromqlError > > {
58- let entries = match & workload. query_batch {
59- Some ( e) if !e. is_empty ( ) => e,
60- _ => return vec ! [ ] ,
60+ use asap_types:: pre_asap:: QueryExpr ;
61+ use asap_types:: workload:: {
62+ BatchEntry , DataWorkload , Evidence , Query , QueryRequirements , TimeSelection ,
6163 } ;
6264
63- if !matches ! ( workload. language, QueryLanguage :: PromQL ) {
64- let lang = format ! ( "{:?}" , workload. language) ;
65- return entries
66- . iter ( )
67- . map ( |_| Err ( PromqlError :: WrongLanguage ( lang. clone ( ) ) ) )
68- . collect ( ) ;
65+ use super :: * ;
66+
67+ fn workload ( query : & str ) -> QueryWorkload {
68+ QueryWorkload {
69+ language : QueryLanguage :: PromQL ,
70+ query_batch : Some ( vec ! [ BatchEntry {
71+ query: Query ( query. into( ) ) ,
72+ requirements: QueryRequirements :: default ( ) ,
73+ predictability: Default :: default ( ) ,
74+ invocations: 1 ,
75+ execute_at: None ,
76+ time_selection: TimeSelection :: default ( ) ,
77+ } ] ) ,
78+ repeating_queries : None ,
79+ data_workload : Some ( DataWorkload {
80+ data_ingestion_interval : Evidence {
81+ value : Some ( DurationMs ( 1_000 ) ) ,
82+ ..Default :: default ( )
83+ } ,
84+ ..Default :: default ( )
85+ } ) ,
86+ }
6987 }
7088
71- entries
72- . iter ( )
73- . map ( |entry| {
74- let accuracy = entry. requirements . accuracy . target ( ) ;
75- lower_promql ( & entry. query . 0 , accuracy)
76- } )
77- . collect ( )
89+ #[ test]
90+ fn instant_selector_uses_declared_ingestion_interval ( ) {
91+ let query = lower_promql_workload ( & workload ( "sum by (job) (data)" ) ) . unwrap ( ) ;
92+ let QueryExpr :: Aggregate { child, .. } = & query[ 0 ] else {
93+ panic ! ( "expected aggregate" )
94+ } ;
95+ assert ! (
96+ matches!( child. as_ref( ) , QueryExpr :: TimeRange { range, child }
97+ if * range == Duration :: from_secs( 1 ) && matches!( child. as_ref( ) , QueryExpr :: Scan { .. } ) )
98+ ) ;
99+ }
100+
101+ #[ test]
102+ fn explicit_range_selector_keeps_its_query_range ( ) {
103+ let query = lower_promql_workload ( & workload ( "sum_over_time(data[5m])" ) ) . unwrap ( ) ;
104+ let QueryExpr :: Aggregate { child, .. } = & query[ 0 ] else {
105+ panic ! ( "expected aggregate" )
106+ } ;
107+ assert ! (
108+ matches!( child. as_ref( ) , QueryExpr :: TimeRange { range, child }
109+ if * range == Duration :: from_secs( 300 ) && matches!( child. as_ref( ) , QueryExpr :: Scan { .. } ) )
110+ ) ;
111+ }
112+
113+ #[ test]
114+ fn workload_without_interval_fails_loudly ( ) {
115+ let mut workload = workload ( "sum(data)" ) ;
116+ workload. data_workload = Some ( DataWorkload :: default ( ) ) ;
117+ assert ! ( matches!(
118+ lower_promql_workload( & workload) ,
119+ Err ( PromqlError :: InvalidWorkload (
120+ asap_types:: workload:: WorkloadError :: MissingDataIngestionInterval
121+ ) )
122+ ) ) ;
123+ }
78124}
0 commit comments