From 5697048647928cd7575cf03ae952a5740483e95b Mon Sep 17 00:00:00 2001 From: zzylol Date: Thu, 15 Oct 2026 18:45:17 +0000 Subject: [PATCH] feat(types): one AccuracyTarget with an optional, derived metric; any metric accepted when unset Merge RequestedAccuracy into AccuracyTarget, a plain struct whose metric is optional. A target that sets no metric accepts a guarantee in any metric (absolute at any scale, relative, rank) that meets its epsilon, delta and scope; a target that sets one keeps today's rule. A quantile without a metric is no longer an input error: KLL and DDSketch compete on cost. - AccuracyTarget serializes as {"metric"?, "error", "failure_probability"?, "scope"?}; the old "Exact"/{"Epsilon"}/{"EpsilonDelta"} forms are gone. - AccuracySpec stays the guarantee's outer shape. - RequestedAccuracy, AccuracyRequestError, for_root and SelectionError::AccuracyRequest are removed; default_metric_for_root keeps the per-intent default table for display. - Stage 3 reports, per valid candidate and query, the accepted metric (Selection::accepted, AcceptedAccuracy); stage_pipeline writes it and the target JSON, and the dag-viewer shows both. - Sizing a share without a metric fails closed (no split) with a debug assertion. --- crates/asap-aware-mapping/src/accuracy/mod.rs | 49 ++- .../asap-aware-mapping/src/asap_aware_cse.rs | 6 +- .../src/maintained_population.rs | 2 +- .../src/pass/stage1_to_stage3.rs | 43 +- .../src/per_computation_candidates.rs | 28 +- .../src/query_physical_lowering.rs | 4 +- crates/asap-aware-mapping/src/replacement.rs | 44 +- crates/asap-aware-mapping/src/rewrite.rs | 6 +- .../src/stage2_physical_asap_optimization.rs | 11 +- .../src/stage3_plan_selection.rs | 205 +++++---- crates/asap-aware-mapping/src/test_support.rs | 6 +- .../src/physical_planner/candidates.rs | 4 +- .../tests/common/mod.rs | 6 +- .../tests/current_series_heap.rs | 4 +- .../tests/deployment_computation.rs | 10 +- .../tests/planspace_series_identity_heap.rs | 6 +- .../tests/precompute_candidates.rs | 16 +- .../tests/weighted_topk_binding.rs | 21 +- .../devtools/examples/canonical_examples.rs | 6 +- crates/devtools/examples/topk_ir.rs | 6 +- crates/devtools/src/bin/analyze_corpora.rs | 6 +- crates/devtools/src/bin/show_logical_dag.rs | 6 +- crates/devtools/src/bin/sketch_coverage.rs | 8 +- crates/devtools/src/bin/stage_pipeline.rs | 78 +++- crates/devtools/src/bin/variant_coverage.rs | 13 +- crates/devtools/src/lib.rs | 2 +- .../devtools/src/planner_layering_examples.rs | 16 +- crates/devtools/src/planning_stages.rs | 4 +- crates/devtools/tests/cross_language.rs | 6 +- crates/devtools/tests/stage_pipeline.rs | 74 +++- crates/frontend-metricsql/src/lib.rs | 8 +- crates/frontend-metricsql/tests/lowering.rs | 10 +- .../tests/lowering_semantics.rs | 14 +- crates/frontend-promql/src/promql.rs | 16 +- .../frontend-promql/tests/count_planning.rs | 16 +- .../tests/histogram_metadata.rs | 18 +- .../tests/maintained_population_horizon.rs | 4 +- .../awesome_prometheus_alerts.rs | 6 +- .../observability/metrics_observability.rs | 6 +- .../tests/observability/o11y_bench_promql.rs | 4 +- .../tests/observability/promql_corpus.rs | 8 +- .../tests/promql_binding_regressions.rs | 10 +- .../tests/promql_conformance.rs | 12 +- .../tests/promql_equivalence.rs | 5 +- .../frontend-promql/tests/promql_lowering.rs | 25 +- crates/frontend-promql/tests/scalar_design.rs | 4 +- crates/frontend-promql/tests/support.rs | 16 +- .../tests/univmon_candidates.rs | 12 +- crates/frontend-sql/src/lib.rs | 6 +- crates/frontend-sql/src/sql/mod.rs | 16 +- .../tests/bgp_analytics/bgp_analytics.rs | 4 +- .../bgp_jan2024_workload.rs | 4 +- .../synthetic_packet_trace.rs | 6 +- .../tests/data_quality_check/tpch_deequ.rs | 4 +- .../frontend-sql/tests/frequency_entropy.rs | 10 +- crates/frontend-sql/tests/frequency_l2.rs | 18 +- .../tests/maintained_population.rs | 4 +- crates/frontend-sql/tests/netflow/netflow.rs | 4 +- crates/frontend-sql/tests/pearson_corr.rs | 8 +- crates/frontend-sql/tests/sql_lowering.rs | 104 ++--- crates/frontend-sql/tests/temporal_types.rs | 14 +- crates/integration-tests/src/lib.rs | 14 +- crates/integration-tests/tests/aggregate.rs | 3 +- crates/integration-tests/tests/binary_op.rs | 4 +- .../tests/exact_composition.rs | 3 +- .../tests/filtered_aggregates.rs | 14 +- .../tests/frequency_execution.rs | 4 +- .../tests/frontend_timestamps.rs | 7 +- .../tests/materialization_e2e.rs | 16 +- crates/integration-tests/tests/nested.rs | 4 +- .../tests/operator_design_examples.rs | 14 +- .../tests/operator_sharing.rs | 10 +- .../tests/per_computation_candidates_sql.rs | 10 +- .../tests/planner_layering_example2.rs | 13 +- .../tests/precompute_raw_samples.rs | 12 +- .../tests/promql_numeric_regressions.rs | 18 +- .../tests/promql_to_post_asap.rs | 53 ++- crates/integration-tests/tests/scan.rs | 4 +- crates/integration-tests/tests/schema.rs | 4 +- .../tests/sql_cardinality.rs | 4 +- .../integration-tests/tests/sql_frequency.rs | 4 +- .../tests/sql_to_physical.rs | 6 +- .../tests/sql_to_post_asap.rs | 30 +- .../tests/stage1_logical_asap_optimization.rs | 6 +- .../stage2_physical_asap_optimization_sql.rs | 10 +- .../tests/summary_coverage_e2e.rs | 6 +- crates/integration-tests/tests/time_range.rs | 3 +- .../tests/tumbling_pane_merge.rs | 18 +- crates/planner/tests/e2e_plan.rs | 6 +- crates/planner/tests/plan_stages.rs | 4 +- crates/planner/tests/summary_sharing.rs | 6 +- crates/types/src/pre_asap/agg_intent.rs | 6 +- crates/types/src/types.rs | 393 ++++++------------ crates/types/src/workload.rs | 22 +- docs/user_guide_docs/stage-viewer.md | 10 +- tools/dag-viewer/README.md | 13 +- tools/dag-viewer/app.js | 7 +- .../examples/planner-layering-example1.json | 176 +++++++- tools/dag-viewer/index.html | 1 + tools/dag-viewer/server.py | 4 +- tools/dag-viewer/stages.js | 25 ++ tools/dag-viewer/test_server.py | 17 +- tools/dag-viewer/test_viewer.py | 22 +- tools/verify_metricsql_external_consumer.sh | 2 +- 104 files changed, 1140 insertions(+), 960 deletions(-) diff --git a/crates/asap-aware-mapping/src/accuracy/mod.rs b/crates/asap-aware-mapping/src/accuracy/mod.rs index 0d1916dd6..072922e1f 100644 --- a/crates/asap-aware-mapping/src/accuracy/mod.rs +++ b/crates/asap-aware-mapping/src/accuracy/mod.rs @@ -63,12 +63,14 @@ pub trait AccuracyModel { stats: &PropagationStats, ) -> Result; - /// Whether `guarantee` meets `target`: a bound in the target's metric - /// (or one with a sound direct comparison) within its ε, and within its - /// δ when it states one, over the values the target's scope covers (a - /// per-value δ does not meet a whole-result target). A bound in another metric does not satisfy, - /// however small. Unknown required dimensions fail; selection - /// separately excludes missing accuracy evidence. + /// Whether `guarantee` meets `target`: a bound within its ε, and within + /// its δ when it states one, over the values the target's scope covers + /// (a per-value δ does not meet a whole-result target). When the target + /// sets a metric, the bound must be in that metric (or one with a sound + /// direct comparison): a bound in another metric does not satisfy, + /// however small. When it sets none, a bound in any metric does, each + /// compared in its own metric. Unknown required dimensions fail; + /// selection separately excludes missing accuracy evidence. fn satisfies(&self, guarantee: &AccuracyGuarantee, target: &AccuracyTarget) -> bool; } @@ -119,7 +121,9 @@ impl AccuracyModel for DefaultAccuracyModel { if target.is_exact() || guarantee.is_exact() { return guarantee.is_exact(); } - metric_meets(guarantee.spec.metric, target.metric) + target + .metric + .is_none_or(|metric| metric_meets(guarantee.spec.metric, metric)) && scope_meets(guarantee, target) && within(guarantee.spec.error.evaluate(), target.error) && target @@ -336,4 +340,35 @@ mod tests { assert!(model.satisfies(&never_fails, &whole)); assert!(model.satisfies(&whole_result_guarantee(&never_fails, None), &whole)); } + + /// A target whose metric the user did not set accepts a guarantee in + /// any metric whose own ε, δ and scope meet the target's numbers: a + /// rank, a relative or an absolute bound at any scale. A larger bound + /// or failure probability still misses it. + #[test] + fn a_target_without_a_metric_accepts_any_metric() { + let model = DefaultAccuracyModel; + let target = AccuracyTarget::epsilon_delta(0.01, 0.01).with_scope(AccuracyScope::PerValue); + assert_eq!(target.metric, None); + for metric in [ + ErrorMetric::ABSOLUTE, + ErrorMetric::ABSOLUTE_STREAM_L1, + ErrorMetric::ABSOLUTE_STREAM_L2, + ErrorMetric::Relative, + ErrorMetric::Rank, + ] { + assert!( + model.satisfies(&guarantee(metric, 0.01, 0.01), &target), + "{metric:?}" + ); + assert!( + !model.satisfies(&guarantee(metric, 0.02, 0.01), &target), + "{metric:?}" + ); + assert!( + !model.satisfies(&guarantee(metric, 0.01, 0.02), &target), + "{metric:?}" + ); + } + } } diff --git a/crates/asap-aware-mapping/src/asap_aware_cse.rs b/crates/asap-aware-mapping/src/asap_aware_cse.rs index 465b60fb4..6a975b137 100644 --- a/crates/asap-aware-mapping/src/asap_aware_cse.rs +++ b/crates/asap-aware-mapping/src/asap_aware_cse.rs @@ -892,13 +892,13 @@ mod tests { use super::*; use crate::test_support::lower_promql; use asap_types::ir::schema_support::with_promql_series_identity; - use asap_types::types::RequestedAccuracy; + use asap_types::types::AccuracyTarget; fn roots(queries: &[&str]) -> Vec { queries .iter() .map(|q| { - let root = lower_promql(q, RequestedAccuracy::exact()); + let root = lower_promql(q, AccuracyTarget::exact()); QueryRoot::Operator(with_promql_series_identity(&root).unwrap()) }) .collect() @@ -1079,7 +1079,7 @@ mod tests { .map(|(i, (q, epsilon))| { let root = lower_promql( q, - asap_types::types::RequestedAccuracy::epsilon_delta(*epsilon, 0.01) + asap_types::types::AccuracyTarget::epsilon_delta(*epsilon, 0.01) .with_metric(asap_types::types::ErrorMetric::Rank), ); ( diff --git a/crates/asap-aware-mapping/src/maintained_population.rs b/crates/asap-aware-mapping/src/maintained_population.rs index 05b3cc48b..7a5e45333 100644 --- a/crates/asap-aware-mapping/src/maintained_population.rs +++ b/crates/asap-aware-mapping/src/maintained_population.rs @@ -355,7 +355,7 @@ mod tests { } fn lower(q: &str) -> Rc { - lower_promql(q, asap_types::types::RequestedAccuracy::exact()) + lower_promql(q, asap_types::types::AccuracyTarget::exact()) } // Instant scalar aggregations share the same retractable series population. diff --git a/crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs b/crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs index f05101a2f..04fd41ae8 100644 --- a/crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs +++ b/crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs @@ -40,7 +40,7 @@ use asap_types::ir::flat::flatten; use asap_types::ir::schema_support::with_promql_series_identity; use asap_types::ir::{ASAPOp, OperatorNode, QueryRoot}; use asap_types::post_asap::{FieldDataType, SketchAlgorithm}; -use asap_types::types::{AccuracyTarget, RequestedAccuracy}; +use asap_types::types::AccuracyTarget; use asap_types::workload::{DataWorkload, QueryLanguage, QueryRecurrence, RepeatedDemand}; use super::{OptimizationInput, OptimizationPass, OptimizeError, PlanOutput, QueryPlan}; @@ -154,7 +154,7 @@ impl Stage1ToStage3Run { /// listed with its reason, and a failed selection is reported in the run. pub fn run_stage1_to_stage3( roots: Vec<(Id, QueryRoot)>, - targets: &[Option], + targets: &[Option], recurrence: &[QueryRecurrence], data: &DataWorkload, models: PlanningModels<'_>, @@ -487,7 +487,7 @@ fn readers_of(roots: &[QueryRoot], node: &Rc) -> Vec { /// enumerating. pub fn run_stage1_to_stage3_from_pass1( per_computation: PerComputationCandidates, - targets: &[Option], + targets: &[Option], recurrence: &[QueryRecurrence], data: &DataWorkload, models: PlanningModels<'_>, @@ -502,7 +502,7 @@ pub fn run_stage1_to_stage3_from_pass1( pub fn run_stage2_and_stage3( per_computation: PerComputationCandidates, stage1: Vec, - targets: &[Option], + targets: &[Option], recurrence: &[QueryRecurrence], data: &DataWorkload, models: PlanningModels<'_>, @@ -545,13 +545,7 @@ pub fn run_stage2_and_stage3( roots: candidate.roots, }); } - let (resolved, selection) = match resolve_targets(&per_computation.roots, targets) { - Ok(targets) => { - let selection = select_plan(&physical, &targets, recurrence, data, models); - (Some(targets), selection) - } - Err(error) => (None, Err(error)), - }; + let selection = select_plan(&physical, targets, recurrence, data, models); let selected_logical = selection.as_ref().ok().and_then(|selection| { let from = &physical .iter() @@ -564,8 +558,7 @@ pub fn run_stage2_and_stage3( .as_ref() .ok()?; // As Stage 3 sized it for its queries' failure probabilities. - let sized = - crate::stage3_plan_selection::sized_roots(roots, resolved.as_deref()?, data).ok()?; + let sized = crate::stage3_plan_selection::sized_roots(roots, targets, data).ok()?; Some(crate::stage3_plan_selection::with_confirmed_weights( &sized, data, )) @@ -582,30 +575,6 @@ pub fn run_stage2_and_stage3( } } -/// Each query's requested accuracy resolved for its frontend root -/// ([`RequestedAccuracy::for_root`]): the metric plan selection checks. -fn resolve_targets( - roots: &[(Id, QueryRoot)], - targets: &[Option], -) -> Result>, SelectionError> { - roots - .iter() - .zip(targets) - .enumerate() - .map(|(query, ((_, root), requested))| { - requested - .as_ref() - .map(|requested| requested.for_root(root)) - .transpose() - .map_err(|error| SelectionError::AccuracyRequest { - query: query + 1, - error, - }) - }) - .chain(targets.iter().skip(roots.len()).map(|_| Ok(None))) - .collect() -} - /// Mixed-radix increment of `choice`: the last target varies fastest. /// `false` when a carry out of the first target ends the enumeration. fn next_choice(inventory: &PerComputationCandidates, choice: &mut [usize]) -> bool { diff --git a/crates/asap-aware-mapping/src/per_computation_candidates.rs b/crates/asap-aware-mapping/src/per_computation_candidates.rs index b10dc2507..7c3f09645 100644 --- a/crates/asap-aware-mapping/src/per_computation_candidates.rs +++ b/crates/asap-aware-mapping/src/per_computation_candidates.rs @@ -2738,7 +2738,19 @@ fn sized(algorithm: &SketchAlgorithm, intent: &AggIntent, share: usize) -> Sketc &query, ) }) - .map_or(target.metric, |guarantee| guarantee.spec.metric); + .map(|guarantee| guarantee.spec.metric) + .or(target.metric); + // The split needs a concrete metric. A summary without an error model + // whose intent's target sets none is not split (never a guessed + // metric): it stays sized for the whole target, and Stage 3 rejects the + // candidate if its composed output then misses the query's target. + let Some(metric) = metric else { + debug_assert!( + target.is_exact(), + "sizing {algorithm:?} for a share of an approximate target without a metric" + ); + return whole; + }; equal_share(&target, metric, share).map_or(whole, |share| size(&share)) } @@ -2936,7 +2948,7 @@ fn statistic(intent: &AggIntent) -> Result AccuracyGuarantee { AccuracyGuarantee::new( @@ -2983,7 +2995,7 @@ mod tests { } /// `query` carrying `guarantee`. fn node_over(query: &str, guarantee: AccuracyGuarantee) -> Rc { - let node = lower_promql(query, RequestedAccuracy::exact()); + let node = lower_promql(query, AccuracyTarget::exact()); Rc::new((*node).clone().with_guarantee(Some(guarantee))) } fn scalar(guarantee: AccuracyGuarantee) -> ScalarExpr { @@ -3591,7 +3603,7 @@ mod tests { fn inventory(query: &str) -> PerComputationCandidates { let root = - crate::test_support::lower_promql(query, RequestedAccuracy::epsilon_delta(0.01, 0.001)); + crate::test_support::lower_promql(query, AccuracyTarget::epsilon_delta(0.01, 0.001)); // Per-series state ranks each series' identity, which the rows carry // as a column; a query over aggregated rows is planned over labels. let root = @@ -3704,7 +3716,7 @@ mod tests { fn whole_expression_topk_partitions_index_the_heap_input() { let root = crate::test_support::lower_promql( "topk by (job) (2, sum by (service, job) (rate(m[1m])))", - RequestedAccuracy::epsilon(0.1), + AccuracyTarget::epsilon(0.1), ); let inventory = generate_per_computation_candidates(vec![(0, QueryRoot::Operator(root))]).unwrap(); @@ -3786,7 +3798,7 @@ mod tests { ("rate(m[1m])", None), ("max_over_time(m[1m])", None), ] { - let root = crate::test_support::lower_promql(query, RequestedAccuracy::exact()); + let root = crate::test_support::lower_promql(query, AccuracyTarget::exact()); assert_eq!(samples_of_metric(&root).as_deref(), metric, "{query}"); } } @@ -4068,7 +4080,7 @@ mod tests { grouping, ) }; - let input = |query| crate::test_support::lower_promql(query, RequestedAccuracy::exact()); + let input = |query| crate::test_support::lower_promql(query, AccuracyTarget::exact()); let guarantee = |family: &FieldDataType, query| { source_distinct_guarantee(family, &SketchStatistic::Cardinality, &input(query)) }; diff --git a/crates/asap-aware-mapping/src/query_physical_lowering.rs b/crates/asap-aware-mapping/src/query_physical_lowering.rs index 49bdc6e91..ade618fab 100644 --- a/crates/asap-aware-mapping/src/query_physical_lowering.rs +++ b/crates/asap-aware-mapping/src/query_physical_lowering.rs @@ -1318,7 +1318,7 @@ mod tests { PromqlEdgeStatistics, PromqlUnaryEdgeStatistics, PromqlValueKind, UnaryEdgeStatistics, }; use asap_types::ir::{BinaryOperator, ExprSemantics, Predicate, SortKey, TimeRangeKind}; - use asap_types::types::RequestedAccuracy; + use asap_types::types::AccuracyTarget; use asap_types::workload::{ DataArrival, DurationMs, QueryRecurrence, QueryTimeScope, TimeSelection, TimestampMs, }; @@ -2885,7 +2885,7 @@ mod tests { query_batch: Some(vec![BatchEntry { query: Query(query.into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::exact()), ..Default::default() }, predictability: Predictability::Unknown, diff --git a/crates/asap-aware-mapping/src/replacement.rs b/crates/asap-aware-mapping/src/replacement.rs index 45e6ac41a..8cfee24f0 100644 --- a/crates/asap-aware-mapping/src/replacement.rs +++ b/crates/asap-aware-mapping/src/replacement.rs @@ -6562,16 +6562,15 @@ mod tests { agg_is_exact, default_cardinality, default_quantile, MathFunc, TimeFunc, }; use asap_types::pre_asap::schema::{DataType, Field, Schema as SchemaTy}; - use asap_types::types::RequestedAccuracy; - use asap_types::types::AccuracyTarget; + use std::collections::HashMap; /// A query result reads an exact state through its finalize; any other /// selected root is returned as assembled. #[test] fn assemble_selected_query_finalizes_only_an_exact_state_root() { - let assemble = |query: &str, accuracy: RequestedAccuracy| { + let assemble = |query: &str, accuracy: AccuracyTarget| { let space = search_workload(vec![("q", lower_promql(query, accuracy))]).unwrap(); let selection = space.global_selection(&DefaultCostModel); let root = &space.roots[0].1; @@ -6580,7 +6579,7 @@ mod tests { selection.assemble_selected_query(root).unwrap().unwrap(), ) }; - let (state, query) = assemble("sum_over_time(m[1m])", RequestedAccuracy::exact()); + let (state, query) = assemble("sum_over_time(m[1m])", AccuracyTarget::exact()); assert!(matches!( state.asap(), Some(ASAPOp::SummaryAgg { @@ -6596,7 +6595,7 @@ mod tests { let (estimate, query) = assemble( "quantile_over_time(0.99, m[1m])", - RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Rank), + AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Rank), ); assert!(estimate.contains_asap()); assert!(Rc::ptr_eq(&estimate, &query)); @@ -6690,7 +6689,7 @@ mod tests { fn search_rejects_a_root_with_an_asap_operator() { let root = lower_promql( "quantile_over_time(0.99, m[1m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ); let space = search_workload(vec![("q", root)]).unwrap(); let summary = space @@ -6731,11 +6730,8 @@ mod tests { #[test] fn rate_candidate_inventories_have_no_timing_only_duplicates() { for (query, accuracy) in [ - ("sum by(job)(rate(m[1m]))", RequestedAccuracy::exact()), - ( - "topk by(job)(2, rate(m[1m]))", - RequestedAccuracy::epsilon(0.1), - ), + ("sum by(job)(rate(m[1m]))", AccuracyTarget::exact()), + ("topk by(job)(2, rate(m[1m]))", AccuracyTarget::epsilon(0.1)), ] { let root = lower_promql(query, accuracy); let inventory = search_workload(vec![(0usize, root)]) @@ -6757,7 +6753,7 @@ mod tests { // so materialization assignment can place it in precompute or at query time. #[test] fn grouped_rate_sum_inventory_keeps_sum_state_for_materialization_placement() { - let root = lower_promql("sum by(job)(rate(m[1m]))", RequestedAccuracy::exact()); + let root = lower_promql("sum by(job)(rate(m[1m]))", AccuracyTarget::exact()); let inventory = search_workload(vec![(0usize, root)]) .unwrap() .enumerate_candidate_dags(4096) @@ -6787,7 +6783,7 @@ mod tests { "sum by(job)(m)", "sum_over_time(m[1m])", ] { - let root = lower_promql(query, RequestedAccuracy::exact()); + let root = lower_promql(query, AccuracyTarget::exact()); let space = search_workload(vec![(0usize, root.clone())]).unwrap(); let inventory = space.enumerate_candidate_dags(4096).unwrap(); assert!(!inventory.candidates.is_empty()); @@ -6935,7 +6931,7 @@ mod tests { // Finite samples can overflow a sum although their native average is finite. #[test] fn temporal_average_requires_finite_division_guard() { - let root = lower_promql("avg_over_time(a[5m])", RequestedAccuracy::exact()); + let root = lower_promql("avg_over_time(a[5m])", AccuracyTarget::exact()); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); let operator = candidates @@ -6962,7 +6958,7 @@ mod tests { fn approximate_temporal_topk_admits_exact_maintained_values() { let root = lower_promql( "topk by(job)(1,count_over_time(a[5m]))", - RequestedAccuracy::epsilon_delta(0.01, 0.01), + AccuracyTarget::epsilon_delta(0.01, 0.01), ); let planning_inputs = CandidatePlanningInputs::with_default_accuracy(&crate::cost_model::DefaultCostModel); @@ -6980,7 +6976,7 @@ mod tests { "topk(5, sum_over_time(a[5m]))", "topk by(job)(5, count_over_time(a[5m]))", ] { - let root = lower_promql(query, RequestedAccuracy::exact()); + let root = lower_promql(query, AccuracyTarget::exact()); let planning_inputs = CandidatePlanningInputs::with_default_accuracy( &crate::cost_model::DefaultCostModel, ); @@ -7033,7 +7029,7 @@ mod tests { } } let requested = - RequestedAccuracy::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Relative); + AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Relative); let target = requested.target_in(ErrorMetric::Relative); let inputs = CandidatePlanningInputs { evidence: &Domain, @@ -7054,7 +7050,7 @@ mod tests { // Missing domain proof permits an uncertified direct quantile ratio only. #[test] fn quantile_ratio_without_input_proof_has_no_root_guarantee() { - let requested = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let requested = AccuracyTarget::epsilon_delta(0.01, 0.01); let target = requested.target_in(ErrorMetric::Relative); let root = lower_promql( "quantile_over_time(0.5,a[5m]) / quantile_over_time(0.9,a[5m])", @@ -9223,7 +9219,7 @@ mod tests { Some(false) } } - let root = lower_promql("sum_over_time(a[1m])", RequestedAccuracy::exact()); + let root = lower_promql("sum_over_time(a[1m])", AccuracyTarget::exact()); let space = search_workload(vec![("q", root)]).unwrap(); let selected = space.global_selection(&Unsupported); assert!(selected @@ -9236,10 +9232,7 @@ mod tests { // Composable temporal/grouped Sum must be executable as one producer. #[test] fn grouped_temporal_sum_has_one_summary_producer_candidate() { - let root = lower_promql( - "sum by(job)(sum_over_time(a[1m]))", - RequestedAccuracy::exact(), - ); + let root = lower_promql("sum by(job)(sum_over_time(a[1m]))", AccuracyTarget::exact()); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); assert!(candidates @@ -9309,10 +9302,7 @@ mod tests { )) } } - let root = lower_promql( - "sum by(job)(sum_over_time(a[1m]))", - RequestedAccuracy::exact(), - ); + let root = lower_promql("sum by(job)(sum_over_time(a[1m]))", AccuracyTarget::exact()); let space = search_workload(vec![("q", root)]).unwrap(); let selection = space.global_selection(&ExplicitCosts); let selected = selection diff --git a/crates/asap-aware-mapping/src/rewrite.rs b/crates/asap-aware-mapping/src/rewrite.rs index 4c16cac8b..74b5b0eba 100644 --- a/crates/asap-aware-mapping/src/rewrite.rs +++ b/crates/asap-aware-mapping/src/rewrite.rs @@ -437,7 +437,6 @@ mod tests { use asap_types::ir::operator_properties::Source; use asap_types::pre_asap::schema::{Field, Schema}; use asap_types::types::AccuracyTarget; - use asap_types::types::RequestedAccuracy; use std::time::Duration; use crate::test_support::metric_scan; @@ -472,10 +471,7 @@ mod tests { // Temporal averages expose two single-measure children without closing labels. #[test] fn temporal_average_components_preserves_schema_and_exposes_sum_count() { - let root = lower_promql( - "avg_over_time(a{job=\"api\"}[5m])", - RequestedAccuracy::exact(), - ); + let root = lower_promql("avg_over_time(a{job=\"api\"}[5m])", AccuracyTarget::exact()); assert!(SemanticEquivalentRewriteStrategy .replacements(&TargetSubDAG::new(&root)) .is_empty()); diff --git a/crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs b/crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs index c821f675b..37edf877c 100644 --- a/crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs +++ b/crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs @@ -257,7 +257,6 @@ mod tests { use asap_types::ir::ASAPOp; use asap_types::post_asap::ExecutionTiming; use asap_types::types::AccuracyTarget; - use asap_types::types::RequestedAccuracy; fn operator_root(candidate: &PhysicalCandidate, i: usize) -> Rc { match &candidate.roots[i] { @@ -298,7 +297,7 @@ mod tests { fn promql_topk_over_series_sorts_by_value_descending() { let root = lower_promql( "topk by (job) (10, sum_over_time(m[1m]))", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ); let candidate = optimize_physical_asap("L1", &[root.clone().into()]).unwrap(); let physical = operator_root(&candidate, 0); @@ -311,7 +310,7 @@ mod tests { /// `topk(k, sum by (job) (..))` sorts by the summed measure. #[test] fn promql_topk_over_grouped_aggregate_sorts_by_its_measure() { - let root = lower_promql("topk(5, sum by (job) (m))", RequestedAccuracy::exact()); + let root = lower_promql("topk(5, sum by (job) (m))", AccuracyTarget::exact()); let candidate = optimize_physical_asap("L1", &[root.into()]).unwrap(); let physical = operator_root(&candidate, 0); assert_eq!(names(&physical), ["topk_5"]); @@ -327,7 +326,7 @@ mod tests { fn consumer_of_exact_topk_reads_its_logical_columns() { let root = lower_promql( "sum by (job) (topk by (job) (10, sum_over_time(m[1m])))", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ); let candidate = optimize_physical_asap("L1", &[root.clone().into()]).unwrap(); let physical = operator_root(&candidate, 0); @@ -407,7 +406,7 @@ mod tests { /// child shared by two roots stays one node (one `Rc`, one DAG node). #[test] fn exact_topk_becomes_sort_then_limit_and_keeps_shared_child() { - let exact = RequestedAccuracy::exact(); + let exact = AccuracyTarget::exact(); let topk = lower_promql("topk by (job) (10, sum_over_time(m[1m]))", exact.clone()); let sum = lower_promql("sum by (job) (sum_over_time(m[1m]))", exact); // Share Q2's per-series child with Q1, as Stage 1 sharing would. @@ -480,7 +479,7 @@ mod tests { fn everything_runs_at_query_time() { let root = lower_promql( "topk by (job) (10, sum_over_time(m[1m]))", - RequestedAccuracy::epsilon_delta(0.01, 0.001), + AccuracyTarget::epsilon_delta(0.01, 0.001), ); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], diff --git a/crates/asap-aware-mapping/src/stage3_plan_selection.rs b/crates/asap-aware-mapping/src/stage3_plan_selection.rs index 0f1991c77..40d4393cd 100644 --- a/crates/asap-aware-mapping/src/stage3_plan_selection.rs +++ b/crates/asap-aware-mapping/src/stage3_plan_selection.rs @@ -171,6 +171,49 @@ pub struct Rejection { pub reason: String, } +/// The accuracy Stage 3 accepted for one query of a valid candidate: the +/// metric the query's target set (if any) and the metric of the guarantee +/// that met it. With no metric set, any metric meeting the target's ε, δ +/// and scope is accepted, so ε means different things across candidates +/// (e.g. 0.01 of the true value vs 0.01 of the rank); this record makes +/// the accepted metric visible. +#[derive(Debug, Clone, PartialEq)] +pub struct AcceptedAccuracy { + /// The target's metric; `None` when the user did not set one. + pub target_metric: Option, + /// The metric the accepted guarantee is stated in; `None` for an exact + /// output, which meets every metric. + pub accepted_metric: Option, + /// The sketch algorithms the query's output reads, in plan order + /// (e.g. `["Kll"]`); empty for an exact plan. + pub sketches: Vec, +} + +impl std::fmt::Display for AcceptedAccuracy { + /// E.g. `metric not set; accepted rank (Kll)`, or + /// `metric relative; accepted relative (DDSketch)`. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = |metric: ErrorMetric| { + serde_json::to_value(metric) + .ok() + .and_then(|value| value.as_str().map(str::to_owned)) + .unwrap_or_else(|| format!("{metric:?}")) + }; + match self.target_metric { + None => write!(f, "metric not set; ")?, + Some(metric) => write!(f, "metric {}; ", name(metric))?, + } + match self.accepted_metric { + None => write!(f, "accepted exact")?, + Some(metric) => write!(f, "accepted {}", name(metric))?, + } + if !self.sketches.is_empty() { + write!(f, " ({})", self.sketches.join(", "))?; + } + Ok(()) + } +} + /// Costs are present for valid candidates only. #[derive(Debug, Clone, PartialEq)] pub struct Selection { @@ -180,6 +223,9 @@ pub struct Selection { /// to run. The Stage 2 candidate keeps the open requirement. pub plan: Vec, pub costs: BTreeMap, + /// For each valid candidate, per query, the accuracy Stage 3 accepted + /// (`None` for a query without a target). + pub accepted: BTreeMap>>, pub rejected: Vec, } @@ -210,13 +256,6 @@ pub enum SelectionError { candidate: String, error: SizingError, }, - /// A query's requested accuracy cannot be resolved for its root, e.g. a - /// quantile query's target names no metric. - #[error("q{query}: {error}")] - AccuracyRequest { - query: usize, - error: asap_types::types::AccuracyRequestError, - }, } /// Stage 3 (plan selection): reject candidates that miss a query's accuracy @@ -231,6 +270,7 @@ pub fn select_plan( models: PlanningModels<'_>, ) -> Result { let mut costs = BTreeMap::new(); + let mut accepted = BTreeMap::new(); let mut rejected = Vec::new(); let mut best: Option<(String, f64, Vec)> = None; for candidate in cands { @@ -249,14 +289,17 @@ pub fn select_plan( error, } })?; - if let Some(reason) = accuracy_violation(candidate, targets, data, &models) { - rejected.push(Rejection { - id: candidate.id.clone(), - valid: false, - reason, - }); - continue; - } + let accepted_accuracy = match accuracy_check(candidate, targets, data, &models) { + Ok(accepted_accuracy) => accepted_accuracy, + Err(reason) => { + rejected.push(Rejection { + id: candidate.id.clone(), + valid: false, + reason, + }); + continue; + } + }; let calibration = illustrative_calibration(); let cost = price(&candidate.dag, recurrence, data, &calibration).map_err(|(node, error)| { @@ -273,6 +316,7 @@ pub fn select_plan( best = Some((candidate.id.clone(), cost.total, candidate.roots.clone())); } costs.insert(candidate.id.clone(), cost); + accepted.insert(candidate.id.clone(), accepted_accuracy); } let Some((selected, best_total, selected_roots)) = best else { return Err(SelectionError::NoValidCandidate(rejected)); @@ -300,6 +344,7 @@ pub fn select_plan( selected, plan, costs, + accepted, rejected, }) } @@ -504,8 +549,8 @@ pub fn sized_roots( with_resized_summaries(roots, &resize) } -/// The first query whose output is not shown to meet its target, as a -/// reason. An output meets its target when its theoretical root guarantee +/// What Stage 3 accepted for each query ([`AcceptedAccuracy`]), or the +/// first query whose output is not shown to meet its target, as a reason. An output meets its target when its theoretical root guarantee /// (composed in Stage 1 with the built-in model) satisfies it, or when the /// deployment's empirical evidence shows that this candidate's output does /// ([`crate::accuracy::AccuracyEvidenceProvider::output_guarantee`]). A @@ -516,15 +561,24 @@ pub fn sized_roots( /// weights proven non-negative, which its bound assumes: an open /// [`WeightDomain::SamplesOfMetric`] requirement is met when `data` declares /// the metric a counter ([`confirmed_weights`]), and unmet otherwise. -fn accuracy_violation( +/// +/// A target that sets no metric accepts a guarantee in any metric that meets +/// its ε, δ and scope ([`crate::accuracy::AccuracyModel::satisfies`]); one that sets a metric +/// accepts only guarantees in it (or convertible to it). +fn accuracy_check( candidate: &PhysicalCandidate, targets: &[Option], data: &DataWorkload, models: &PlanningModels<'_>, -) -> Option { +) -> Result>, String> { + let mut accepted = Vec::with_capacity(targets.len()); for (query, (root, target)) in candidate.roots.iter().zip(targets).enumerate() { let query = query + 1; - let Some(target) = target else { continue }; + let Some(target) = target else { + accepted.push(None); + continue; + }; + let mut sketches: Vec = Vec::new(); for node in root_operators(root) .iter() .flat_map(|node| OperatorNode::reachable(node)) @@ -535,6 +589,12 @@ fn accuracy_violation( else { continue; }; + if let FieldDataType::Sketch(kind, _) = family { + let name = format!("{:?}", kind.algorithm()); + if !sketches.contains(&name) { + sketches.push(name); + } + } if matches!(family, FieldDataType::Sketch(kind, _) if matches!(kind.algorithm(), SketchAlgorithm::Cms | SketchAlgorithm::CmsWithHeap)) && !matches!( @@ -548,7 +608,7 @@ fn accuracy_violation( } _ => String::new(), }; - return Some(format!( + return Err(format!( "q{query}: {} needs non-negative update weights, and these are not proven \ non-negative{unconfirmed}", family_name(family) @@ -564,17 +624,22 @@ fn accuracy_violation( }; let guarantee = bound_root_guarantee(root, data, models).map(over_scope); let meets = |g: &AccuracyGuarantee| models.accuracy.satisfies(g, target); - if guarantee.as_ref().is_some_and(meets) - || models + let met = guarantee.clone().filter(meets).or_else(|| { + models .evidence .output_guarantee(root) .map(over_scope) - .as_ref() - .is_some_and(meets) - { + .filter(meets) + }); + if let Some(met) = met { + accepted.push(Some(AcceptedAccuracy { + target_metric: target.metric, + accepted_metric: (!met.is_exact()).then_some(met.spec.metric), + sketches, + })); continue; } - return Some(match guarantee { + return Err(match guarantee { Some(g) if values.is_none() && target.scope == AccuracyScope::WholeResult @@ -603,7 +668,7 @@ fn accuracy_violation( ), }); } - None + Ok(accepted) } /// `domain` with an open [`WeightDomain::SamplesOfMetric`] requirement @@ -1688,7 +1753,7 @@ mod tests { BoundExpr, ErrorMetric, GroupingStrategy, ProbabilityExpr, SketchKind, }; use asap_types::pre_asap::expr_ir::ArithmeticOpKind; - use asap_types::types::RequestedAccuracy; + use asap_types::types::AccuracyTarget; use asap_types::workload::{Evidence, Rate}; /// `n` queries repeating every 10 s, as Example 1's panels do. @@ -1743,7 +1808,7 @@ mod tests { fn candidates() -> Vec { let root = lower_promql( "topk by (job) (10, sum_over_time(m[1m]))", - RequestedAccuracy::epsilon_delta(0.01, 0.001), + AccuracyTarget::epsilon_delta(0.01, 0.001), ); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], @@ -1778,7 +1843,7 @@ mod tests { /// estimates carry no guarantee since #575, so a quantile summary is used.) #[test] fn accuracy_failing_candidate_is_rejected_as_invalid() { - let planned = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let planned = AccuracyTarget::epsilon_delta(0.01, 0.001); let candidates = all_candidates("quantile_over_time(0.99, m[1m])", &planned); let strict = AccuracyTarget::epsilon_delta(1e-6, 1e-9); let summaries: Vec<_> = candidates @@ -1826,7 +1891,7 @@ mod tests { /// non-negative is invalid. #[test] fn count_min_over_signed_weights_is_rejected_as_invalid() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let selection = select_plan( &candidates(), &[Some(target.target_in(ErrorMetric::ABSOLUTE_STREAM_L1))], @@ -1847,7 +1912,7 @@ mod tests { #[test] fn count_min_over_samples_needs_a_declared_counter() { use asap_types::workload::MetricType; - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let with_types = |types: &[(&str, MetricType)]| DataWorkload { metric_types: types.iter().map(|(m, t)| (m.to_string(), *t)).collect(), ..data() @@ -1953,7 +2018,7 @@ mod tests { ProbabilityExpr::Constant { value: 0.001 }, vec![], ); - let node = lower_promql("sum(m)", RequestedAccuracy::exact()); + let node = lower_promql("sum(m)", AccuracyTarget::exact()); let root = QueryRoot::Operator(Rc::new((*node).clone().with_guarantee(Some(gated)))); let with = |types: &[(&str, MetricType)]| DataWorkload { metric_types: types.iter().map(|(m, t)| (m.to_string(), *t)).collect(), @@ -1983,7 +2048,7 @@ mod tests { #[test] fn the_selected_plan_carries_confirmed_weights() { use asap_types::workload::MetricType; - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let root = lower_promql("topk by (job) (10, sum_over_time(m[1m]))", target.clone()); let root = asap_types::ir::schema_support::with_promql_series_identity(&root).unwrap(); let data = DataWorkload { @@ -2098,7 +2163,7 @@ mod tests { #[test] fn per_node_costs_cover_the_dag_and_sum_to_the_total() { let candidates = candidates(); - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let selection = select_plan( &candidates, &[Some(target.target_in(ErrorMetric::ABSOLUTE_STREAM_L1))], @@ -2130,7 +2195,7 @@ mod tests { /// and the nodes below it once, since they run only when it does. #[test] fn not_materialized_shared_nodes_are_charged_per_query() { - let outer = lower_promql("sum(sum_over_time(m[1m]))", RequestedAccuracy::exact()); + let outer = lower_promql("sum(sum_over_time(m[1m]))", AccuracyTarget::exact()); let inner = outer.children()[0].clone(); let mut candidate = optimize_physical_asap( "L", @@ -2178,7 +2243,7 @@ mod tests { /// Every combination of Pass 1 candidates of one query, as Stage 2 /// candidates `P1`, `P2`, ... - fn all_candidates(query: &str, target: &RequestedAccuracy) -> Vec { + fn all_candidates(query: &str, target: &AccuracyTarget) -> Vec { let root = lower_promql(query, target.clone()); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], @@ -2226,7 +2291,7 @@ mod tests { /// The KLL candidates of `quantile_over_time(0.99, x[5m])`, per series, /// within rank error 0.01 at δ = 0.01 over `scope`. fn per_series_kll(scope: AccuracyScope) -> (Vec, AccuracyTarget) { - let requested = RequestedAccuracy::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank); + let requested = AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank); let candidates = all_candidates("quantile_over_time(0.99, x[5m])", &requested) .into_iter() .filter(|c| !kll_params(&c.roots).is_empty()) @@ -2341,9 +2406,8 @@ mod tests { #[test] fn a_root_of_at_most_one_row_returns_one_value() { let none = DataWorkload::default(); - let root = |query: &str| { - QueryRoot::Operator(lower_promql(query, RequestedAccuracy::epsilon(0.01))) - }; + let root = + |query: &str| QueryRoot::Operator(lower_promql(query, AccuracyTarget::epsilon(0.01))); assert_eq!(output_values(&root("sum(rate(x[5m]))"), &none), Some(1.0)); assert_eq!(output_values(&root("topk(5, rate(x[5m]))"), &none), None); assert_eq!( @@ -2420,7 +2484,7 @@ mod tests { ..data() }; let target = - RequestedAccuracy::epsilon_delta(0.5, 0.5).target_in(ErrorMetric::ABSOLUTE_STREAM_L1); + AccuracyTarget::epsilon_delta(0.5, 0.5).target_in(ErrorMetric::ABSOLUTE_STREAM_L1); let select = |metric| { select_plan( &candidates(), @@ -2456,7 +2520,7 @@ mod tests { /// output admits them. #[test] fn combined_estimates_need_evidence_about_the_output() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let candidates = all_candidates( "quantile_over_time(0.99, m[1m]) - quantile_over_time(0.5, m[1m])", &target, @@ -2525,7 +2589,7 @@ mod tests { /// query runs is not charged.) #[test] fn exact_quantile_state_costs_more_than_a_sketch_over_large_groups() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); for (query, sketch_wins) in [ ("quantile(0.99, rate(x[5m]))", true), ("quantile by (job) (0.99, rate(x[5m]))", false), @@ -2556,7 +2620,7 @@ mod tests { #[test] fn exact_l2_state_is_priced_like_exact_quantile_state() { let aggregate_cost = |query: &str| { - let candidates = all_candidates(query, &RequestedAccuracy::exact()); + let candidates = all_candidates(query, &AccuracyTarget::exact()); let selection = select_plan( &candidates, &[None], @@ -2725,7 +2789,7 @@ mod tests { /// sketch is. #[test] fn exact_distinct_count_beats_a_sketch_over_small_groups() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let one_series = DataWorkload { input_cardinality: Evidence { value: Some(1), @@ -2780,7 +2844,7 @@ mod tests { /// At δ = 0.001 a KLL candidate meets the target and is valid. #[test] fn kll_candidate_meets_a_failure_probability_below_one_percent() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let candidates = all_candidates("quantile(0.99, rate(x[5m]))", &target); let selection = select_plan( &candidates, @@ -2809,7 +2873,7 @@ mod tests { /// operands only is exact. #[test] fn scalar_root_guarantees_compose_through_arithmetic() { - let node = lower_promql("sum(m)", RequestedAccuracy::exact()); + let node = lower_promql("sum(m)", AccuracyTarget::exact()); let approximate = AccuracyGuarantee::new( ErrorMetric::ABSOLUTE, BoundExpr::Constant { value: 0.01 }, @@ -2855,7 +2919,7 @@ mod tests { /// Every candidate of the Stage 1 to Stage 3 run of one query. fn run( query: &str, - target: &RequestedAccuracy, + target: &AccuracyTarget, data: &DataWorkload, models: PlanningModels<'_>, ) -> (Vec, Selection) { @@ -2866,8 +2930,8 @@ mod tests { /// against `selected`. fn run_selecting( query: &str, - planned: &RequestedAccuracy, - selected: &RequestedAccuracy, + planned: &AccuracyTarget, + selected: &AccuracyTarget, data: &DataWorkload, models: PlanningModels<'_>, ) -> (Vec, Selection) { @@ -2916,7 +2980,7 @@ mod tests { /// result is a rank interval: no guarantee in the MVP. #[test] fn extremum_and_scaling_over_per_series_estimates_compose() { - let target = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Relative); for query in [ "max by (job) (quantile_over_time(0.99, m[1m]))", "2 * max by (job) (quantile_over_time(0.99, m[1m]))", @@ -2948,7 +3012,7 @@ mod tests { /// guarantee in the MVP, so those are not. #[test] fn lipschitz_functions_keep_and_shifts_and_comparisons_drop_a_relative_bound() { - let target = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Relative); let q = "quantile_over_time(0.99, m[1m])"; for (query, kept) in [ (format!("-{q}"), true), @@ -2987,7 +3051,7 @@ mod tests { /// the query, and the fold has no guarantee. #[test] fn a_fold_over_subquery_estimates_counts_its_steps() { - let target = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Relative); let q = "quantile_over_time(0.99, m[1m])"; let (candidates, selection) = run( &format!("max_over_time({q}[10m:1m])"), @@ -3028,7 +3092,7 @@ mod tests { /// are labels, not the estimates (#669). #[test] fn counting_estimates_by_a_label_stays_exact() { - let target = RequestedAccuracy::epsilon(0.01); + let target = AccuracyTarget::epsilon(0.01); let (candidates, _) = run( "count by (le) (quantile_over_time(0.99, m[1m]))", &target, @@ -3061,7 +3125,7 @@ mod tests { /// symbolic and plan selection treats it as unmet. #[test] fn average_over_relative_estimates_needs_the_sign() { - let target = RequestedAccuracy::epsilon(0.01); + let target = AccuracyTarget::epsilon(0.01); let (candidates, selection) = run( "avg by (job) (quantile_over_time(0.99, m[1m]))", &target, @@ -3085,8 +3149,7 @@ mod tests { /// workload's input cardinality; without one, the target is unmet. #[test] fn stage3_binds_the_input_row_count_from_the_data_workload() { - let target = - RequestedAccuracy::epsilon_delta(0.01, 0.001).with_metric(ErrorMetric::Relative); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001).with_metric(ErrorMetric::Relative); let query = "max by (job) (quantile_over_time(0.99, m[1m]))"; let ddsketch = |candidates: &[PhysicalCandidate]| -> Vec { candidates @@ -3132,8 +3195,8 @@ mod tests { /// twice that, is checked against a looser one. #[test] fn ddsketch_ratio_meets_its_target_only_with_operand_domains() { - let planned = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); - let target = RequestedAccuracy::epsilon(0.05).with_metric(ErrorMetric::Relative); + let planned = AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = AccuracyTarget::epsilon(0.05).with_metric(ErrorMetric::Relative); let query = "quantile_over_time(0.99, m[1m]) / quantile_over_time(0.5, m[1m])"; let run = |models| run_selecting(query, &planned, &target, &data(), models); let (candidates, without) = run(PlanningModels::builtin()); @@ -3151,7 +3214,7 @@ mod tests { /// `query` carrying `guarantee`, as a `scalar(…)` operand. fn scalar_of(query: &str, guarantee: AccuracyGuarantee) -> ScalarExpr { - let node = lower_promql(query, RequestedAccuracy::exact()); + let node = lower_promql(query, AccuracyTarget::exact()); ScalarExpr::PromqlScalarFromVector(Rc::new((*node).clone().with_guarantee(Some(guarantee)))) } @@ -3324,8 +3387,8 @@ mod tests { /// inherited it, so the candidate is selectable. #[test] fn a_scaled_ratio_binds_at_its_division() { - let planned = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); - let target = RequestedAccuracy::epsilon(0.05).with_metric(ErrorMetric::Relative); + let planned = AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = AccuracyTarget::epsilon(0.05).with_metric(ErrorMetric::Relative); let query = "(quantile_over_time(0.99, m[1m]) / quantile_over_time(0.5, m[1m])) \ / max_over_time(m[1m])"; let (candidates, selection) = run_selecting( @@ -3352,7 +3415,7 @@ mod tests { /// have expired) is not used; timeless declared evidence is. #[test] fn stage3_binds_only_fresh_input_cardinality() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); + let target = AccuracyTarget::epsilon_delta(0.01, 0.001); let query = "max by (job) (quantile_over_time(0.99, m[1m]))"; let expiring = DataWorkload { input_cardinality: Evidence { @@ -3382,7 +3445,7 @@ mod tests { }; let root = lower_promql( "quantile_over_time(0.99, m[1m])", - RequestedAccuracy::epsilon_delta(0.01, 0.01), + AccuracyTarget::epsilon_delta(0.01, 0.01), ); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], @@ -3428,7 +3491,7 @@ mod tests { /// The physical DAG of `query`'s first per-computation candidates, /// with nothing materialized. fn first_candidate_dag(query: &str) -> PhysicalASAPDAG { - let root = lower_promql(query, RequestedAccuracy::exact()); + let root = lower_promql(query, AccuracyTarget::exact()); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], ) @@ -3556,7 +3619,7 @@ mod tests { "quantile_over_time(0.99, m[1m])", "quantile by (job) (0.99, m)", ] { - let target = RequestedAccuracy::exact(); + let target = AccuracyTarget::exact(); let root = lower_promql(query, target); let dag = optimize_physical_asap("L", &[QueryRoot::Operator(root)]) .unwrap() @@ -3593,7 +3656,7 @@ mod tests { #[test] fn kept_exact_state_is_charged_by_the_values_it_keeps() { let kept = |query: &str| { - let root = lower_promql(query, RequestedAccuracy::exact()); + let root = lower_promql(query, AccuracyTarget::exact()); let mut dag = optimize_physical_asap("L", &[QueryRoot::Operator(root)]) .unwrap() .dag; @@ -3628,7 +3691,7 @@ mod tests { /// rate, where queries at the same interval run together. #[test] fn rates_follow_per_consumer_charging() { - let root = lower_promql("sum by (job) (rate(m[1m]))", RequestedAccuracy::exact()); + let root = lower_promql("sum by (job) (rate(m[1m]))", AccuracyTarget::exact()); let one = optimize_physical_asap("L", &[QueryRoot::Operator(root.clone())]) .unwrap() .dag; @@ -3663,7 +3726,7 @@ mod tests { /// invocations × per-evaluation cost / horizon_s. #[test] fn one_off_recurrence_is_spread_over_the_horizon() { - let root = lower_promql("sum by (job) (rate(m[1m]))", RequestedAccuracy::exact()); + let root = lower_promql("sum by (job) (rate(m[1m]))", AccuracyTarget::exact()); let dag = optimize_physical_asap("L", &[QueryRoot::Operator(root)]) .unwrap() .dag; diff --git a/crates/asap-aware-mapping/src/test_support.rs b/crates/asap-aware-mapping/src/test_support.rs index 613c6ddc3..41dbc1844 100644 --- a/crates/asap-aware-mapping/src/test_support.rs +++ b/crates/asap-aware-mapping/src/test_support.rs @@ -4,13 +4,13 @@ use std::rc::Rc; use asap_types::ir::OperatorNode; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, }; -pub(crate) fn lower_promql(query: &str, accuracy: RequestedAccuracy) -> Rc { +pub(crate) fn lower_promql(query: &str, accuracy: AccuracyTarget) -> Rc { let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -216,7 +216,7 @@ pub(crate) fn time_and_export( pub(crate) fn cms_summary(query: &str) -> OperatorNode { use asap_types::ir::{ASAPOp, QueryRoot}; use asap_types::post_asap::{FieldDataType, SketchAlgorithm}; - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let root = lower_promql(query, target.clone()); let run = crate::pass::run_stage1_to_stage3( vec![(0, QueryRoot::Operator(root))], diff --git a/crates/asap-physical-operators/src/physical_planner/candidates.rs b/crates/asap-physical-operators/src/physical_planner/candidates.rs index b3a3ec170..bb3c1e343 100644 --- a/crates/asap-physical-operators/src/physical_planner/candidates.rs +++ b/crates/asap-physical-operators/src/physical_planner/candidates.rs @@ -351,7 +351,7 @@ mod tests { query: Query("sum by(job)(rate(m[1m]))".into()), requirements: QueryRequirements { accuracy: AccuracyRequirement::Explicit( - planner_types::types::RequestedAccuracy::exact(), + planner_types::types::AccuracyTarget::exact(), ), ..Default::default() }, @@ -378,7 +378,7 @@ mod tests { // ingestion time. let run = asap_aware_mapping::run_stage1_to_stage3( vec![(0, planner_types::ir::QueryRoot::Operator(root))], - &[Some(planner_types::types::RequestedAccuracy::exact())], + &[Some(planner_types::types::AccuracyTarget::exact())], &[planner_types::workload::QueryRecurrence::Unknown], &workload.data_workload.unwrap(), asap_aware_mapping::PlanningModels::builtin(), diff --git a/crates/asap-physical-operators/tests/common/mod.rs b/crates/asap-physical-operators/tests/common/mod.rs index ada16b2f0..d04894d4e 100644 --- a/crates/asap-physical-operators/tests/common/mod.rs +++ b/crates/asap-physical-operators/tests/common/mod.rs @@ -21,7 +21,7 @@ pub fn compile_physical_asap_dag( /// with the built-in models. pub fn selected_dag( root: Rc, - accuracy: planner_types::types::RequestedAccuracy, + accuracy: planner_types::types::AccuracyTarget, ) -> Rc { let run = run_stage1_to_stage3(root, accuracy); let [QueryRoot::Operator(selected)] = run.selected_logical().expect("a plan is selected") @@ -36,7 +36,7 @@ pub fn selected_dag( /// compose panics, so none is silently skipped. pub fn logical_candidates( root: Rc, - accuracy: planner_types::types::RequestedAccuracy, + accuracy: planner_types::types::AccuracyTarget, ) -> Vec> { run_stage1_to_stage3(root, accuracy) .stage1_logical_asap_optimization @@ -56,7 +56,7 @@ pub fn logical_candidates( fn run_stage1_to_stage3( root: Rc, - accuracy: planner_types::types::RequestedAccuracy, + accuracy: planner_types::types::AccuracyTarget, ) -> asap_aware_mapping::Stage1ToStage3Run { use planner_types::workload::{DataArrival, DataWorkload, Evidence, EvidenceSource, Rate}; let data = DataWorkload { diff --git a/crates/asap-physical-operators/tests/current_series_heap.rs b/crates/asap-physical-operators/tests/current_series_heap.rs index 5ab8f9280..35fa03838 100644 --- a/crates/asap-physical-operators/tests/current_series_heap.rs +++ b/crates/asap-physical-operators/tests/current_series_heap.rs @@ -12,7 +12,7 @@ use asap_physical_operators::{ use common::compile_physical_asap_dag; use futures::{executor::block_on, StreamExt}; use planner_types::ir::physical_export::PhysicalASAPOperatorPayload; -use planner_types::types::RequestedAccuracy; +use planner_types::types::AccuracyTarget; use planner_types::{post_asap::*, pre_asap::DataType}; use std::{collections::BTreeMap, sync::Arc}; @@ -305,7 +305,7 @@ fn planner_current_series_candidate_compiles_with_dynamic_identity() { query_batch: Some(vec![BatchEntry { query: Query("topk by(job)(1, m)".into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::exact()), ..Default::default() }, predictability: Predictability::Unknown, diff --git a/crates/asap-physical-operators/tests/deployment_computation.rs b/crates/asap-physical-operators/tests/deployment_computation.rs index 124e7f4aa..732dd7b51 100644 --- a/crates/asap-physical-operators/tests/deployment_computation.rs +++ b/crates/asap-physical-operators/tests/deployment_computation.rs @@ -11,14 +11,14 @@ use common::{compile_physical_asap_dag, logical_candidates, selected_dag}; use futures::{executor::block_on, StreamExt}; use planner_types::ir::physical_export::{PhysicalASAPDAG, PhysicalASAPOperatorPayload}; use planner_types::ir::ASAPOp; -use planner_types::{post_asap::*, types::RequestedAccuracy, workload::*}; +use planner_types::{post_asap::*, types::AccuracyTarget, workload::*}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; fn lower(query: &str) -> Rc { - lower_with(query, RequestedAccuracy::exact()) + lower_with(query, AccuracyTarget::exact()) } -fn lower_with(query: &str, accuracy: RequestedAccuracy) -> Rc { +fn lower_with(query: &str, accuracy: AccuracyTarget) -> Rc { let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -52,7 +52,7 @@ fn lower_with(query: &str, accuracy: RequestedAccuracy) -> Rc PhysicalASAPDAG { let expression = lower(query); let root = promql_rows::with_series_identity(&expression).unwrap_or(expression); - compile_physical_asap_dag(&selected_dag(root, RequestedAccuracy::exact())).unwrap() + compile_physical_asap_dag(&selected_dag(root, AccuracyTarget::exact())).unwrap() } fn population_dag(query: &str) -> PhysicalASAPDAG { @@ -614,7 +614,7 @@ fn population_sums_and_averages_are_compensated() { #[test] fn stored_count_min_bare_count_compiles_to_a_evaluation() { use asap_physical_operators::summary_kernels::CountMinSketchAccumulator; - let accuracy = RequestedAccuracy::epsilon(0.02); + let accuracy = AccuracyTarget::epsilon(0.02); let root = lower_with("count(up)", accuracy.clone()); let dag = logical_candidates(root, accuracy) .into_iter() diff --git a/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs b/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs index d9e1876e6..fd0d67048 100644 --- a/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs +++ b/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs @@ -7,7 +7,7 @@ use planner_types::ir::OperatorNode; use asap_physical_operators::physical_planner::promql_rows::SERIES_IDENTITY_COLUMN; use planner_types::{ - types::RequestedAccuracy, + types::AccuracyTarget, workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence as WorkloadEvidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, @@ -16,7 +16,7 @@ use planner_types::{ }; use std::rc::Rc; -fn lower(query: &str, accuracy: &RequestedAccuracy) -> Rc { +fn lower(query: &str, accuracy: &AccuracyTarget) -> Rc { let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -63,7 +63,7 @@ const CURRENT_SERIES_TOPK: &str = "topk by(job)(1, m)"; #[test] fn selection_never_commits_a_series_identity_heap() { - let accuracy = RequestedAccuracy::epsilon(0.1); + let accuracy = AccuracyTarget::epsilon(0.1); let root = lower(CURRENT_SERIES_TOPK, &accuracy); assert!(!carries_identity(&selected_dag(root, accuracy))); } diff --git a/crates/asap-physical-operators/tests/precompute_candidates.rs b/crates/asap-physical-operators/tests/precompute_candidates.rs index 0391ccdcd..8ee68a1b9 100644 --- a/crates/asap-physical-operators/tests/precompute_candidates.rs +++ b/crates/asap-physical-operators/tests/precompute_candidates.rs @@ -15,7 +15,7 @@ use common::{compile_physical_asap_dag, logical_candidates, selected_dag}; use futures::{executor::block_on, StreamExt}; use planner_types::ir::physical_export::{PhysicalASAPDAG, PhysicalASAPOperatorPayload}; use planner_types::ir::ASAPOp; -use planner_types::types::RequestedAccuracy; +use planner_types::types::AccuracyTarget; use planner_types::{post_asap::*, pre_asap::DataType, workload::*}; use std::{collections::BTreeMap, sync::Arc}; @@ -32,7 +32,7 @@ fn exact_promql_root(query: &str) -> std::rc::Rc std::rc::Rc PhysicalASAPDAG { - compile_physical_asap_dag(&selected_dag( - grouped_rate_root(), - RequestedAccuracy::exact(), - )) - .unwrap() + compile_physical_asap_dag(&selected_dag(grouped_rate_root(), AccuracyTarget::exact())).unwrap() } fn run(plan: &CompiledPhysicalDAG, inputs: BTreeMap, scope: Scope) -> Vec { let sources = inputs @@ -419,7 +415,7 @@ fn bounded_inventory_exposes_grouped_rate_physical_frontiers() { #[test] fn enumerated_grouped_rate_candidates_execute_numeric_query_outputs() { let mut executed = 0; - for root in logical_candidates(grouped_rate_root(), RequestedAccuracy::exact()) { + for root in logical_candidates(grouped_rate_root(), AccuracyTarget::exact()) { let dag = compile_physical_asap_dag(&root).unwrap(); let Some(state) = dag.nodes.iter().find(|node| { matches!( @@ -637,7 +633,7 @@ fn population_topk_cuts_equal_per_frontier_compilation() { query_batch: Some(vec![BatchEntry { query: Query("topk by(job)(1, m)".into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::exact()), ..Default::default() }, predictability: Predictability::Unknown, @@ -747,6 +743,6 @@ fn cut_candidate_rejects_invalid_frontiers() { fn logical_candidates_fails_on_a_candidate_that_does_not_compose() { logical_candidates( exact_promql_root("sum by (job) (topk by (job, instance) (1, rate(m[5m])))"), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ); } diff --git a/crates/asap-physical-operators/tests/weighted_topk_binding.rs b/crates/asap-physical-operators/tests/weighted_topk_binding.rs index e0fc7c808..666aef86c 100644 --- a/crates/asap-physical-operators/tests/weighted_topk_binding.rs +++ b/crates/asap-physical-operators/tests/weighted_topk_binding.rs @@ -10,7 +10,7 @@ use common::{compile_physical_asap_dag, logical_candidates}; use futures::{executor::block_on, StreamExt}; use planner_types::ir::physical_export::{PhysicalASAPDAG, PhysicalASAPOperatorPayload}; use planner_types::ir::ASAPOp; -use planner_types::{post_asap::*, pre_asap::DataType, types::RequestedAccuracy}; +use planner_types::{post_asap::*, pre_asap::DataType, types::AccuracyTarget}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; #[test] #[ignore = "the whole-expression heap's SummaryEstimate{TopK} output is not the keyed-evaluation \ @@ -37,7 +37,7 @@ fn heap_candidate( root: Rc, algorithm: &SketchAlgorithm, ) -> Rc { - logical_candidates(root, RequestedAccuracy::epsilon(0.1)) + logical_candidates(root, AccuracyTarget::epsilon(0.1)) .into_iter() .find(|candidate| builds(&compile_physical_asap_dag(candidate).unwrap(), algorithm)) .unwrap_or_else(|| panic!("missing {algorithm:?} candidate")) @@ -46,7 +46,7 @@ fn heap_candidate( fn assert_weighted_binding(algorithm: SketchAlgorithm) { let root = lower_promql( "topk by(job)(2, sum by(service, job)(rate(m[1m])))", - RequestedAccuracy::epsilon(0.1), + AccuracyTarget::epsilon(0.1), ) .unwrap(); let plan = heap_candidate(root, &algorithm); @@ -182,7 +182,7 @@ use planner_types::workload::{ }; pub fn lower_promql( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result, asap_frontend_promql::PromqlError> { let workload = PlanningWorkload { query_workload: QueryWorkload { @@ -257,11 +257,8 @@ fn direct_rate_topk_preserves_dynamic_unreferenced_labels() { use asap_physical_operators::physical_planner::promql_rows::{ decode_series_identity, series_row, with_series_identity, SERIES_IDENTITY_COLUMN, }; - let logical = lower_promql( - "topk by(job)(2, rate(m[1m]))", - RequestedAccuracy::epsilon(0.1), - ) - .unwrap(); + let logical = + lower_promql("topk by(job)(2, rate(m[1m]))", AccuracyTarget::epsilon(0.1)).unwrap(); let root = with_series_identity(&logical).unwrap(); for algorithm in [SketchAlgorithm::CountSketchWithHeap] { let candidate = &heap_candidate(Rc::clone(&root), &algorithm); @@ -562,11 +559,7 @@ fn maintained_rate_heap_compiles_fixed_window_precompute() { }; let root = Rc::new( with_series_identity( - &lower_promql( - "topk by(job)(2, rate(m[1m]))", - RequestedAccuracy::epsilon(0.1), - ) - .unwrap(), + &lower_promql("topk by(job)(2, rate(m[1m]))", AccuracyTarget::epsilon(0.1)).unwrap(), ) .unwrap(), ); diff --git a/crates/devtools/examples/canonical_examples.rs b/crates/devtools/examples/canonical_examples.rs index 29a0d5d31..7f78e66c9 100644 --- a/crates/devtools/examples/canonical_examples.rs +++ b/crates/devtools/examples/canonical_examples.rs @@ -6,7 +6,7 @@ use asap_devtools::lower_promql_with_data_ingestion_interval; use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; fn col(name: &str, dtype: DataType) -> Field { @@ -70,7 +70,7 @@ async fn main() { ]; for (label, q) in promql_examples { println!("=== {label} === promql> {q}"); - match lower_promql_with_data_ingestion_interval(q, RequestedAccuracy::exact(), 1_000) { + match lower_promql_with_data_ingestion_interval(q, AccuracyTarget::exact(), 1_000) { Ok(qe) => println!("{qe:#?}"), Err(e) => println!("ERR: {e}"), } @@ -102,7 +102,7 @@ async fn main() { q, catalog, SqlDialect::DataFusionSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await { diff --git a/crates/devtools/examples/topk_ir.rs b/crates/devtools/examples/topk_ir.rs index d9ec9ccdf..16e800dbe 100644 --- a/crates/devtools/examples/topk_ir.rs +++ b/crates/devtools/examples/topk_ir.rs @@ -5,7 +5,7 @@ use asap_devtools::{lower_promql_with_data_ingestion_interval, lower_sql, SqlCatalog}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn col(name: &str, dtype: DataType) -> Field { Field::plain(name, dtype, false) @@ -39,7 +39,7 @@ fn catalog() -> SqlCatalog { async fn show_sql(label: &str, query: &str) { println!("━━━ {label} ━━━"); println!("{query}"); - match lower_sql(query, &catalog(), RequestedAccuracy::exact()).await { + match lower_sql(query, &catalog(), AccuracyTarget::exact()).await { Ok(qe) => println!("{qe:#?}"), Err(e) => println!("ERR: {e}"), } @@ -49,7 +49,7 @@ async fn show_sql(label: &str, query: &str) { fn show_promql(label: &str, query: &str) { println!("━━━ {label} ━━━"); println!("{query}"); - match lower_promql_with_data_ingestion_interval(query, RequestedAccuracy::exact(), 1_000) { + match lower_promql_with_data_ingestion_interval(query, AccuracyTarget::exact(), 1_000) { Ok(qe) => println!("{qe:#?}"), Err(e) => println!("ERR: {e}"), } diff --git a/crates/devtools/src/bin/analyze_corpora.rs b/crates/devtools/src/bin/analyze_corpora.rs index 4b6b3aafb..8279acdf9 100644 --- a/crates/devtools/src/bin/analyze_corpora.rs +++ b/crates/devtools/src/bin/analyze_corpora.rs @@ -7,7 +7,7 @@ use asap_devtools::{lower_promql_with_data_ingestion_interval, SqlCatalog}; use asap_frontend_sql::lower_sql_dialect; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; use serde::Serialize; use serde_json::Value; @@ -208,7 +208,7 @@ fn run_corpus(name: &str, source: &str, interval_ms: u64) -> CorpusResult { let structural_shape = structural_shape(expression); match lower_promql_with_data_ingestion_interval( expression, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), interval_ms, ) { Ok(ir) => result.lowered.push(DumpRecord { @@ -482,7 +482,7 @@ async fn run_sql_corpora(out_dir: PathBuf) { &expression, &catalog, dialect.clone(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await { diff --git a/crates/devtools/src/bin/show_logical_dag.rs b/crates/devtools/src/bin/show_logical_dag.rs index 4a3a7ff97..f6516279c 100644 --- a/crates/devtools/src/bin/show_logical_dag.rs +++ b/crates/devtools/src/bin/show_logical_dag.rs @@ -19,7 +19,7 @@ use asap_devtools::{lower_promql_with_data_ingestion_interval, lower_sql, SqlCatalog}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use std::io::Read; fn col(name: &str, dtype: DataType) -> Field { @@ -77,14 +77,14 @@ async fn main() { } println!("━━━ {line} ━━━"); if let Some(q) = line.strip_prefix("sql>") { - match lower_sql(q.trim(), &catalog, RequestedAccuracy::exact()).await { + match lower_sql(q.trim(), &catalog, AccuracyTarget::exact()).await { Ok(qe) => println!("{qe:#?}"), Err(e) => println!("ERR: {e}"), } } else if let Some(q) = line.strip_prefix("promql>") { match lower_promql_with_data_ingestion_interval( q.trim(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), interval_ms, ) { Ok(qe) => println!("{qe:#?}"), diff --git a/crates/devtools/src/bin/sketch_coverage.rs b/crates/devtools/src/bin/sketch_coverage.rs index e158aa2db..9bfae2a23 100644 --- a/crates/devtools/src/bin/sketch_coverage.rs +++ b/crates/devtools/src/bin/sketch_coverage.rs @@ -2,12 +2,12 @@ // // Lowers every query in every corpus we have (mirrors `variant_coverage`'s // corpus list exactly, so the two reports are directly comparable) with an -// *approximate* `RequestedAccuracy`, runs Pass 1 of logical ASAP-aware +// *approximate* `AccuracyTarget`, runs Pass 1 of logical ASAP-aware // optimization (`generate_per_computation_candidates`) over each query, and // reports which summary candidates Pass 1 offers for it, plus the fraction of // lowered queries with at least one. // -// `--epsilon ` (default 0.01) sets the `RequestedAccuracy` every query in +// `--epsilon ` (default 0.01) sets the `AccuracyTarget` every query in // every corpus lowers with. Without an approximate target, Pass 1 offers no // summary candidate. @@ -18,7 +18,7 @@ use asap_devtools::lower_promql_with_data_ingestion_interval; use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_types::ir::{OperatorNode, QueryRoot}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; use std::collections::BTreeSet; use std::rc::Rc; @@ -219,7 +219,7 @@ fn parse_ingestion_interval() -> u64 { async fn main() { let epsilon = parse_epsilon(); let interval_ms = parse_ingestion_interval(); - let accuracy = RequestedAccuracy::epsilon(epsilon); + let accuracy = AccuracyTarget::epsilon(epsilon); let mut results = Vec::new(); // ── PromQL corpora — same set as `variant_coverage` ── diff --git a/crates/devtools/src/bin/stage_pipeline.rs b/crates/devtools/src/bin/stage_pipeline.rs index f965c8a7e..c66ea0c93 100644 --- a/crates/devtools/src/bin/stage_pipeline.rs +++ b/crates/devtools/src/bin/stage_pipeline.rs @@ -28,7 +28,10 @@ // cost; a composed logical candidate Stage 2 cannot implement is listed // under `pruned` with the reason; // - stage3_plan_selection: the selected candidate, chosen among all candidates; -// the shown candidates' costs, and every other shown candidate as +// the shown candidates' costs and accepted accuracy (per query: the +// target's metric or null when not set, the accepted guarantee's +// metric or null when exact, the sketches read and a one-line +// summary), and every other shown candidate as // rejected (`valid: false`) or costlier. Each node's `cost` is one // computation's price times `multiplier`, the times it is computed: // once if materialized, else once per query (or materialized node) @@ -49,8 +52,11 @@ // names the query before it (default `q`). `--epsilon`/ // `--delta` apply to every query; without them the queries are exact. // `--metric` (`absolute`, `absolute_stream_l1`, `absolute_stream_l2`, -// `relative` or `rank`) names the error metric of that target; a quantile -// query's target must name one. +// `relative` or `rank`) sets the error metric of that target; without it, +// a candidate is accepted in whatever metric its guarantee meets ε (and δ) +// in, and `stage3_plan_selection.accepted` reports, per shown valid +// candidate and query, the metric accepted (e.g. "metric not set; accepted +// rank (Kll)"). // `--interval-ms` is the source cadence (default 15000). `--table ''` // (repeatable) declares a table the SQL queries read: `{"name": ..., // "columns": [{"name": ..., "type": "timestamp|utf8|string|float64|double| @@ -72,7 +78,7 @@ use asap_devtools::SqlCatalog; use asap_types::ir::flat::{flatten, FlatDag}; use asap_types::ir::QueryRoot; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{default_metric_for_root, AccuracyTarget, ErrorMetric}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataArrival, DataWorkload, DurationMs, LatencyRequirement, PlanningWorkload, Query, QueryLanguage, QueryRecurrence, QueryRequirements, QueryWorkload, @@ -133,6 +139,7 @@ fn document_of( result: Result, max_displayed: usize, ) -> Result { + let queries = with_default_metrics(queries, roots); match result { Ok(run) => Ok(stage_pipeline(queries, &run, max_displayed)), Err(PlanningStagesError::Pass1(error)) => Ok(json!({ @@ -230,9 +237,9 @@ fn run(args: Vec) -> Result<(), Failure> { } (None, false) => { let accuracy = match (epsilon, delta) { - (None, None) => RequestedAccuracy::exact(), - (Some(epsilon), None) => RequestedAccuracy::epsilon(epsilon), - (Some(epsilon), Some(delta)) => RequestedAccuracy::epsilon_delta(epsilon, delta), + (None, None) => AccuracyTarget::exact(), + (Some(epsilon), None) => AccuracyTarget::epsilon(epsilon), + (Some(epsilon), Some(delta)) => AccuracyTarget::epsilon_delta(epsilon, delta), (None, Some(_)) => return Err("--delta needs --epsilon".into()), }; let accuracy = match metric { @@ -397,8 +404,30 @@ fn plan_selection_json( ) }) .collect(); + let accepted: serde_json::Map<_, _> = selection + .accepted + .iter() + .filter(|(id, _)| shown(id)) + .map(|(id, queries)| { + let queries: Vec = queries + .iter() + .map(|accepted| { + accepted.as_ref().map_or(Value::Null, |accepted| { + json!({ + "target_metric": accepted.target_metric, + "accepted_metric": accepted.accepted_metric, + "sketches": accepted.sketches, + "summary": accepted.to_string(), + }) + }) + }) + .collect(); + (id.clone(), Value::Array(queries)) + }) + .collect(); json!({ "costs": costs, + "accepted": accepted, "selected": selection.selected, "rejected": rejections_json(&selection.rejected), }) @@ -426,19 +455,7 @@ fn workload_queries(workload: &PlanningWorkload) -> Vec { .entries() .enumerate() .map(|(index, entry)| { - let requested = entry.requirements.accuracy.requested(); - let accuracy = if requested.is_exact() { - json!("exact") - } else { - let mut accuracy = json!({ "epsilon": requested.error }); - if let Some(delta) = requested.failure_probability { - accuracy["delta"] = json!(delta); - } - if let Some(metric) = requested.metric { - accuracy["metric"] = json!(metric); - } - accuracy - }; + let accuracy = json!(entry.requirements.accuracy.requested()); let mut requirements = json!({ "accuracy": accuracy }); if let LatencyRequirement::ExplicitMaxMs(ms) = entry.requirements.response_latency { requirements["latency_ms"] = json!(ms); @@ -458,6 +475,23 @@ fn workload_queries(workload: &PlanningWorkload) -> Vec { .collect() } +/// `queries` with, for each approximate target that sets no metric, the +/// metric its root's result is naturally stated in +/// ([`default_metric_for_root`]; null for a quantile, which has none) as +/// `requirements.default_metric`. Display only: Stage 3 accepts any metric. +fn with_default_metrics(mut queries: Vec, roots: &[QueryRoot]) -> Vec { + for (query, root) in queries.iter_mut().zip(roots) { + let accuracy = &query["requirements"]["accuracy"]; + let Ok(target) = serde_json::from_value::(accuracy.clone()) else { + continue; + }; + if target.metric.is_none() && !target.is_exact() { + query["requirements"]["default_metric"] = json!(default_metric_for_root(root)); + } + } + queries +} + /// The tables `--table` declares. fn sql_catalog(tables: &[String]) -> Result { let mut catalog = SqlCatalog::new(); @@ -515,7 +549,7 @@ fn sql_catalog(tables: &[String]) -> Result { fn query_batch( language: QueryLanguage, queries: &[String], - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, interval_ms: u64, ) -> PlanningWorkload { PlanningWorkload { @@ -578,7 +612,7 @@ mod tests { let workload = query_batch( QueryLanguage::PromQL, &["sum(m)".into()], - RequestedAccuracy::exact(), + AccuracyTarget::exact(), 15_000, ); let run = diff --git a/crates/devtools/src/bin/variant_coverage.rs b/crates/devtools/src/bin/variant_coverage.rs index c1a21de66..9f277df73 100644 --- a/crates/devtools/src/bin/variant_coverage.rs +++ b/crates/devtools/src/bin/variant_coverage.rs @@ -10,7 +10,7 @@ use asap_devtools::lower_promql_with_data_ingestion_interval; use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_types::ir::{OperatorNode, ScalarExpr}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; use std::collections::BTreeSet; use std::rc::Rc; @@ -297,11 +297,8 @@ async fn main() { let mut lowered = 0; let mut failed = 0; for q in promql_lines(corpus) { - match lower_promql_with_data_ingestion_interval( - q, - RequestedAccuracy::exact(), - interval_ms, - ) { + match lower_promql_with_data_ingestion_interval(q, AccuracyTarget::exact(), interval_ms) + { Ok(qe) => { walk(&qe, &mut variants); lowered += 1; @@ -333,7 +330,7 @@ async fn main() { &q, &catalog, SqlDialect::DataFusionSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await { @@ -366,7 +363,7 @@ async fn main() { &q, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await { diff --git a/crates/devtools/src/lib.rs b/crates/devtools/src/lib.rs index d83df3372..3a44b6cee 100644 --- a/crates/devtools/src/lib.rs +++ b/crates/devtools/src/lib.rs @@ -28,7 +28,7 @@ pub use asap_frontend_sql::{lower_sql, lower_sql_batch, SqlCatalog, SqlError, Sq /// Tools intentionally require the cadence rather than choosing a default. pub fn lower_promql_with_data_ingestion_interval( query: &str, - accuracy: asap_types::types::RequestedAccuracy, + accuracy: asap_types::types::AccuracyTarget, interval_ms: u64, ) -> Result, PromqlError> { use asap_types::workload::{ diff --git a/crates/devtools/src/planner_layering_examples.rs b/crates/devtools/src/planner_layering_examples.rs index a964b956a..c5ca5d383 100644 --- a/crates/devtools/src/planner_layering_examples.rs +++ b/crates/devtools/src/planner_layering_examples.rs @@ -2,7 +2,7 @@ //! (`docs/design_docs/proposals/planner-layering.md`), shared by //! `stage_pipeline` and the planner-layering acceptance tests. -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataArrival, DataDistribution, DataWorkload, DurationMs, Evidence, EvidenceSource, LatencyRequirement, MetricType, PlanningWorkload, Predictability, @@ -71,12 +71,12 @@ pub fn example1() -> PlanningWorkload { repeating_queries: Some(vec![ panel( EXAMPLE1_Q1, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), LatencyRequirement::Unspecified, ), panel( EXAMPLE1_Q2, - RequestedAccuracy::epsilon_delta(0.01, 0.001), + AccuracyTarget::epsilon_delta(0.01, 0.001), LatencyRequirement::ExplicitMaxMs(100.0), ), ]), @@ -137,9 +137,9 @@ pub fn example2() -> PlanningWorkload { query: Query((*query).into()), demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit( - RequestedAccuracy::epsilon_delta(epsilon, delta), - ), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::epsilon_delta( + epsilon, delta, + )), response_latency: LatencyRequirement::Unspecified, }, predictability: Predictability::Predictable { known_at: None }, @@ -194,7 +194,7 @@ pub enum PatternARecurrence { fn pattern_a_requirements() -> QueryRequirements { QueryRequirements { accuracy: AccuracyRequirement::Explicit( - RequestedAccuracy::epsilon_delta(0.005, 0.01).with_metric(ErrorMetric::Rank), + AccuracyTarget::epsilon_delta(0.005, 0.01).with_metric(ErrorMetric::Rank), ), response_latency: LatencyRequirement::Unspecified, } @@ -274,7 +274,7 @@ pub fn pattern_b() -> PlanningWorkload { )), requirements: QueryRequirements { accuracy: AccuracyRequirement::Explicit( - RequestedAccuracy::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank), + AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank), ), response_latency: LatencyRequirement::ExplicitMaxMs(200.0), }, diff --git a/crates/devtools/src/planning_stages.rs b/crates/devtools/src/planning_stages.rs index e1d7281c7..e29b5a17d 100644 --- a/crates/devtools/src/planning_stages.rs +++ b/crates/devtools/src/planning_stages.rs @@ -10,7 +10,7 @@ use asap_aware_mapping::stage3_plan_selection::{Selection, SelectionError}; use asap_aware_mapping::PlanningModels; use asap_types::ir::schema_support::with_promql_series_identity; use asap_types::ir::QueryRoot; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{DataWorkload, PlanningWorkload, QueryRecurrence}; #[derive(Debug)] @@ -140,7 +140,7 @@ pub fn run_planning_stages( /// evaluated as `recurrence[i]` says. pub fn run_planning_stages_over( roots: Vec, - targets: &[RequestedAccuracy], + targets: &[AccuracyTarget], recurrence: &[QueryRecurrence], data: &DataWorkload, max_combinations: Option, diff --git a/crates/devtools/tests/cross_language.rs b/crates/devtools/tests/cross_language.rs index 3b0d9e76a..5c39ba7c2 100644 --- a/crates/devtools/tests/cross_language.rs +++ b/crates/devtools/tests/cross_language.rs @@ -17,7 +17,7 @@ use asap_devtools::{lower_promql_with_data_ingestion_interval, lower_sql, SqlCat use asap_types::ir::{NonASAPOp, OperatorNode}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::{AggIntent, GroupKeys}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use std::rc::Rc; fn col(name: &str, dtype: DataType) -> Field { @@ -42,13 +42,13 @@ fn catalog() -> SqlCatalog { } async fn sql(q: &str) -> Rc { - lower_sql(q, &catalog(), RequestedAccuracy::exact()) + lower_sql(q, &catalog(), AccuracyTarget::exact()) .await .unwrap_or_else(|e| panic!("SQL {q:?} failed to lower: {e:?}")) } fn promql(q: &str) -> Rc { - lower_promql_with_data_ingestion_interval(q, RequestedAccuracy::exact(), 1_000) + lower_promql_with_data_ingestion_interval(q, AccuracyTarget::exact(), 1_000) .unwrap_or_else(|e| panic!("PromQL {q:?} failed to lower: {e:?}")) } diff --git a/crates/devtools/tests/stage_pipeline.rs b/crates/devtools/tests/stage_pipeline.rs index 103b3e663..037f977f0 100644 --- a/crates/devtools/tests/stage_pipeline.rs +++ b/crates/devtools/tests/stage_pipeline.rs @@ -539,25 +539,75 @@ fn table_time_index_must_be_a_column_index() { } } -/// A quantile query's target names its metric: with `--metric rank` a plan -/// is selected; without one, plan selection reports the missing metric. +/// A query whose target sets no metric shows the metric its result is +/// naturally stated in (the default-metric table, display only): a top-k's +/// is the L1-scaled absolute error; an exact query shows none. #[test] -fn a_quantile_target_names_its_metric() { +fn a_target_without_a_metric_shows_the_query_default() { + let document = generate(&[]); + let queries = &document["workload"]["queries"]; + assert!(queries[0]["requirements"].get("default_metric").is_none()); + assert_eq!( + queries[1]["requirements"]["default_metric"], + "absolute_stream_l1" + ); +} + +/// A quantile query's target may leave its metric unset: then KLL (rank) +/// and DDSketch (relative) candidates both meet it and compete on cost, and +/// the document reports, per valid candidate, the metric each was accepted +/// in and that the target's metric was not set. With `--metric rank` only +/// rank guarantees (and exact plans) are accepted, and the report says so. The workload's +/// accuracy is the target's own JSON form. +#[test] +fn a_quantile_target_without_a_metric_accepts_rank_or_relative() { let query = [ "--promql", "quantile_over_time(0.99, m[1m])", "--epsilon", "0.01", ]; - let named = document_of(&[&query[..], &["--metric", "rank"]].concat()); - assert!(named["stage3_plan_selection"]["selected"].is_string()); - let unnamed = document_of(&query); - assert!(unnamed["stage3_plan_selection"]["selected"].is_null()); + let accepted = |document: &Value| -> Vec { + let selection = &document["stage3_plan_selection"]; + assert!(selection["selected"].is_string(), "{selection}"); + selection["accepted"] + .as_object() + .unwrap() + .values() + .map(|queries| queries[0]["summary"].as_str().unwrap().to_owned()) + .collect() + }; + let unset = document_of(&query); + assert_eq!( + unset["workload"]["queries"][0]["requirements"]["accuracy"], + serde_json::json!({"error": 0.01, "scope": "whole_result"}) + ); + // A quantile has no single default metric to show. + assert!(unset["workload"]["queries"][0]["requirements"] + .get("default_metric") + .is_some_and(Value::is_null)); + let summaries = accepted(&unset); assert!( - unnamed["stage3_plan_selection"]["error"] - .as_str() - .is_some_and(|error| error.contains("must name its metric")), - "{}", - unnamed["stage3_plan_selection"] + summaries + .iter() + .all(|s| s.starts_with("metric not set; accepted ")), + "{summaries:?}" + ); + for wanted in ["accepted rank (Kll)", "accepted relative (DDSketch)"] { + assert!( + summaries.iter().any(|s| s.contains(wanted)), + "{wanted}: {summaries:?}" + ); + } + let selected = unset["stage3_plan_selection"]["selected"].as_str().unwrap(); + assert!(unset["stage3_plan_selection"]["accepted"][selected][0]["accepted_metric"].is_string()); + + let rank = document_of(&[&query[..], &["--metric", "rank"]].concat()); + let summaries = accepted(&rank); + assert!( + summaries + .iter() + .all(|s| s == "metric rank; accepted rank (Kll)" || s == "metric rank; accepted exact"), + "{summaries:?}" ); } diff --git a/crates/frontend-metricsql/src/lib.rs b/crates/frontend-metricsql/src/lib.rs index 4bf6e2b00..d8981b6ee 100644 --- a/crates/frontend-metricsql/src/lib.rs +++ b/crates/frontend-metricsql/src/lib.rs @@ -10,7 +10,7 @@ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, BinaryOpKind, ColumnRef, CompareOpKind, GroupKeys, PromQLVectorSetOpKind, Reduction, ScalarValue, Source, }; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; use asap_types::workload::{DurationMs, PlanningWorkload, QueryLanguage, WorkloadError}; use metricsql_parser::ast::{AggregateModifier, DurationExpr, Expr, MetricExpr, RollupExpr}; use metricsql_parser::functions::{AggregateFunction, BuiltinFunction, RollupFunction}; @@ -77,7 +77,7 @@ pub fn lower_metricsql_query_workload( pub fn lower_metricsql( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ingestion_interval: Duration, ) -> Result, MetricsqlError> { match lower_metricsql_query(query, accuracy, ingestion_interval)? { @@ -90,7 +90,7 @@ pub fn lower_metricsql( /// instant selector looks back `ingestion_interval` for its latest sample. pub fn lower_metricsql_query( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ingestion_interval: Duration, ) -> Result { let ast = parse_metricsql(query)?; @@ -110,7 +110,7 @@ pub fn lower_metricsql_query( } struct Lowerer { - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ingestion_interval: Duration, } diff --git a/crates/frontend-metricsql/tests/lowering.rs b/crates/frontend-metricsql/tests/lowering.rs index 3785e747b..58f88c2bf 100644 --- a/crates/frontend-metricsql/tests/lowering.rs +++ b/crates/frontend-metricsql/tests/lowering.rs @@ -6,13 +6,13 @@ use asap_frontend_metricsql::{ }; use asap_types::ir::{NonASAPOp, OperatorNode, TimeRangeKind}; use asap_types::pre_asap::{AggIntent, Reduction, Source}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; /// The workload's ingestion interval, the lookback of a bare selector. const INTERVAL: Duration = Duration::from_secs(15); fn lower(query: &str) -> Rc { - lower_metricsql(query, RequestedAccuracy::epsilon(0.01), INTERVAL).unwrap() + lower_metricsql(query, AccuracyTarget::epsilon(0.01), INTERVAL).unwrap() } #[test] @@ -72,7 +72,7 @@ fn keep_metric_names_is_preserved_in_ast_and_rejected_without_lineage() { assert!(ast.keep_metric_names()); let error = lower_metricsql( "rate(requests_total[5m]) keep_metric_names", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), INTERVAL, ) .unwrap_err(); @@ -97,7 +97,7 @@ fn victoria_metrics_extension_corpus_parses_natively_and_fails_closed() { let ast = parse_metricsql(query) .unwrap_or_else(|error| panic!("native MetricsQL parser rejected {query:?}: {error}")); assert!(!ast.to_string().is_empty()); - let result = lower_metricsql(query, RequestedAccuracy::exact(), INTERVAL); + let result = lower_metricsql(query, AccuracyTarget::exact(), INTERVAL); assert!( result.is_err(), "extension semantics must be represented or routed to exact fallback: {query}" @@ -135,7 +135,7 @@ fn canonical_identity_recursively_formats_metricsql_extensions() { fn metricsql_multi_argument_aggregates_fail_closed() { for query in ["sum(foo, bar)", "avg(foo, bar)", "count(foo, bar)"] { parse_metricsql(query).expect("MetricsQL accepts multi-argument aggregates"); - let error = lower_metricsql(query, RequestedAccuracy::exact(), INTERVAL).unwrap_err(); + let error = lower_metricsql(query, AccuracyTarget::exact(), INTERVAL).unwrap_err(); assert!( matches!(error, MetricsqlError::UnsupportedFeature(message) if message.contains("requires exactly 1")), "{query} must not silently discard an aggregate input" diff --git a/crates/frontend-metricsql/tests/lowering_semantics.rs b/crates/frontend-metricsql/tests/lowering_semantics.rs index 52572b069..32ad27ce5 100644 --- a/crates/frontend-metricsql/tests/lowering_semantics.rs +++ b/crates/frontend-metricsql/tests/lowering_semantics.rs @@ -4,7 +4,7 @@ use std::time::Duration; use asap_frontend_metricsql::{lower_metricsql_query_workload, MetricsqlError}; use asap_types::ir::{NonASAPOp, OperatorNode, QueryRoot, TimeRangeKind}; use asap_types::pre_asap::AggIntent; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{ BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, @@ -14,7 +14,7 @@ const INTERVAL: Duration = Duration::from_secs(15); fn lower_metricsql( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result, MetricsqlError> { asap_frontend_metricsql::lower_metricsql(query, accuracy, INTERVAL) } @@ -58,7 +58,7 @@ fn time_range_under_aggregate(node: &OperatorNode) -> (Duration, TimeRangeKind) /// `count(v)` counts series; it is not a distinct count of sample values. #[test] fn count_counts_series() { - let node = lower_metricsql("count(up)", RequestedAccuracy::exact()).unwrap(); + let node = lower_metricsql("count(up)", AccuracyTarget::exact()).unwrap(); let NonASAPOp::Aggregate { measures, .. } = node.expect_non_asap() else { panic!("expected an aggregate, got {node:?}"); }; @@ -78,25 +78,25 @@ fn rollups_without_an_explicit_range_are_rejected() { "max_over_time(m)", "default_rollup(m)", ] { - let error = lower_metricsql(query, RequestedAccuracy::exact()).unwrap_err(); + let error = lower_metricsql(query, AccuracyTarget::exact()).unwrap_err(); assert!( matches!(&error, MetricsqlError::UnsupportedFeature(message) if message.contains("evaluation step")), "{query}: {error:?}" ); } - assert!(lower_metricsql("rate(m[5m])", RequestedAccuracy::exact()).is_ok()); + assert!(lower_metricsql("rate(m[5m])", AccuracyTarget::exact()).is_ok()); } /// A bare instant selector reads the latest sample within the ingestion /// interval, as in PromQL; an explicit range keeps its own window. #[test] fn instant_selectors_look_back_one_ingestion_interval() { - let node = lower_metricsql("sum(up)", RequestedAccuracy::exact()).unwrap(); + let node = lower_metricsql("sum(up)", AccuracyTarget::exact()).unwrap(); assert_eq!( time_range_under_aggregate(&node), (INTERVAL, TimeRangeKind::Instant) ); - let node = lower_metricsql("rate(m[5m])", RequestedAccuracy::exact()).unwrap(); + let node = lower_metricsql("rate(m[5m])", AccuracyTarget::exact()).unwrap(); assert_eq!( time_range_under_aggregate(&node), (Duration::from_secs(300), TimeRangeKind::Range) diff --git a/crates/frontend-promql/src/promql.rs b/crates/frontend-promql/src/promql.rs index 0e35e9e74..be73a867e 100644 --- a/crates/frontend-promql/src/promql.rs +++ b/crates/frontend-promql/src/promql.rs @@ -79,7 +79,7 @@ use asap_types::pre_asap::agg_intent::{topk, AggIntent, TimeFunc}; use asap_types::pre_asap::{ ArithmeticOpKind, ColumnRef, CompareOpKind, InfoMatcher, SampleKind, ScalarValue, }; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; /// Every scalar expression this front end builds follows PromQL's numeric rules. const PROMQL: ExprSemantics = ExprSemantics::Promql; @@ -183,7 +183,7 @@ const MAX_DEPTH: usize = 256; impl PromqlLowerer { pub(crate) fn lower_query_with_ingestion_interval( query: &str, - accuracy: &RequestedAccuracy, + accuracy: &AccuracyTarget, interval: Duration, ) -> Result { let _guard = AccuracyGuard::install(accuracy.clone()); @@ -214,18 +214,18 @@ impl PromqlLowerer { } std::thread_local! { - static ACCURACY: std::cell::RefCell = - const { std::cell::RefCell::new(RequestedAccuracy::exact()) }; + static ACCURACY: std::cell::RefCell = + const { std::cell::RefCell::new(AccuracyTarget::exact()) }; static INGESTION_INTERVAL: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; } /// RAII guard installing `accuracy` as the ambient accuracy target for the /// current thread's lowering, restoring the prior value on drop — same shape /// as `histogram::CatalogGuard`. -struct AccuracyGuard(RequestedAccuracy); +struct AccuracyGuard(AccuracyTarget); impl AccuracyGuard { - fn install(accuracy: RequestedAccuracy) -> Self { + fn install(accuracy: AccuracyTarget) -> Self { let prev = ACCURACY.with(|a| a.replace(accuracy)); AccuracyGuard(prev) } @@ -234,12 +234,12 @@ impl AccuracyGuard { impl Drop for AccuracyGuard { fn drop(&mut self) { ACCURACY - .with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, RequestedAccuracy::exact())); + .with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, AccuracyTarget::exact())); } } /// The ambient accuracy target installed by the current [`PromqlLowerer::lower`] call. -fn current_accuracy() -> RequestedAccuracy { +fn current_accuracy() -> AccuracyTarget { ACCURACY.with(|a| a.borrow().clone()) } diff --git a/crates/frontend-promql/tests/count_planning.rs b/crates/frontend-promql/tests/count_planning.rs index 1472bdb92..bc0563964 100644 --- a/crates/frontend-promql/tests/count_planning.rs +++ b/crates/frontend-promql/tests/count_planning.rs @@ -6,7 +6,7 @@ use asap_types::post_asap::{ ExactKind, FieldDataType, GroupingStrategy, NonNegativeWeightProof, SketchAlgorithm, SummaryInputExpr, WeightDomain, }; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use std::rc::Rc; use support::{logical_candidates, lower_promql, post_asap_dag, selected_dag}; @@ -26,7 +26,7 @@ fn reads_hydra(node: &Rc) -> bool { #[test] #[ignore = "Pass 1 offers no Hydra state for a grouped count yet (#605)"] fn grouped_count_offers_hydra_but_selection_keeps_per_group_state() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); // Hydra hashes a non-null item per row: the series identity (PromQL // labels are nullable). let root = asap_types::ir::schema_support::with_promql_series_identity( @@ -43,8 +43,8 @@ fn grouped_count_offers_hydra_but_selection_keeps_per_group_state() { #[test] fn exact_counts_select_count_accumulators() { for query in ["count(up)", "count by(job)(up)", "count_over_time(up[5m])"] { - let root = lower_promql(query, RequestedAccuracy::exact()).unwrap(); - let selected = selected_dag(root, RequestedAccuracy::exact()); + let root = lower_promql(query, AccuracyTarget::exact()).unwrap(); + let selected = selected_dag(root, AccuracyTarget::exact()); assert!( OperatorNode::reachable(&selected).iter().any(|node| { matches!( @@ -64,7 +64,7 @@ fn exact_counts_select_count_accumulators() { #[test] fn frequency_count_candidates_use_unit_weights() { for query in ["count_over_time(up[5m])", "count(up)"] { - let accuracy = RequestedAccuracy::epsilon(0.02); + let accuracy = AccuracyTarget::epsilon(0.02); let root = lower_promql(query, accuracy.clone()).unwrap(); let candidates = logical_candidates(root, accuracy); let mut algorithms = Vec::new(); @@ -138,7 +138,7 @@ fn frequency_count_candidates_use_unit_weights() { fn aggregate_fixture(query: &str, series: &[Vec]) -> Vec { use asap_types::ir::NonASAPOp; use asap_types::pre_asap::{AggIntent, Reduction}; - let root = lower_promql(query, RequestedAccuracy::exact()).unwrap(); + let root = lower_promql(query, AccuracyTarget::exact()).unwrap(); let NonASAPOp::Aggregate { reduction, measures, @@ -227,7 +227,7 @@ fn count_over_time_counts_scrapes_not_sample_values() { #[test] fn cms_count_updates_total_ten_for_zero_positive_and_negative_samples() { use asap_types::pre_asap::ColumnRef; - let accuracy = RequestedAccuracy::epsilon(0.02); + let accuracy = AccuracyTarget::epsilon(0.02); let root = lower_promql("count_over_time(up[5m])", accuracy.clone()).unwrap(); let candidates = logical_candidates(root, accuracy); let dag = candidates @@ -281,7 +281,7 @@ fn cms_count_updates_total_ten_for_zero_positive_and_negative_samples() { #[test] #[should_panic(expected = "does not compose")] fn logical_candidates_fails_on_a_candidate_that_does_not_compose() { - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let root = lower_promql( "sum by (job) (topk by (job, instance) (1, rate(m[5m])))", accuracy.clone(), diff --git a/crates/frontend-promql/tests/histogram_metadata.rs b/crates/frontend-promql/tests/histogram_metadata.rs index d1942c984..e5f585f69 100644 --- a/crates/frontend-promql/tests/histogram_metadata.rs +++ b/crates/frontend-promql/tests/histogram_metadata.rs @@ -9,7 +9,7 @@ use asap_frontend_promql::{HistogramCatalog, HistogramKind}; mod support; use asap_types::ir::{NonASAPOp, OperatorNode}; use asap_types::pre_asap::AggIntent; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::{lower_promql, lower_promql_with_histograms}; /// The histogram/quantile intent kind in the lowered DAG: `"HQ"` for the @@ -40,11 +40,11 @@ fn quantile_kind(qe: &OperatorNode) -> &'static str { } fn heuristic(q: &str) -> &'static str { - quantile_kind(&lower_promql(q, RequestedAccuracy::exact()).unwrap()) + quantile_kind(&lower_promql(q, AccuracyTarget::exact()).unwrap()) } fn with_meta(q: &str, catalog: HistogramCatalog) -> &'static str { - quantile_kind(&lower_promql_with_histograms(q, RequestedAccuracy::exact(), catalog).unwrap()) + quantile_kind(&lower_promql_with_histograms(q, AccuracyTarget::exact(), catalog).unwrap()) } #[test] @@ -58,7 +58,7 @@ fn heuristic_baseline_is_unchanged_without_a_catalog() { ); assert!(lower_promql( "histogram_quantile(0.9, native_latency)", - RequestedAccuracy::exact() + AccuracyTarget::exact() ) .is_err()); } @@ -68,7 +68,7 @@ fn declared_classic_bucket_fixes_the_false_negative() { // A classic histogram exposed WITHOUT the `_bucket` suffix and queried with // no `le` grouping/matcher requires an explicit sample-type declaration. let q = "histogram_quantile(0.9, latency_seconds)"; - assert!(lower_promql(q, RequestedAccuracy::exact()).is_err()); + assert!(lower_promql(q, AccuracyTarget::exact()).is_err()); assert_eq!( with_meta( q, @@ -98,10 +98,8 @@ fn declared_raw_extension_and_native_gap_override_the_heuristic() { "raw samples are sketch-able" ); let catalog = HistogramCatalog::new().with("foo_bucket", HistogramKind::Native); - assert!(lower_promql_with_histograms(q, RequestedAccuracy::exact(), catalog.clone()).is_err()); - assert!( - lower_promql_with_histograms("foo_bucket", RequestedAccuracy::exact(), catalog).is_err() - ); + assert!(lower_promql_with_histograms(q, AccuracyTarget::exact(), catalog.clone()).is_err()); + assert!(lower_promql_with_histograms("foo_bucket", AccuracyTarget::exact(), catalog).is_err()); } #[test] @@ -118,7 +116,7 @@ fn undeclared_metric_falls_back_to_the_heuristic() { ); assert!(lower_promql_with_histograms( "histogram_quantile(0.9, native_thing)", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), catalog ) .is_err()); diff --git a/crates/frontend-promql/tests/maintained_population_horizon.rs b/crates/frontend-promql/tests/maintained_population_horizon.rs index f04f886ec..9d4e6a837 100644 --- a/crates/frontend-promql/tests/maintained_population_horizon.rs +++ b/crates/frontend-promql/tests/maintained_population_horizon.rs @@ -2,12 +2,12 @@ mod support; use asap_aware_mapping::maintained_population::MaintainedPopulationStrategy; use asap_types::ir::{ASAPOp, NonASAPOp, Operator}; use asap_types::post_asap::maintained_population::PopulationInput; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; // A population for a one-second selector must expire members after one second. #[test] fn population_preserves_selector_horizon() { - let root = support::lower_promql("sum(a)", RequestedAccuracy::exact()).unwrap(); + let root = support::lower_promql("sum(a)", AccuracyTarget::exact()).unwrap(); let candidate = MaintainedPopulationStrategy::new(std::slice::from_ref(&root)) .candidate(&root) .unwrap(); diff --git a/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs b/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs index 66e354631..a2dececd1 100644 --- a/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs +++ b/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs @@ -33,7 +33,7 @@ use std::rc::Rc; use asap_types::ir::{BinaryOperator, NonASAPOp, OperatorNode, ScalarExpr}; use asap_types::pre_asap::{AggIntent, BinaryOpKind, CompareOpKind, Reduction, ScalarValue}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::lower_promql; const CORPUS: &str = include_str!("data/awesome_prometheus_alerts.txt"); @@ -48,7 +48,7 @@ fn queries() -> impl Iterator { /// Lower, expecting success. fn ok(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("expected {q:?} to lower, got error: {e}")) } @@ -96,7 +96,7 @@ fn corpus_lowering_is_total_and_fully_parseable() { for q in queries() { // A panic here (not an `Err`) fails the test — that is the totality // guarantee over real-world input. - match lower_promql(q, RequestedAccuracy::exact()) { + match lower_promql(q, AccuracyTarget::exact()) { Ok(_) => t.lowered += 1, Err(LoweringError::Parse(_)) => t.unparseable += 1, Err(_) => t.rejected += 1, diff --git a/crates/frontend-promql/tests/observability/metrics_observability.rs b/crates/frontend-promql/tests/observability/metrics_observability.rs index 73256382b..9d6afd971 100644 --- a/crates/frontend-promql/tests/observability/metrics_observability.rs +++ b/crates/frontend-promql/tests/observability/metrics_observability.rs @@ -7,7 +7,7 @@ use asap_frontend_promql::PromqlError; #[path = "../support.rs"] mod support; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::{logical_plan, lower_promql}; const CORPORA: &[(&str, &str)] = &[ @@ -71,7 +71,7 @@ fn benchmark_corpora_are_total_and_report_coverage() { total += 1; // Approximate accuracy exercises the sketch-replacement boundary // used by the existing post-ASAP corpus binding test. - match lower_promql(query, RequestedAccuracy::epsilon(0.01)) { + match lower_promql(query, AccuracyTarget::epsilon(0.01)) { Ok(expr) => { lowered += 1; match logical_plan(&expr) { @@ -119,7 +119,7 @@ fn kubernetes_mixin_aggregates_over_topk_compose() { r#"sum without (id, metrics_path, name, image, endpoint, job, node) ( topk by (cluster, namespace, pod, container, instance) (1, increase( container_cpu_cfs_throttled_periods_total{container!="", job="cadvisor", } [5m]) ) ) / on (cluster, namespace, pod, container, instance) group_left sum without (id, metrics_path, name, image, endpoint, job, node) ( topk by (cluster, namespace, pod, container, instance) (1, increase( container_cpu_cfs_periods_total{job="cadvisor", } [5m]) ) ) > ( 25 / 100 )"#, r#"histogram_quantile(0.99, sum by (cluster, instance, le) ( topk by (cluster, instance, le, operation_type) (1, rate(kubelet_pod_worker_duration_seconds_bucket{job="kubelet"}[5m]) ) ) ) * on(cluster, instance) group_left(node) topk by (cluster, instance, node) (1, kubelet_node_name{job="kubelet"} ) > 60"#, ] { - let root = lower_promql(query, RequestedAccuracy::epsilon(0.01)).unwrap(); + let root = lower_promql(query, AccuracyTarget::epsilon(0.01)).unwrap(); if let Err(error) = logical_plan(&root) { panic!("{query}: {error}"); } diff --git a/crates/frontend-promql/tests/observability/o11y_bench_promql.rs b/crates/frontend-promql/tests/observability/o11y_bench_promql.rs index 6d6e781e6..5fbbcfce0 100644 --- a/crates/frontend-promql/tests/observability/o11y_bench_promql.rs +++ b/crates/frontend-promql/tests/observability/o11y_bench_promql.rs @@ -21,7 +21,7 @@ use asap_frontend_promql::PromqlError as LoweringError; #[path = "../support.rs"] mod support; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::lower_promql; const CORPUS: &str = include_str!("data/o11y_bench_promql.txt"); @@ -40,7 +40,7 @@ fn lowering_is_total_over_the_o11y_bench_corpus() { let mut rejected = Vec::new(); for q in queries() { - match lower_promql(q, RequestedAccuracy::exact()) { + match lower_promql(q, AccuracyTarget::exact()) { Ok(_) => lowered += 1, Err(LoweringError::Parse(e)) => panic!("unexpected parse failure for {q:?}: {e}"), Err(e) => rejected.push((q, e)), diff --git a/crates/frontend-promql/tests/observability/promql_corpus.rs b/crates/frontend-promql/tests/observability/promql_corpus.rs index ae84468aa..54a022e3c 100644 --- a/crates/frontend-promql/tests/observability/promql_corpus.rs +++ b/crates/frontend-promql/tests/observability/promql_corpus.rs @@ -16,7 +16,7 @@ use asap_frontend_promql::PromqlError as LoweringError; #[path = "../support.rs"] mod support; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::{logical_plan, lower_promql}; const DOCS: &str = include_str!("data/promql_corpus_docs.txt"); @@ -48,7 +48,7 @@ fn tally(corpus: &str) -> Tally { let mut t = Tally::default(); for q in queries(corpus) { match asap_frontend_promql::lower_promql_query_workload( - &support::workload(q, RequestedAccuracy::exact()), + &support::workload(q, AccuracyTarget::exact()), 0, ) { Ok(_) => t.lowered += 1, @@ -75,7 +75,7 @@ struct BindTally { errored: usize, } -fn bind_tally(corpus: &str, accuracy: RequestedAccuracy) -> BindTally { +fn bind_tally(corpus: &str, accuracy: AccuracyTarget) -> BindTally { let mut t = BindTally::default(); for q in queries(corpus) { let Ok(dag) = lower_promql(q, accuracy.clone()) else { @@ -92,7 +92,7 @@ fn bind_tally(corpus: &str, accuracy: RequestedAccuracy) -> BindTally { #[test] fn binding_is_total_over_the_entire_corpus() { - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let docs = bind_tally(DOCS, accuracy.clone()); let td = bind_tally(TESTDATA, accuracy); eprintln!("docs corpus post-ASAP binding: {docs:?}"); diff --git a/crates/frontend-promql/tests/promql_binding_regressions.rs b/crates/frontend-promql/tests/promql_binding_regressions.rs index a8597e316..6e3c8b444 100644 --- a/crates/frontend-promql/tests/promql_binding_regressions.rs +++ b/crates/frontend-promql/tests/promql_binding_regressions.rs @@ -1,5 +1,5 @@ mod support; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::lower_promql; /// Prometheus treats these quantile parameters as valid queries returning special values. @@ -12,7 +12,7 @@ fn quantile_parameters_retain_prometheus_special_value_semantics() { format!("histogram_quantile({parameter}, smoke_bucket)"), ] { assert!( - lower_promql(&query, RequestedAccuracy::exact()).is_ok(), + lower_promql(&query, AccuracyTarget::exact()).is_ok(), "{query}" ); } @@ -23,8 +23,8 @@ fn quantile_parameters_retain_prometheus_special_value_semantics() { #[test] fn irate_and_rate_have_distinct_canonical_intents() { for input in ["smoke_counter_total[5m]", "smoke_counter_total[5m:1m]"] { - let rate = lower_promql(&format!("rate({input})"), RequestedAccuracy::exact()).unwrap(); - let irate = lower_promql(&format!("irate({input})"), RequestedAccuracy::exact()).unwrap(); + let rate = lower_promql(&format!("rate({input})"), AccuracyTarget::exact()).unwrap(); + let irate = lower_promql(&format!("irate({input})"), AccuracyTarget::exact()).unwrap(); assert_ne!(rate, irate, "rate and irate must not collapse: {input}"); } } @@ -34,7 +34,7 @@ fn irate_and_rate_have_distinct_canonical_intents() { fn count_is_row_count_not_distinct_sample_value_count() { use asap_types::ir::NonASAPOp; use asap_types::pre_asap::AggIntent; - let tree = lower_promql("count(smoke_gauge)", RequestedAccuracy::exact()).unwrap(); + let tree = lower_promql("count(smoke_gauge)", AccuracyTarget::exact()).unwrap(); let NonASAPOp::Aggregate { measures, .. } = tree.expect_non_asap() else { panic!("expected aggregate") }; diff --git a/crates/frontend-promql/tests/promql_conformance.rs b/crates/frontend-promql/tests/promql_conformance.rs index 2edf7546a..bdb06702b 100644 --- a/crates/frontend-promql/tests/promql_conformance.rs +++ b/crates/frontend-promql/tests/promql_conformance.rs @@ -44,21 +44,21 @@ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, AtModifier, BinaryOpKind, CompareOpKind, PromQLVectorSetOpKind, Reduction, SampleKind, ScalarValue, Source, TimeFunc, }; +use asap_types::types::AccuracyTarget; use asap_types::types::ErrorMetric; -use asap_types::types::RequestedAccuracy; use support::{lower_promql, promql_scalar}; // ── harness helpers ───────────────────────────────────────────────────────────── /// Lower, expecting success. fn ok(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("expected {q:?} to lower, got error: {e}")) } /// Lower, expecting a clean `LoweringError` (an unsupported capability). fn rejected(q: &str) -> LoweringError { - match lower_promql(q, RequestedAccuracy::exact()) { + match lower_promql(q, AccuracyTarget::exact()) { Err(e) => e, Ok(dag) => panic!("expected {q:?} to be rejected, but it lowered to: {dag:?}"), } @@ -693,7 +693,7 @@ fn double_unary_negation_nests() { #[test] fn count_maps_to_count_and_inherits_accuracy() { // Counts preserve the workload accuracy target without counting distinct values. - let exact = lower_promql("count by (job) (up)", RequestedAccuracy::exact()).unwrap(); + let exact = lower_promql("count by (job) (up)", AccuracyTarget::exact()).unwrap(); assert!( has(&exact, |i| matches!( i, @@ -703,12 +703,12 @@ fn count_maps_to_count_and_inherits_accuracy() { intents(&exact) ); - let approx = lower_promql("count by (job) (up)", RequestedAccuracy::epsilon(0.01)).unwrap(); + let approx = lower_promql("count by (job) (up)", AccuracyTarget::epsilon(0.01)).unwrap(); assert!( has(&approx, |i| matches!( i, AggIntent::Count { accuracy } if (accuracy.error - 0.01).abs() < 1e-9 - && accuracy.metric == ErrorMetric::ABSOLUTE_STREAM_L1 + && accuracy.metric == Some(ErrorMetric::ABSOLUTE_STREAM_L1) )), "Count must carry the approximate target, got {:?}", intents(&approx) diff --git a/crates/frontend-promql/tests/promql_equivalence.rs b/crates/frontend-promql/tests/promql_equivalence.rs index fa5efc803..9f7f3d395 100644 --- a/crates/frontend-promql/tests/promql_equivalence.rs +++ b/crates/frontend-promql/tests/promql_equivalence.rs @@ -21,12 +21,11 @@ use std::rc::Rc; mod support; use asap_types::ir::OperatorNode; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use support::lower_promql; fn lo(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) - .unwrap_or_else(|e| panic!("{q:?} should lower: {e}")) + lower_promql(q, AccuracyTarget::exact()).unwrap_or_else(|e| panic!("{q:?} should lower: {e}")) } /// Every member of an equivalence class must lower to the *same* canonical DAG. diff --git a/crates/frontend-promql/tests/promql_lowering.rs b/crates/frontend-promql/tests/promql_lowering.rs index 8138f04d2..9e777fd31 100644 --- a/crates/frontend-promql/tests/promql_lowering.rs +++ b/crates/frontend-promql/tests/promql_lowering.rs @@ -17,11 +17,10 @@ use asap_types::workload::{ use asap_frontend_promql::{lower_promql_workload, PromqlError as LoweringError}; mod support; -use asap_types::types::RequestedAccuracy; use support::lower_promql; fn lower(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } @@ -60,7 +59,7 @@ fn distinct_over_time_preserves_cardinality_accuracy_and_nested_windows() { "distinct_over_time((cpu_usage + 1)[5m:1m])", "sum by(job)(distinct_over_time(cpu_usage[5m]))", ] { - for accuracy in [RequestedAccuracy::exact(), RequestedAccuracy::epsilon(0.02)] { + for accuracy in [AccuracyTarget::exact(), AccuracyTarget::epsilon(0.02)] { let dag = lower_promql(query, accuracy.clone()).unwrap(); let mut intents = Vec::new(); collect_intents(&dag, &mut intents); @@ -384,7 +383,7 @@ fn classic_histogram_quantile_keeps_out_of_range_quantiles() { fn classic_histogram_quantile_rejects_an_argument_without_le() { let error = lower_promql( "histogram_quantile(0.9, sum by (job) (rate(x_bucket[5m])))", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .unwrap_err(); assert!(error.to_string().contains("le"), "{error}"); @@ -1031,7 +1030,7 @@ fn nan_or_overflowing_topk_k_is_rejected() { "limitk(Inf, http_requests)", ] { assert!( - lower_promql(q, RequestedAccuracy::exact()).is_err(), + lower_promql(q, AccuracyTarget::exact()).is_err(), "{q} should be rejected" ); } @@ -1046,7 +1045,7 @@ fn out_of_range_quantile_phi_is_accepted() { "histogram_quantile(2.0, rate(b_bucket[5m]))", ] { assert!( - lower_promql(query, RequestedAccuracy::exact()).is_ok(), + lower_promql(query, AccuracyTarget::exact()).is_ok(), "{query}" ); } @@ -1059,7 +1058,7 @@ fn function_wrapped_range_vector_is_rejected_not_stripped() { assert!( lower_promql( "rate(abs(http_requests_total[5m]))", - RequestedAccuracy::exact() + AccuracyTarget::exact() ) .is_err(), "function-wrapped range vector should be rejected" @@ -1071,7 +1070,7 @@ fn pathologically_nested_query_is_rejected_not_stack_overflow() { // 300 nested parens parse fine but exceed the walker's depth limit (256); // it must return an error, not overflow the stack. let q = format!("{}m{}", "(".repeat(300), ")".repeat(300)); - let err = lower_promql(&q, RequestedAccuracy::exact()).unwrap_err(); + let err = lower_promql(&q, AccuracyTarget::exact()).unwrap_err(); assert!(format!("{err}").contains("nesting"), "got {err}"); } @@ -1090,7 +1089,7 @@ fn fill_modifiers_are_rejected_not_ignored() { "a > bool on(job) fill(0) b", "sum(a - on(job) group_right fill_left(0) b)", ] { - match lower_promql(q, RequestedAccuracy::exact()) { + match lower_promql(q, AccuracyTarget::exact()) { Err(LoweringError::UnsupportedFeature(m)) if m.contains("`fill`") => {} other => panic!("expected fill rejection for {q:?}, got {other:?}"), } @@ -1103,7 +1102,7 @@ fn fill_modifiers_are_rejected_not_ignored() { fn accuracy_target_flows_into_quantile_intent() { let qe = lower_promql( "quantile_over_time(0.9, m[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let NonASAPOp::Aggregate { measures, .. } = qe.expect_non_asap() else { @@ -1180,7 +1179,7 @@ fn batch_lowers_each_entry_and_reads_per_query_accuracy() { BatchEntry { query: Query("quantile_over_time(0.9, b[5m])".into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon(0.02)), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::epsilon(0.02)), ..Default::default() }, predictability: Predictability::Unknown, @@ -1389,7 +1388,7 @@ fn quantile_branches(q: &OperatorNode) -> Vec<(String, AggIntent)> { fn histogram_quantiles_rejects_unrepresented_native_histograms() { assert!(lower_promql( r#"histogram_quantiles(testhistogram3, "q", 0, 0.25, 1)"#, - RequestedAccuracy::exact() + AccuracyTarget::exact() ) .is_err()); } @@ -1661,7 +1660,7 @@ fn scalar_comparison_without_bool_is_rejected() { // PromQL has no scalar filter: a scalar/scalar comparison needs `bool`. for q in ["1 < 2", "time() > 0", "(1 + 1) == 2"] { assert!( - lower_promql(q, RequestedAccuracy::exact()).is_err(), + lower_promql(q, AccuracyTarget::exact()).is_err(), "{q} must be rejected without `bool`" ); } diff --git a/crates/frontend-promql/tests/scalar_design.rs b/crates/frontend-promql/tests/scalar_design.rs index 1b5410655..ca51bd0dc 100644 --- a/crates/frontend-promql/tests/scalar_design.rs +++ b/crates/frontend-promql/tests/scalar_design.rs @@ -2,11 +2,11 @@ mod support; use asap_types::ir::{NonASAPOp, QueryRoot, ScalarExpr}; use asap_types::pre_asap::{ArithmeticOpKind, CompareOpKind, ScalarValue}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn root(query: &str) -> QueryRoot { asap_frontend_promql::lower_promql_query_workload( - &support::workload(query, RequestedAccuracy::exact()), + &support::workload(query, AccuracyTarget::exact()), 0, ) .unwrap() diff --git a/crates/frontend-promql/tests/support.rs b/crates/frontend-promql/tests/support.rs index c3ee039f4..d6b20bcab 100644 --- a/crates/frontend-promql/tests/support.rs +++ b/crates/frontend-promql/tests/support.rs @@ -5,13 +5,13 @@ use asap_frontend_promql::{ }; use asap_types::ir::{NonASAPOp, OperatorNode, ScalarExpr}; use asap_types::pre_asap::ScalarValue; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, }; -pub fn workload(query: &str, accuracy: RequestedAccuracy) -> PlanningWorkload { +pub fn workload(query: &str, accuracy: AccuracyTarget) -> PlanningWorkload { PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -41,7 +41,7 @@ pub fn workload(query: &str, accuracy: RequestedAccuracy) -> PlanningWorkload { #[allow(dead_code)] pub fn lower_promql( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result, PromqlError> { let mut lowered = lower_promql_workload(&workload(query, accuracy), 0)?; Ok(lowered.remove(0)) @@ -50,7 +50,7 @@ pub fn lower_promql( #[allow(dead_code)] pub fn lower_promql_with_histograms( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, histograms: HistogramCatalog, ) -> Result, PromqlError> { let mut lowered = @@ -88,7 +88,7 @@ pub fn post_asap_dag(root: &Rc) -> asap_types::ir::physical_export #[allow(dead_code)] pub fn scalar_root(query: &str) -> ScalarExpr { match asap_frontend_promql::lower_promql_query_workload( - &workload(query, RequestedAccuracy::exact()), + &workload(query, AccuracyTarget::exact()), 0, ) .unwrap() @@ -115,7 +115,7 @@ pub fn sample_expression(node: &OperatorNode) -> &ScalarExpr { #[allow(dead_code)] pub fn run_stage1_to_stage3( root: Rc, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> asap_aware_mapping::Stage1ToStage3Run { asap_aware_mapping::run_stage1_to_stage3( vec![(0, asap_types::ir::QueryRoot::Operator(root))], @@ -130,7 +130,7 @@ pub fn run_stage1_to_stage3( /// The logical DAG the planner pipeline selects for the single query `root` /// at `accuracy`. #[allow(dead_code)] -pub fn selected_dag(root: Rc, accuracy: RequestedAccuracy) -> Rc { +pub fn selected_dag(root: Rc, accuracy: AccuracyTarget) -> Rc { let run = run_stage1_to_stage3(root, accuracy); match run.selected_logical().expect("a plan is selected") { [asap_types::ir::QueryRoot::Operator(node)] => Rc::clone(node), @@ -144,7 +144,7 @@ pub fn selected_dag(root: Rc, accuracy: RequestedAccuracy) -> Rc, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Vec> { run_stage1_to_stage3(root, accuracy) .stage1_logical_asap_optimization diff --git a/crates/frontend-promql/tests/univmon_candidates.rs b/crates/frontend-promql/tests/univmon_candidates.rs index e78717f9b..aeed7651a 100644 --- a/crates/frontend-promql/tests/univmon_candidates.rs +++ b/crates/frontend-promql/tests/univmon_candidates.rs @@ -7,7 +7,7 @@ use asap_types::ir::{ASAPOp, Operator, OperatorNode}; use asap_types::post_asap::{ ErrorMetric, FieldDataType, SketchAlgorithm, SketchStatistic, SummaryInputExpr, }; -use asap_types::types::{AccuracyTarget, RequestedAccuracy}; +use asap_types::types::AccuracyTarget; use support::{logical_candidates, lower_promql, post_asap_dag, run_stage1_to_stage3}; /// The summary evaluation over a UnivMon state that `root` reads, if any. @@ -22,7 +22,7 @@ fn univmon_estimate(root: &Rc) -> Option> { } /// The logical candidate of `query` that evaluates a UnivMon state. -fn candidate(query: &str, accuracy: RequestedAccuracy) -> Rc { +fn candidate(query: &str, accuracy: AccuracyTarget) -> Rc { let root = lower_promql(query, accuracy.clone()).unwrap(); logical_candidates(root, accuracy) .into_iter() @@ -36,7 +36,7 @@ fn four_evaluations_size_their_value_frequency_states_and_keep_honest_guarantees // own shapes, while the total and entropy keep the baseline shape and so // share one state. Sharing across readouts is the summary-capability // rule's job. - let accuracy = RequestedAccuracy::epsilon(0.02); + let accuracy = AccuracyTarget::epsilon(0.02); let roots: Vec<_> = [ ("distinct_over_time(m[5m])", accuracy.clone()), ("count_over_time(m[5m])", accuracy.clone()), @@ -128,9 +128,9 @@ fn uncalibrated_frequency_evaluations_do_not_bypass_accuracy_targets() { // L2 is certified; entropy is not. for query in ["entropy_over_time(m[5m])"] { for target in [ - RequestedAccuracy::exact(), - RequestedAccuracy::epsilon(0.02), - RequestedAccuracy::epsilon_delta(0.02, 0.01), + AccuracyTarget::exact(), + AccuracyTarget::epsilon(0.02), + AccuracyTarget::epsilon_delta(0.02, 0.01), ] { let root = lower_promql(query, target.clone()).unwrap(); let run = run_stage1_to_stage3(root, target.clone()); diff --git a/crates/frontend-sql/src/lib.rs b/crates/frontend-sql/src/lib.rs index d2c40dade..a06f9de4d 100644 --- a/crates/frontend-sql/src/lib.rs +++ b/crates/frontend-sql/src/lib.rs @@ -16,7 +16,7 @@ use std::rc::Rc; use asap_frontend_common::resolve_root; use asap_types::ir::OperatorNode; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{QueryLanguage, QueryWorkload, SqlDialect}; pub use error::SqlError; @@ -31,7 +31,7 @@ pub use sql::{SqlCatalog, SqlLowerer}; pub async fn lower_sql( query: &str, catalog: &SqlCatalog, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result, SqlError> { lower_sql_dialect(query, catalog, SqlDialect::DataFusionSQL, accuracy).await } @@ -48,7 +48,7 @@ pub async fn lower_sql_dialect( query: &str, catalog: &SqlCatalog, dialect: SqlDialect, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result, SqlError> { let unresolved = SqlLowerer::with_dialect(catalog, dialect) .lower(query, &accuracy) diff --git a/crates/frontend-sql/src/sql/mod.rs b/crates/frontend-sql/src/sql/mod.rs index d11a1afb3..fa4dcfc4d 100644 --- a/crates/frontend-sql/src/sql/mod.rs +++ b/crates/frontend-sql/src/sql/mod.rs @@ -69,7 +69,7 @@ use asap_types::pre_asap::{ resolve_column_ref, ColumnRef, CompareOpKind, JoinKind, RelationalSetOpKind, ScalarValue, WindowFuncKind, }; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; use asap_types::workload::SqlDialect; use crate::error::SqlError as LoweringError; @@ -84,8 +84,8 @@ pub use types::SqlCatalog; use self::types::{arrow_to_dtype, scalar_value_to_asap, schema_to_arrow}; std::thread_local! { - static ACCURACY: std::cell::RefCell = - const { std::cell::RefCell::new(RequestedAccuracy::exact()) }; + static ACCURACY: std::cell::RefCell = + const { std::cell::RefCell::new(AccuracyTarget::exact()) }; } /// RAII guard installing `accuracy` as the ambient accuracy target for the @@ -95,10 +95,10 @@ std::thread_local! { /// `lower_plan` recursion without a parameter on every one of its /// signatures, consulted only at the couple of sites that build an /// accuracy-bearing `AggIntent`. -struct AccuracyGuard(RequestedAccuracy); +struct AccuracyGuard(AccuracyTarget); impl AccuracyGuard { - fn install(accuracy: RequestedAccuracy) -> Self { + fn install(accuracy: AccuracyTarget) -> Self { let prev = ACCURACY.with(|a| a.replace(accuracy)); AccuracyGuard(prev) } @@ -107,11 +107,11 @@ impl AccuracyGuard { impl Drop for AccuracyGuard { fn drop(&mut self) { ACCURACY - .with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, RequestedAccuracy::exact())); + .with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, AccuracyTarget::exact())); } } -fn current_accuracy() -> RequestedAccuracy { +fn current_accuracy() -> AccuracyTarget { ACCURACY.with(|a| a.borrow().clone()) } @@ -176,7 +176,7 @@ impl<'a> SqlLowerer<'a> { pub async fn lower( &self, sql: &str, - accuracy: &RequestedAccuracy, + accuracy: &AccuracyTarget, ) -> Result { let ctx = self.build_context()?; let state = ctx.state(); diff --git a/crates/frontend-sql/tests/bgp_analytics/bgp_analytics.rs b/crates/frontend-sql/tests/bgp_analytics/bgp_analytics.rs index 0432570b2..31894624f 100644 --- a/crates/frontend-sql/tests/bgp_analytics/bgp_analytics.rs +++ b/crates/frontend-sql/tests/bgp_analytics/bgp_analytics.rs @@ -41,7 +41,7 @@ use asap_frontend_sql::{lower_sql_dialect, SqlCatalog, SqlError as LoweringError use asap_types::ir::{NonASAPOp, OperatorNode}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::{AggIntent, GroupKeys}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; use datafusion::error::DataFusionError; @@ -100,7 +100,7 @@ async fn lower(q: &str) -> Result, LoweringError> { q, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await } diff --git a/crates/frontend-sql/tests/bgp_jan2024_workload/bgp_jan2024_workload.rs b/crates/frontend-sql/tests/bgp_jan2024_workload/bgp_jan2024_workload.rs index 175a004fb..8238150f5 100644 --- a/crates/frontend-sql/tests/bgp_jan2024_workload/bgp_jan2024_workload.rs +++ b/crates/frontend-sql/tests/bgp_jan2024_workload/bgp_jan2024_workload.rs @@ -15,7 +15,7 @@ use asap_frontend_sql::{lower_sql_dialect, SqlCatalog, SqlError}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; use datafusion::error::DataFusionError; use serde::Deserialize; @@ -75,7 +75,7 @@ async fn lower(q: &str) -> Result, Sql q, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await } diff --git a/crates/frontend-sql/tests/data_quality_check/synthetic_packet_trace.rs b/crates/frontend-sql/tests/data_quality_check/synthetic_packet_trace.rs index db7f32294..5833e236f 100644 --- a/crates/frontend-sql/tests/data_quality_check/synthetic_packet_trace.rs +++ b/crates/frontend-sql/tests/data_quality_check/synthetic_packet_trace.rs @@ -25,7 +25,7 @@ use asap_frontend_sql::{lower_sql, SqlCatalog, SqlError as LoweringError}; use asap_types::ir::{NonASAPOp, OperatorNode}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::{AggIntent, GroupKeys}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; const CORPUS: &str = include_str!("data/synthetic_packet_trace_queries.sql"); @@ -125,7 +125,7 @@ fn has_window_func(node: &OperatorNode) -> bool { } async fn lower(q: &str) -> Rc { - lower_sql(q, &catalog(), RequestedAccuracy::exact()) + lower_sql(q, &catalog(), AccuracyTarget::exact()) .await .unwrap_or_else(|e| panic!("expected {q:?} to lower, got error: {e}")) } @@ -145,7 +145,7 @@ async fn corpus_lowering_covers_every_query() { let mut t = Tally::default(); for q in queries() { // A panic here (not an `Err`) fails the test — the totality guarantee. - match lower_sql(&q, &cat, RequestedAccuracy::exact()).await { + match lower_sql(&q, &cat, AccuracyTarget::exact()).await { Ok(_) => t.lowered += 1, // DataFusion surfaces parse/plan failures as `DataFusion(_)`. Err(LoweringError::DataFusion(_)) => t.unparseable += 1, diff --git a/crates/frontend-sql/tests/data_quality_check/tpch_deequ.rs b/crates/frontend-sql/tests/data_quality_check/tpch_deequ.rs index e3a8d22a2..340594521 100644 --- a/crates/frontend-sql/tests/data_quality_check/tpch_deequ.rs +++ b/crates/frontend-sql/tests/data_quality_check/tpch_deequ.rs @@ -24,7 +24,7 @@ use asap_frontend_sql::{lower_sql, SqlCatalog, SqlError as LoweringError}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; const CORPUS: &str = include_str!("data/tpch_deequ_queries.sql"); @@ -85,7 +85,7 @@ async fn lowers_the_warehouse_ingestion_check_set() { let mut rejected = Vec::new(); let mut lowered = 0; for (id, query) in queries { - match lower_sql(query, &cat, RequestedAccuracy::exact()).await { + match lower_sql(query, &cat, AccuracyTarget::exact()).await { Ok(_) => lowered += 1, Err(LoweringError::UnsupportedAggregate(reason)) => rejected.push((id, reason)), Err(error) => panic!("unexpected failure for U-{id}: {error}"), diff --git a/crates/frontend-sql/tests/frequency_entropy.rs b/crates/frontend-sql/tests/frequency_entropy.rs index 4fa911e4d..152c94c50 100644 --- a/crates/frontend-sql/tests/frequency_entropy.rs +++ b/crates/frontend-sql/tests/frequency_entropy.rs @@ -5,7 +5,7 @@ use asap_frontend_sql::{lower_sql, SqlCatalog, SqlLowerer}; use asap_types::{ ir::{NonASAPOp, OperatorNode}, pre_asap::{AggIntent, DataType, Field, Schema}, - types::{ErrorMetric, RequestedAccuracy}, + types::{AccuracyTarget, ErrorMetric}, }; fn catalog(nullable: bool) -> SqlCatalog { @@ -32,12 +32,12 @@ fn has_entropy(node: &OperatorNode) -> bool { #[tokio::test] async fn recognizes_sql_entropy_in_nats() { let sql = "SELECT -SUM(p * LN(p)) AS entropy FROM (SELECT COUNT(*) * 1.0 / SUM(COUNT(*)) OVER () AS p FROM flows GROUP BY src_ip) f"; - let rewritten = lower_sql(sql, &catalog(false), RequestedAccuracy::exact()) + let rewritten = lower_sql(sql, &catalog(false), AccuracyTarget::exact()) .await .unwrap(); let original = resolve_root( &SqlLowerer::new(&catalog(false)) - .lower(sql, &RequestedAccuracy::exact()) + .lower(sql, &AccuracyTarget::exact()) .await .unwrap(), ) @@ -60,7 +60,7 @@ async fn declines_non_equivalent_entropy_shapes() { ("SELECT -SUM(p*LN(p)) FROM (SELECT COUNT(*)*1.0/SUM(COUNT(*)) OVER () AS p FROM flows GROUP BY src_ip HAVING COUNT(*) > 1) f", false), ("SELECT -SUM(p*LN(p)) FROM (SELECT COUNT(*)*1.0/SUM(COUNT(*)) OVER () AS p FROM flows GROUP BY src_ip) f", true), ] { - let root = lower_sql(sql, &catalog(nullable), RequestedAccuracy::exact()) + let root = lower_sql(sql, &catalog(nullable), AccuracyTarget::exact()) .await .unwrap(); assert!(!has_entropy(&root), "{sql}"); @@ -71,7 +71,7 @@ async fn declines_non_equivalent_entropy_shapes() { /// is an exact count. #[tokio::test] async fn entropy_accuracy_and_population_guard_are_separate() { - let target = RequestedAccuracy::epsilon_delta(0.05, 0.01); + let target = AccuracyTarget::epsilon_delta(0.05, 0.01); let node = lower_sql("SELECT -SUM(p*LN(p)) FROM (SELECT COUNT(*)*1.0/SUM(COUNT(*)) OVER () AS p FROM flows GROUP BY src_ip) f", &catalog(false), target.clone()).await.unwrap(); let NonASAPOp::Project { child, .. } = node.expect_non_asap() else { panic!("project"); diff --git a/crates/frontend-sql/tests/frequency_l2.rs b/crates/frontend-sql/tests/frequency_l2.rs index 153dab4ba..76c87cebc 100644 --- a/crates/frontend-sql/tests/frequency_l2.rs +++ b/crates/frontend-sql/tests/frequency_l2.rs @@ -5,7 +5,7 @@ use asap_frontend_sql::{lower_sql, lower_sql_batch, SqlCatalog, SqlLowerer}; use asap_types::{ ir::{NonASAPOp, OperatorNode}, pre_asap::{AggIntent, DataType, Field, Schema}, - types::{ErrorMetric, RequestedAccuracy}, + types::{AccuracyTarget, ErrorMetric}, }; use std::rc::Rc; @@ -35,7 +35,7 @@ fn has_l2(node: &OperatorNode) -> bool { async fn relational(sql: &str, catalog: &SqlCatalog) -> Rc { resolve_root( &SqlLowerer::new(catalog) - .lower(sql, &RequestedAccuracy::exact()) + .lower(sql, &AccuracyTarget::exact()) .await .unwrap(), ) @@ -50,7 +50,7 @@ const CAST_COUNT: &str = "SELECT SQRT(SUM(c * c)) AS norm FROM (SELECT src_ip, C #[tokio::test] async fn recognizes_float_frequency_l2() { for sql in [CAST_PRODUCT, CAST_COUNT] { - let rewritten = lower_sql(sql, &catalog(false), RequestedAccuracy::exact()) + let rewritten = lower_sql(sql, &catalog(false), AccuracyTarget::exact()) .await .unwrap(); let original = relational(sql, &catalog(false)).await; @@ -71,7 +71,7 @@ async fn declines_non_equivalent_frequency_shapes() { ("SELECT SQRT(SUM(c*c)) FROM (SELECT src_ip, CAST(COUNT(*) AS DOUBLE) AS c FROM flows GROUP BY src_ip HAVING COUNT(*) > 1) f", false), ("SELECT SQRT(SUM(c*c)) FROM (SELECT src_ip, CAST(SUM(CAST(keep AS BIGINT)) AS DOUBLE) AS c FROM flows GROUP BY src_ip) f", false), ] { - let root = lower_sql(sql, &catalog(nullable), RequestedAccuracy::exact()) + let root = lower_sql(sql, &catalog(nullable), AccuracyTarget::exact()) .await .unwrap(); assert!(!has_l2(&root), "{sql}"); @@ -81,7 +81,7 @@ async fn declines_non_equivalent_frequency_shapes() { /// The L2 intent carries the query's accuracy target, not a default. #[tokio::test] async fn frequency_l2_preserves_accuracy_target() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let node = lower_sql(CAST_COUNT, &catalog(false), target.clone()) .await .unwrap(); @@ -103,7 +103,7 @@ async fn frequency_l2_preserves_accuracy_target() { /// empty-input guard is an exact count. #[tokio::test] async fn frequency_empty_input_guard_uses_an_exact_count() { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let node = lower_sql(CAST_COUNT, &catalog(false), target) .await .unwrap(); @@ -134,7 +134,7 @@ async fn batch_lowering_recognizes_frequency_l2() { query_batch: Some(vec![BatchEntry { query: Query(CAST_COUNT.into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::exact()), response_latency: LatencyRequirement::Unspecified, }, predictability: Predictability::Unknown, @@ -156,7 +156,7 @@ async fn stage1_offers_exact_and_summary_l2_candidates() { generate_per_computation_candidates, ComputationCandidate, }; use asap_types::ir::QueryRoot; - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let root = lower_sql(CAST_COUNT, &catalog(false), target) .await .unwrap(); @@ -206,7 +206,7 @@ async fn example2_queries_lower_to_frequency_intents() { let lower = |sql: String| { let catalog = &catalog; async move { - lower_sql(&sql, catalog, RequestedAccuracy::exact()) + lower_sql(&sql, catalog, AccuracyTarget::exact()) .await .unwrap() } diff --git a/crates/frontend-sql/tests/maintained_population.rs b/crates/frontend-sql/tests/maintained_population.rs index 28043b810..d3a4b7493 100644 --- a/crates/frontend-sql/tests/maintained_population.rs +++ b/crates/frontend-sql/tests/maintained_population.rs @@ -1,7 +1,7 @@ //! SQL and PromQL use the same shared-state rule without sharing membership semantics. use asap_aware_mapping::maintained_population::MaintainedPopulationStrategy; use asap_frontend_sql::{lower_sql, SqlCatalog}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::{ ir::{ apply_materialization_timings, cse::share_common_sub_dags, @@ -21,7 +21,7 @@ async fn aggregate(q: &str) -> Rc { Field::plain("job", DataType::Utf8, false), ]), ); - lower_sql(q, &catalog, RequestedAccuracy::exact()) + lower_sql(q, &catalog, AccuracyTarget::exact()) .await .unwrap() } diff --git a/crates/frontend-sql/tests/netflow/netflow.rs b/crates/frontend-sql/tests/netflow/netflow.rs index eed469f78..fd57143b4 100644 --- a/crates/frontend-sql/tests/netflow/netflow.rs +++ b/crates/frontend-sql/tests/netflow/netflow.rs @@ -10,7 +10,7 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_types::ir::{NonASAPOp, OperatorNode}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::{AggIntent, GroupKeys}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; const CORPUS: &str = include_str!("data/netflow.sql"); @@ -113,7 +113,7 @@ async fn netflow_sql_corpus_lowers_to_expected_intents() { for (idx, (query, expected)) in queries.iter().zip(EXPECTED).enumerate() { // A successful `lower_sql` already derived every node's schema. - let qe = lower_sql(query, &catalog(), RequestedAccuracy::exact()) + let qe = lower_sql(query, &catalog(), AccuracyTarget::exact()) .await .unwrap_or_else(|err| panic!("q{} failed to lower:\n{query}\n{err}", idx + 1)); assert!( diff --git a/crates/frontend-sql/tests/pearson_corr.rs b/crates/frontend-sql/tests/pearson_corr.rs index 8cc827f01..fb73072d0 100644 --- a/crates/frontend-sql/tests/pearson_corr.rs +++ b/crates/frontend-sql/tests/pearson_corr.rs @@ -7,7 +7,7 @@ use asap_types::ir::{ MaterializationAssignment, NonASAPOp, OperatorNode, ScalarExpr, TimingMemo, }; use asap_types::pre_asap::{AggIntent, DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn catalog() -> SqlCatalog { let schema = Schema::new(vec![ @@ -21,7 +21,7 @@ fn catalog() -> SqlCatalog { } async fn lower(sql: &str) -> Rc { - lower_sql(sql, &catalog(), RequestedAccuracy::exact()) + lower_sql(sql, &catalog(), AccuracyTarget::exact()) .await .unwrap() } @@ -114,7 +114,7 @@ async fn corr_rejects_unrepresented_forms() { "SELECT corr(x) FROM a", ] { assert!( - lower_sql(sql, &catalog(), RequestedAccuracy::exact()) + lower_sql(sql, &catalog(), AccuracyTarget::exact()) .await .is_err(), "{sql}" @@ -150,7 +150,7 @@ async fn corr_survives_exact_plan_compilation() { let query = lower("SELECT corr(x, y) AS r FROM a").await; let run = asap_aware_mapping::run_stage1_to_stage3( vec![(0, asap_types::ir::QueryRoot::Operator(query.clone()))], - &[Some(asap_types::types::RequestedAccuracy::exact())], + &[Some(asap_types::types::AccuracyTarget::exact())], &[asap_types::workload::QueryRecurrence::Unknown], &Default::default(), asap_aware_mapping::PlanningModels::builtin(), diff --git a/crates/frontend-sql/tests/sql_lowering.rs b/crates/frontend-sql/tests/sql_lowering.rs index b2a10abf7..c65f84be2 100644 --- a/crates/frontend-sql/tests/sql_lowering.rs +++ b/crates/frontend-sql/tests/sql_lowering.rs @@ -20,7 +20,7 @@ use asap_types::pre_asap::{ AggIntent, ColumnRef, CompareOpKind, GroupKeys, JoinKind, Reduction, ScalarValue, Source, WindowFrameBound, WindowFrameOffset, WindowFrameUnits, WindowFuncKind, }; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; fn col(name: &str, dtype: DataType) -> Field { @@ -53,7 +53,7 @@ fn catalog() -> SqlCatalog { } async fn lower(sql: &str) -> Rc { - lower_sql(sql, &catalog(), RequestedAccuracy::exact()) + lower_sql(sql, &catalog(), AccuracyTarget::exact()) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")) } @@ -65,7 +65,7 @@ fn op(node: &OperatorNode) -> &NonASAPOp { #[tokio::test] async fn planning_subquery_bridge_rejects_a_relation_without_vector_conversion() { - let result = lower_sql("SELECT max(value) FROM (SELECT asap_promql_subquery(21600000, 60000) AS value FROM (SELECT sum(bytes) AS value FROM metrics))", &catalog(), RequestedAccuracy::exact()).await; + let result = lower_sql("SELECT max(value) FROM (SELECT asap_promql_subquery(21600000, 60000) AS value FROM (SELECT sum(bytes) AS value FROM metrics))", &catalog(), AccuracyTarget::exact()).await; assert!(result.is_err()); } @@ -99,7 +99,7 @@ async fn planning_relation_bridges_reject_ambiguous_shapes() { let missing_alias = lower_sql( "SELECT asap_promql_subquery(300000, 60000) FROM metrics", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -108,7 +108,7 @@ async fn planning_relation_bridges_reject_ambiguous_shapes() { let histogram_with_extra_column = lower_sql( "SELECT service, asap_histogram_quantile(0.95) AS value FROM metrics", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -119,7 +119,7 @@ async fn planning_relation_bridges_reject_ambiguous_shapes() { let invalid_q = lower_sql( "SELECT asap_histogram_quantile(1.5) AS value FROM metrics", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -367,7 +367,7 @@ async fn filtered_count_ranking_is_not_a_heavy_hitter() { GROUP BY service ORDER BY c DESC LIMIT 10", "SELECT service, COUNT(bytes) AS c FROM metrics GROUP BY service ORDER BY c DESC LIMIT 10", ] { - let qe = lower_sql(sql, &catalog, RequestedAccuracy::exact()) + let qe = lower_sql(sql, &catalog, AccuracyTarget::exact()) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")); let (_, measures) = find_aggregate(&qe).expect("expected an Aggregate"); @@ -408,7 +408,7 @@ async fn distinct_value_reducer_is_rejected_not_dropped() { let res = lower_sql( "SELECT SUM(DISTINCT bytes) FROM metrics", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await; assert!(res.is_err(), "SUM(DISTINCT ...) should be rejected"); @@ -738,7 +738,7 @@ async fn a_multi_column_in_subquery_is_rejected() { let err = lower_sql( "SELECT service FROM metrics WHERE service IN (SELECT service, region FROM hosts)", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("IN must select one column"); @@ -963,7 +963,7 @@ async fn groups_frame_is_rejected() { "SELECT service, SUM(latency) OVER (PARTITION BY service ORDER BY ts \ GROUPS BETWEEN 2 PRECEDING AND CURRENT ROW) FROM metrics", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("GROUPS frame unit must be rejected"); @@ -1168,7 +1168,7 @@ async fn a_correlated_in_subquery_is_rejected() { "SELECT service FROM metrics m WHERE service IN \ (SELECT h.service FROM hosts h WHERE h.region = m.service)", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("correlated IN needs both a key match and a correlation"); @@ -1181,7 +1181,7 @@ async fn a_correlated_in_subquery_is_rejected() { async fn lower_unresolved(sql: &str) -> UnresolvedOp { let catalog = catalog(); SqlLowerer::new(&catalog) - .lower(sql, &RequestedAccuracy::exact()) + .lower(sql, &AccuracyTarget::exact()) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")) } @@ -1459,7 +1459,7 @@ async fn median_threads_the_accuracy_target() { let qe = lower_sql( "SELECT approx_median(latency) FROM metrics", &catalog(), - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await .expect("approx_median should lower"); @@ -1710,7 +1710,7 @@ async fn grouping_function_is_rejected() { let err = lower_sql( "SELECT service, SUM(bytes), GROUPING(service) FROM metrics GROUP BY ROLLUP(service)", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("GROUPING() must be rejected while __grouping_id is dropped"); @@ -1724,7 +1724,7 @@ async fn a_non_column_key_inside_a_grouping_set_is_rejected() { let err = lower_sql( "SELECT date_trunc('minute', ts) AS m, SUM(bytes) FROM metrics GROUP BY ROLLUP(m)", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("expression key inside ROLLUP must be rejected"); @@ -1768,7 +1768,7 @@ async fn an_ambiguous_passthrough_column_is_rejected_only_when_projecting() { "SELECT m.service, h.service, SUM(m.bytes) FROM metrics m \ JOIN hosts h ON m.service = h.service GROUP BY m.service, h.service", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await; assert!( @@ -1780,7 +1780,7 @@ async fn an_ambiguous_passthrough_column_is_rejected_only_when_projecting() { "SELECT m.service, h.service, SUM(m.bytes * 2) FROM metrics m \ JOIN hosts h ON m.service = h.service GROUP BY m.service, h.service", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("ambiguous passthrough must be rejected, not silently resolved"); @@ -1803,7 +1803,7 @@ async fn array_agg_is_deliberately_rejected() { let err = lower_sql( "SELECT array_agg(service) FROM metrics", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("array_agg must not lower to an intent"); @@ -1821,7 +1821,7 @@ async fn lower_clickhouse(sql: &str) -> Rc { sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")) @@ -1884,7 +1884,7 @@ async fn temporal_aggregate_rejects_non_timestamp_and_non_positive_window() { sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("invalid temporal arguments must fail closed"); @@ -1902,7 +1902,7 @@ async fn temporal_aggregate_rejects_mixed_reducers() { "SELECT asap_rate(latency, ts, 300000), sum(bytes) FROM metrics", &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("one child cannot carry temporal and ordinary aggregate semantics"); @@ -1915,7 +1915,7 @@ async fn last_fails_closed_until_an_executable_summary_exists() { "SELECT service, asap_last(latency, ts, 300000) FROM metrics GROUP BY service", &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("last must not be advertised without an executable physical summary"); @@ -1945,7 +1945,7 @@ async fn temporal_grouping_requires_the_complete_declared_series_identity() { sql, &multi_series, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("partial identity must not merge counter series"); @@ -1957,7 +1957,7 @@ async fn temporal_grouping_requires_the_complete_declared_series_identity() { FROM samples GROUP BY service, instance", &multi_series, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect("the complete declared series identity is safe"); @@ -1978,7 +1978,7 @@ async fn temporal_grouping_requires_the_complete_declared_series_identity() { "SELECT service, asap_rate(value, ts, 300000) FROM samples GROUP BY service", &row_id_only, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("a row key without time does not prove a series identity"); @@ -1996,7 +1996,7 @@ async fn temporal_grouping_rejects_value_time_and_duplicate_resolved_columns() { sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("unsafe or duplicate resolved grouping must fail closed"); @@ -2188,7 +2188,7 @@ async fn arg_max_rejects_a_non_column_argument() { "SELECT argMax(service, latency * 2) FROM metrics", &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect_err("argMax over a non-column expression must be rejected"); @@ -2327,7 +2327,7 @@ async fn count_null_semantics_become_a_measure_filter() { "SELECT count(value) FROM samples", "SELECT count(value + 1) FROM samples", ] { - let qe = lower_sql(sql, &catalog, RequestedAccuracy::exact()) + let qe = lower_sql(sql, &catalog, AccuracyTarget::exact()) .await .unwrap_or_else(|error| panic!("{sql}: {error}")); assert!( @@ -2340,7 +2340,7 @@ async fn count_null_semantics_become_a_measure_filter() { "SELECT count(NULL) FROM samples", "SELECT count(nullable_value + 1) FROM samples", ] { - let qe = lower_sql(sql, &catalog, RequestedAccuracy::exact()) + let qe = lower_sql(sql, &catalog, AccuracyTarget::exact()) .await .unwrap_or_else(|error| panic!("{sql}: {error}")); let [Some(Predicate(cond))] = aggregate_filters(&qe) else { @@ -2355,7 +2355,7 @@ async fn count_null_semantics_become_a_measure_filter() { let qe = lower_sql( "SELECT count(*), count(nullable_value) FROM samples", &catalog, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2363,7 +2363,7 @@ async fn count_null_semantics_become_a_measure_filter() { let error = lower_sql( "SELECT count(nullable_value) FROM samples GROUP BY ROLLUP(value)", &catalog, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -2392,7 +2392,7 @@ async fn grouped_map_column_preserves_map_type() { "SELECT labels, max(value) AS value FROM raw_samples GROUP BY labels ORDER BY labels", &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2421,7 +2421,7 @@ async fn clickhouse_modulo_uses_native_arithmetic_types_and_nullability() { &format!("SELECT {call} AS value FROM numbers"), &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2429,7 +2429,7 @@ async fn clickhouse_modulo_uses_native_arithmetic_types_and_nullability() { &format!("SELECT {native} AS value FROM numbers"), &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2440,7 +2440,7 @@ async fn clickhouse_modulo_uses_native_arithmetic_types_and_nullability() { "SELECT modulo(n, 3) AS value FROM numbers", &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap() @@ -2480,7 +2480,7 @@ async fn original_o11y_map_queries_lower_with_typed_results() { sql, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_or_else(|e| panic!("{sql}: {e}")); @@ -2512,7 +2512,7 @@ async fn clickhouse_modulo_preserves_projection_names_and_outer_references() { sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2542,7 +2542,7 @@ async fn clickhouse_map_access_keeps_generated_names_and_rejects_variant_coercio "SELECT labels['job'] FROM t", &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2554,7 +2554,7 @@ async fn clickhouse_map_access_keeps_generated_names_and_rejects_variant_coercio "SELECT map()['a'] FROM t", &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .is_err()); @@ -2562,7 +2562,7 @@ async fn clickhouse_map_access_keeps_generated_names_and_rejects_variant_coercio "SELECT map('a', integer, 'b', floating) FROM t", &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact() + AccuracyTarget::exact() ) .await .is_err()); @@ -2594,7 +2594,7 @@ async fn arg_selector_result_schema_tracks_selected_argument() { sql, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2628,7 +2628,7 @@ async fn clickhouse_list_element_uses_canonical_typed_access() { sql, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2644,7 +2644,7 @@ async fn clickhouse_list_element_uses_canonical_typed_access() { sql, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact() + AccuracyTarget::exact() ) .await .is_err(), @@ -2687,7 +2687,7 @@ async fn clickhouse_tuple_element_preserves_declared_field_metadata() { sql, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2705,7 +2705,7 @@ async fn clickhouse_tuple_element_preserves_declared_field_metadata() { &sql, &catalog, SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact() + AccuracyTarget::exact() ) .await .is_err(), @@ -2738,7 +2738,7 @@ async fn composite_distinct_counts_tuples() { let composite = lower_sql( "SELECT COUNT(DISTINCT a, b) FROM t", &cat, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2755,7 +2755,7 @@ async fn composite_distinct_counts_tuples() { let single = lower_sql( "SELECT COUNT(DISTINCT a) FROM t", &cat, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -2784,7 +2784,7 @@ async fn composite_distinct_rejects_expression_arguments() { let error = lower_sql( "SELECT COUNT(DISTINCT a, b + 1) FROM t", &cat, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -2810,7 +2810,7 @@ async fn distinct_with_derived_sibling() { "SELECT count(DISTINCT a, b), sum(b + 1) FROM t", "SELECT count(DISTINCT a, b), corr(a,b) FROM t", ] { - let result = lower_sql(sql, &catalog, RequestedAccuracy::exact()).await; + let result = lower_sql(sql, &catalog, AccuracyTarget::exact()).await; assert!(result.is_ok(), "{sql}: {result:?}"); } } @@ -2934,7 +2934,7 @@ async fn measure_filter_inside_a_rollup_is_rejected() { let err = lower_sql( "SELECT service, count(*) FILTER (WHERE latency > 1.0) FROM metrics GROUP BY ROLLUP(service)", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -2942,7 +2942,7 @@ async fn measure_filter_inside_a_rollup_is_rejected() { } async fn lower_err(sql: &str) -> String { - lower_sql(sql, &catalog(), RequestedAccuracy::exact()) + lower_sql(sql, &catalog(), AccuracyTarget::exact()) .await .err() .unwrap_or_else(|| panic!("expected {sql:?} to be rejected")) @@ -3094,7 +3094,7 @@ async fn duration_in_a_case_branch_is_rejected_as_a_duration() { let err = asap_frontend_sql::lower_sql( "SELECT CASE WHEN true THEN INTERVAL '1 day' ELSE d - d END FROM t", &catalog, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err() diff --git a/crates/frontend-sql/tests/temporal_types.rs b/crates/frontend-sql/tests/temporal_types.rs index 82d1529c4..22a94d26f 100644 --- a/crates/frontend-sql/tests/temporal_types.rs +++ b/crates/frontend-sql/tests/temporal_types.rs @@ -1,6 +1,6 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn catalog() -> SqlCatalog { SqlCatalog::new().with_table( "t", @@ -16,7 +16,7 @@ async fn temporal_subtraction_rejects_unrepresentable_duration() { let error = lower_sql( "SELECT d - d AS elapsed FROM t", &catalog, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_err(); @@ -33,7 +33,7 @@ async fn date_shifts_keep_their_type() { "SELECT d + INTERVAL '30' DAY AS shifted FROM t", "SELECT d - CAST('1 day' AS INTERVAL) AS shifted FROM t", ] { - let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) + let node = lower_sql(query, &catalog(), AccuracyTarget::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Date); @@ -46,7 +46,7 @@ async fn interval_cast_lowers_like_interval_literal() { "SELECT INTERVAL '1' DAY AS duration FROM t", "SELECT CAST('1 day' AS INTERVAL) AS duration FROM t", ] { - let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) + let node = lower_sql(query, &catalog(), AccuracyTarget::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Interval); @@ -62,7 +62,7 @@ async fn nested_temporal_subtraction_is_rejected() { "SELECT CASE WHEN true THEN INTERVAL '1 day' ELSE d - d END FROM t", "SELECT d FROM t ORDER BY d - d", ] { - let result = lower_sql(query, &catalog(), RequestedAccuracy::exact()).await; + let result = lower_sql(query, &catalog(), AccuracyTarget::exact()).await; let error = result.expect_err(&format!("accepted unsupported duration: {query}")); assert!( error.to_string().contains("unsupported duration type"), @@ -79,7 +79,7 @@ async fn negative_intervals_keep_their_type() { "SELECT -CAST('1 day' AS INTERVAL) AS duration FROM t", "SELECT -(INTERVAL '1 day' + INTERVAL '2 days') AS duration FROM t", ] { - let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) + let node = lower_sql(query, &catalog(), AccuracyTarget::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Interval, "{query}"); @@ -93,7 +93,7 @@ async fn sql_date_literals_keep_their_type() { "SELECT DATE '2024-02-29' FROM t", "SELECT DATE '1969-12-31' + INTERVAL '1 day' FROM t", ] { - let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) + let node = lower_sql(query, &catalog(), AccuracyTarget::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Date); diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index 892df98dd..435730358 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -15,7 +15,7 @@ pub mod fixtures { use asap_types::ir::OperatorNode; use asap_types::pre_asap::schema::{DataType, Field, Schema}; - use asap_types::types::RequestedAccuracy; + use asap_types::types::AccuracyTarget; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, @@ -26,7 +26,7 @@ pub mod fixtures { /// suite's declared one-second source cadence. pub fn lower_promql( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result, asap_frontend_promql::PromqlError> { match lower_promql_root(query, accuracy)? { asap_types::ir::QueryRoot::Operator(node) => Ok(node), @@ -38,7 +38,7 @@ pub mod fixtures { pub fn lower_promql_root( query: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Result { let workload = PlanningWorkload { query_workload: QueryWorkload { @@ -146,14 +146,14 @@ pub mod post_asap { /// query, with the built-in models. pub mod planning { use asap_types::ir::{OperatorNode, QueryRoot}; - use asap_types::types::RequestedAccuracy; + use asap_types::types::AccuracyTarget; use asap_types::workload::DataWorkload; use std::rc::Rc; /// Every planning stage's output for the single query `root`. pub fn run_stage1_to_stage3( root: Rc, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> asap_aware_mapping::Stage1ToStage3Run { asap_aware_mapping::run_stage1_to_stage3( vec![(0, QueryRoot::Operator(root))], @@ -167,7 +167,7 @@ pub mod planning { /// The logical DAG the planner pipeline selects for the single query /// `root` at `accuracy`. - pub fn selected_dag(root: Rc, accuracy: RequestedAccuracy) -> Rc { + pub fn selected_dag(root: Rc, accuracy: AccuracyTarget) -> Rc { match run_stage1_to_stage3(root, accuracy).selected_logical() { Some([QueryRoot::Operator(node)]) => Rc::clone(node), other => panic!("one selected operator root: {other:?}"), @@ -179,7 +179,7 @@ pub mod planning { /// compose panics, so none is silently skipped. pub fn logical_candidates( root: Rc, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ) -> Vec> { run_stage1_to_stage3(root, accuracy) .stage1_logical_asap_optimization diff --git a/crates/integration-tests/tests/aggregate.rs b/crates/integration-tests/tests/aggregate.rs index 39821d80a..a85c5b025 100644 --- a/crates/integration-tests/tests/aggregate.rs +++ b/crates/integration-tests/tests/aggregate.rs @@ -16,10 +16,9 @@ use asap_integration_tests::fixtures::metric_schema; use asap_types::ir::{NonASAPOp, OperatorNode, TimeRangeKind}; use asap_types::pre_asap::{AggIntent, Reduction, Source}; use asap_types::types::AccuracyTarget; -use asap_types::types::RequestedAccuracy; fn lower(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } diff --git a/crates/integration-tests/tests/binary_op.rs b/crates/integration-tests/tests/binary_op.rs index 36f5e38fd..a8d32f407 100644 --- a/crates/integration-tests/tests/binary_op.rs +++ b/crates/integration-tests/tests/binary_op.rs @@ -16,10 +16,10 @@ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, BinaryOpKind, CompareOpKind, GroupSide, Reduction, Source, VectorGrouping, VectorMatch, VectorMatchKind, }; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn lower(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } diff --git a/crates/integration-tests/tests/exact_composition.rs b/crates/integration-tests/tests/exact_composition.rs index 3bc45e981..62b54ecc9 100644 --- a/crates/integration-tests/tests/exact_composition.rs +++ b/crates/integration-tests/tests/exact_composition.rs @@ -37,7 +37,6 @@ use asap_types::pre_asap::agg_intent::{default_quantile, AggIntent}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::types::AccuracyTarget; -use asap_types::types::RequestedAccuracy; // ── fixtures ──────────────────────────────────────────────────────────── @@ -809,7 +808,7 @@ fn physical_export_carries_explicit_timing_for_a_composed_plan() { fn promql_max_by_zone_over_quantile_over_time_composes() { let expr = lower_promql( "max by (zone) (quantile_over_time(0.99, latency[5m]))", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let space = plan(vec![("q", expr)], &StatsModel); diff --git a/crates/integration-tests/tests/filtered_aggregates.rs b/crates/integration-tests/tests/filtered_aggregates.rs index c2118b2cd..8c31f9c28 100644 --- a/crates/integration-tests/tests/filtered_aggregates.rs +++ b/crates/integration-tests/tests/filtered_aggregates.rs @@ -19,7 +19,7 @@ use asap_types::ir::physical_export::PhysicalASAPOperatorPayload; use asap_types::ir::{ASAPOp, NonASAPOp, Operator, OperatorNode, Predicate}; use asap_types::post_asap::{ExactKind, FieldDataType, SketchAlgorithm}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use futures::StreamExt; fn catalog() -> SqlCatalog { @@ -135,15 +135,15 @@ fn execute(root: &Rc) -> Vec { out } -async fn lower(sql: &str, accuracy: RequestedAccuracy) -> Rc { +async fn lower(sql: &str, accuracy: AccuracyTarget) -> Rc { lower_sql(sql, &catalog(), accuracy).await.unwrap() } /// The selected plan for an exact `sql`, through all three stages. async fn exact_plan(sql: &str) -> Rc { selected_dag( - lower(sql, RequestedAccuracy::exact()).await, - RequestedAccuracy::exact(), + lower(sql, AccuracyTarget::exact()).await, + AccuracyTarget::exact(), ) } @@ -204,7 +204,7 @@ fn with_summary_filter(root: &Rc, filter: &Predicate) -> Rc (Predicate, Schema) { - OperatorNode::reachable(&lower(sql, RequestedAccuracy::exact()).await) + OperatorNode::reachable(&lower(sql, AccuracyTarget::exact()).await) .into_iter() .find_map(|n| match n.non_asap() { Some(NonASAPOp::Aggregate { filters, child, .. }) => { @@ -222,7 +222,7 @@ async fn hand_filtered( family: impl Fn(&FieldDataType) -> bool, filter: &(Predicate, Schema), ) -> Vec> { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let mut result = vec![]; for root in logical_candidates(lower(sql, target.clone()).await, target) { let builds: Vec<_> = OperatorNode::reachable(&root) @@ -290,7 +290,7 @@ async fn hand_filtered_kll_compiles_and_executes() { /// The KLL candidates of `sql`, unchanged. async fn kll_candidates(sql: &str) -> Vec> { - let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); let roots: Vec<_> = logical_candidates(lower(sql, target.clone()).await, target) .into_iter() .filter(|root| { diff --git a/crates/integration-tests/tests/frequency_execution.rs b/crates/integration-tests/tests/frequency_execution.rs index a1a89cb5c..67535ad2c 100644 --- a/crates/integration-tests/tests/frequency_execution.rs +++ b/crates/integration-tests/tests/frequency_execution.rs @@ -19,7 +19,7 @@ use asap_types::ir::{ TimingMemo, }; use asap_types::post_asap::{FieldDataType, SketchAlgorithm}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; mod physical_common; @@ -124,7 +124,7 @@ fn standin_exact_and_univmon_candidates_execute() { 0.01, ), ] { - let accuracy = RequestedAccuracy::epsilon_delta(epsilon, delta); + let accuracy = AccuracyTarget::epsilon_delta(epsilon, delta); let root = lower_promql(query, accuracy.clone()).unwrap(); let root = promql_rows::with_series_identity(&root).unwrap(); let mut executed = Vec::new(); diff --git a/crates/integration-tests/tests/frontend_timestamps.rs b/crates/integration-tests/tests/frontend_timestamps.rs index 412bca345..91c0bde8d 100644 --- a/crates/integration-tests/tests/frontend_timestamps.rs +++ b/crates/integration-tests/tests/frontend_timestamps.rs @@ -4,15 +4,14 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_integration_tests::fixtures::lower_promql_root; use asap_types::ir::{NonASAPOp, ScalarExpr}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; /// PromQL exposes its evaluation time as Unix seconds, whereas SQL exposes /// `CURRENT_TIMESTAMP` as a timestamp. They represent related clock concepts /// but must remain distinguishable in the shared IR and type inference. #[tokio::test] async fn promql_eval_time_and_sql_current_timestamp_remain_distinct() { - let promql = - lower_promql_root("time()", RequestedAccuracy::exact()).expect("lower PromQL time()"); + let promql = lower_promql_root("time()", AccuracyTarget::exact()).expect("lower PromQL time()"); assert!( matches!( promql, @@ -35,7 +34,7 @@ async fn promql_eval_time_and_sql_current_timestamp_remain_distinct() { let sql = lower_sql( "SELECT CURRENT_TIMESTAMP FROM metrics", &catalog, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect("lower SQL CURRENT_TIMESTAMP"); diff --git a/crates/integration-tests/tests/materialization_e2e.rs b/crates/integration-tests/tests/materialization_e2e.rs index 4188666c6..b95faeb14 100644 --- a/crates/integration-tests/tests/materialization_e2e.rs +++ b/crates/integration-tests/tests/materialization_e2e.rs @@ -25,7 +25,7 @@ use asap_types::ir::{ use asap_types::post_asap::maintained_population::PopulationInput; use asap_types::post_asap::ExecutionTiming; use asap_types::pre_asap::Source; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; mod physical_common; @@ -34,7 +34,7 @@ const AT_INGESTION: Materialization = Materialization::MatAtIngestion(Medium::Me /// The logical DAG the planner pipeline selects for `query`, optionally with /// a PromQL series identity (per-entity state needs one). fn selected(query: &str, series_identity: bool) -> Rc { - selected_with(query, series_identity, RequestedAccuracy::epsilon(0.01)) + selected_with(query, series_identity, AccuracyTarget::epsilon(0.01)) } /// [`selected`] for a quantile query, whose target names its metric (a @@ -43,15 +43,11 @@ fn selected_quantile(query: &str, series_identity: bool) -> Rc { selected_with( query, series_identity, - RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Rank), + AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Rank), ) } -fn selected_with( - query: &str, - series_identity: bool, - accuracy: RequestedAccuracy, -) -> Rc { +fn selected_with(query: &str, series_identity: bool, accuracy: AccuracyTarget) -> Rc { let mut root = lower_promql(query, accuracy.clone()).unwrap(); if series_identity { root = promql_rows::with_series_identity(&root).unwrap(); @@ -62,7 +58,7 @@ fn selected_with( /// The first logical candidate for `query` that builds a sketch: plan /// selection picks the exact plan while every summary runs at query time. fn sketch_candidate(query: &str) -> Rc { - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let root = lower_promql(query, accuracy.clone()).unwrap(); logical_candidates(root, accuracy) .into_iter() @@ -261,7 +257,7 @@ fn materialization_decides_precompute_contents() { #[test] fn population_materialization_decides_precompute_contents() { let root = promql_rows::with_series_identity( - &lower_promql("topk by(job)(1, m)", RequestedAccuracy::epsilon(0.01)).unwrap(), + &lower_promql("topk by(job)(1, m)", AccuracyTarget::epsilon(0.01)).unwrap(), ) .unwrap(); let root = MaintainedPopulationStrategy::new(std::slice::from_ref(&root)) diff --git a/crates/integration-tests/tests/nested.rs b/crates/integration-tests/tests/nested.rs index 820034bd5..3e385824b 100644 --- a/crates/integration-tests/tests/nested.rs +++ b/crates/integration-tests/tests/nested.rs @@ -20,10 +20,10 @@ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, AtModifier, BinaryOpKind, CompareOpKind, GroupKeys, PromQLVectorSetOpKind, Reduction, ScalarValue, Source, TimeShift, VectorMatch, VectorMatchKind, }; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn lower(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } diff --git a/crates/integration-tests/tests/operator_design_examples.rs b/crates/integration-tests/tests/operator_design_examples.rs index 87284ff34..4eeaeab41 100644 --- a/crates/integration-tests/tests/operator_design_examples.rs +++ b/crates/integration-tests/tests/operator_design_examples.rs @@ -5,7 +5,7 @@ use asap_types::post_asap::{ ExactKind, ExactParams, FieldDataType, GroupingStrategy, SummaryUpdate, }; use asap_types::pre_asap::{AggIntent, ColumnRef, DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use std::rc::Rc; mod physical_common; @@ -30,7 +30,7 @@ async fn sql_filter_projection_example() { let root = lower_sql( "SELECT l_quantity * 2 AS q2 FROM lineitem WHERE l_quantity > 10", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -53,7 +53,7 @@ async fn sql_sum_projection_before_and_after_summary_rewrite() { let root = lower_sql( "SELECT SUM(bytes) + 1 AS total_bytes FROM requests WHERE status = 200", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -123,7 +123,7 @@ async fn sql_scalar_subquery_retains_its_cardinality_contract() { "SELECT (SELECT bytes FROM requests) AS v FROM lineitem", "SELECT l_quantity NOT IN (SELECT bytes FROM requests) AS present FROM lineitem", ] { - let root = lower_sql(query, &catalog(), RequestedAccuracy::exact()) + let root = lower_sql(query, &catalog(), AccuracyTarget::exact()) .await .unwrap(); root.validate_structure().unwrap(); @@ -150,7 +150,7 @@ async fn sql_sum_example_executes_with_sql_null_semantics() { let root = lower_sql( "SELECT SUM(bytes) + 1 AS total_bytes FROM requests WHERE status = 200", &catalog(), - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); @@ -248,7 +248,7 @@ async fn sql_window_and_filtered_aggregate_types() { "SELECT MIN(l_quantity) OVER (ORDER BY l_quantity ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING) AS s FROM lineitem", "SELECT SUM(l_quantity) FILTER (WHERE l_quantity < 0) AS s FROM lineitem GROUP BY l_quantity", ] { - let root = lower_sql(query, &catalog(), RequestedAccuracy::exact()).await.unwrap(); + let root = lower_sql(query, &catalog(), AccuracyTarget::exact()).await.unwrap(); root.validate_structure().unwrap(); assert_eq!(root.schema.fields[0], Field::plain("s", DataType::Int64, true), "{query}"); } @@ -282,7 +282,7 @@ async fn batch_planning_replaces_summary_operators() { .map(|query| BatchEntry { query: Query((*query).into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::exact()), ..Default::default() }, predictability: Predictability::Unknown, diff --git a/crates/integration-tests/tests/operator_sharing.rs b/crates/integration-tests/tests/operator_sharing.rs index e5d643fd3..da81dcc64 100644 --- a/crates/integration-tests/tests/operator_sharing.rs +++ b/crates/integration-tests/tests/operator_sharing.rs @@ -13,7 +13,7 @@ use asap_integration_tests::planning::selected_dag; use asap_integration_tests::post_asap::post_asap_dag; use asap_types::ir::{ASAPOp, NonASAPOp, Operator, OperatorNode}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn col(name: &str, dtype: DataType) -> Field { Field::plain(name, dtype, false) @@ -46,7 +46,7 @@ fn catalog() -> SqlCatalog { } /// Lower `sql` and take the logical DAG the planner pipeline selects. -async fn plan(sql: &str, accuracy: RequestedAccuracy) -> Rc { +async fn plan(sql: &str, accuracy: AccuracyTarget) -> Rc { let pre = lower_sql(sql, &catalog(), accuracy.clone()) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")); @@ -97,7 +97,7 @@ async fn project_above_and_scan_below_a_summary_are_both_non_asap_nodes() { let root = plan( "WITH metric AS (SELECT avg(CASE WHEN l_quantity BETWEEN 1 AND 50 THEN 1.0 ELSE 0.0 END) \ AS in_range FROM lineitem) SELECT in_range, in_range = 1.0 AS ok FROM metric", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await; // The root is the outer SELECT list: a NonASAP Project. @@ -128,7 +128,7 @@ async fn project_above_and_scan_below_a_summary_are_both_non_asap_nodes() { async fn exact_aggregate_and_sketch_share_one_scan() { let root = plan( "SELECT avg(l_extendedprice), approx_percentile_cont(l_discount, 0.99) FROM lineitem", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; let exact = find_all(&root, |node| { @@ -162,7 +162,7 @@ async fn each_side_of_union_all_holds_a_summary_estimate() { let root = plan( "SELECT approx_distinct(l_partkey) FROM lineitem \ UNION ALL SELECT approx_distinct(l_suppkey) FROM lineitem", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; // The SQL front end lowers UNION ALL to `SetOp { all: true }`. diff --git a/crates/integration-tests/tests/per_computation_candidates_sql.rs b/crates/integration-tests/tests/per_computation_candidates_sql.rs index 8ffbbaa3d..dfbabed9f 100644 --- a/crates/integration-tests/tests/per_computation_candidates_sql.rs +++ b/crates/integration-tests/tests/per_computation_candidates_sql.rs @@ -11,7 +11,7 @@ use asap_types::ir::{ASAPOp, OperatorNode, QueryRoot}; use asap_types::post_asap::{FieldDataType, SketchAlgorithm, SummaryInputExpr}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::ColumnRef; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; async fn targets(sql: &str) -> Vec { let catalog = SqlCatalog::new().with_table( @@ -21,7 +21,7 @@ async fn targets(sql: &str) -> Vec { Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let accuracy = AccuracyTarget::epsilon_delta(0.01, 0.01); let root = lower_sql(sql, &catalog, accuracy) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")); @@ -96,7 +96,7 @@ async fn a_filter_or_group_key_over_an_estimate_has_no_guarantee() { Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); for sql in [ "SELECT count(*) FILTER (WHERE p > 5) FROM \ (SELECT src, approx_percentile_cont(bytes, 0.99) AS p FROM flows GROUP BY src) s", @@ -163,7 +163,7 @@ async fn a_sql_fold_over_estimates_leaves_its_row_count_unbound() { Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let sql = "SELECT max(p) FROM \ (SELECT src, approx_percentile_cont(bytes, 0.99) AS p FROM flows GROUP BY src) s"; let root = lower_sql(sql, &catalog, accuracy.clone()).await.unwrap(); @@ -191,7 +191,7 @@ async fn heap_updates(key: &str, key_type: DataType, sql: &str) -> Vec<(String, Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = RequestedAccuracy::epsilon_delta(0.01, 0.01); + let accuracy = AccuracyTarget::epsilon_delta(0.01, 0.01); let root = lower_sql(sql, &catalog, accuracy).await.unwrap(); let inventory = generate_per_computation_candidates(vec![(0, QueryRoot::Operator(root))]).unwrap(); diff --git a/crates/integration-tests/tests/planner_layering_example2.rs b/crates/integration-tests/tests/planner_layering_example2.rs index 36944c9a3..265f4bbb1 100644 --- a/crates/integration-tests/tests/planner_layering_example2.rs +++ b/crates/integration-tests/tests/planner_layering_example2.rs @@ -24,7 +24,7 @@ use asap_types::post_asap::{ Field, ProbabilityExpr, Schema, SketchAlgorithm, SketchParams, SketchStatistic, }; use asap_types::pre_asap::DataType; -use asap_types::types::{AccuracyScope, RequestedAccuracy}; +use asap_types::types::{AccuracyScope, AccuracyTarget}; use asap_types::workload::{ AccuracyRequirement, DataArrival, DataDistribution, DataWorkload, DurationMs, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, @@ -59,9 +59,9 @@ fn panel(query: &str, (epsilon, delta): (f64, f64), scope: AccuracyScope) -> Rep query: Query(query.into()), demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy { + accuracy: AccuracyRequirement::Explicit(AccuracyTarget { scope, - ..RequestedAccuracy::epsilon_delta(epsilon, delta) + ..AccuracyTarget::epsilon_delta(epsilon, delta) }), response_latency: LatencyRequirement::Unspecified, }, @@ -678,9 +678,9 @@ fn stage3_plan_selection_accepts_kmv_for_any_failure_probability_without_a_bound .repeating_queries .as_mut() .expect("repeating queries")[0]; - q1.requirements.accuracy = AccuracyRequirement::Explicit(RequestedAccuracy { + q1.requirements.accuracy = AccuracyRequirement::Explicit(AccuracyTarget { scope: AccuracyScope::PerValue, - ..RequestedAccuracy::epsilon_delta(0.02, delta) + ..AccuracyTarget::epsilon_delta(0.02, delta) }); let run = run_stages(&workload, lower_promql(&workload)); for id in q1_only_candidates(&run, "Kmv") { @@ -952,8 +952,7 @@ fn stage3_plan_selection_selected_kmv_plan_executes_end_to_end() { let entries: Vec<_> = workload.query_workload.entries().collect(); let targets: Vec<_> = entries .iter() - .zip(&roots) - .map(|(e, root)| Some(e.requirements.accuracy.requested().for_root(root).unwrap())) + .map(|e| Some(e.requirements.accuracy.requested())) .collect(); let recurrence: Vec<_> = entries.iter().map(|e| e.recurrence.clone()).collect(); let selection = select_plan( diff --git a/crates/integration-tests/tests/precompute_raw_samples.rs b/crates/integration-tests/tests/precompute_raw_samples.rs index 71fe0cb37..72278948a 100644 --- a/crates/integration-tests/tests/precompute_raw_samples.rs +++ b/crates/integration-tests/tests/precompute_raw_samples.rs @@ -23,7 +23,7 @@ use asap_types::post_asap::{ SummaryUpdate, }; use asap_types::pre_asap::{expr_ir::ColumnRef, Reduction}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use futures::{executor::block_on, StreamExt}; type Series = BTreeMap; @@ -53,7 +53,7 @@ fn canonical(labels: &Series) -> Series { /// `m` declared a counter: plan selection's answer to a heap's open /// requirement that its weights are counter samples, which the executor /// needs before it builds a Count-Min heap. -fn candidates(query: &str, accuracy: RequestedAccuracy) -> Vec> { +fn candidates(query: &str, accuracy: AccuracyTarget) -> Vec> { use asap_aware_mapping::stage3_plan_selection::with_confirmed_weights; use asap_types::ir::QueryRoot; use asap_types::workload::{DataWorkload, MetricType}; @@ -396,11 +396,11 @@ fn check( // and each population's estimates equal feeding its kernel sample by sample. #[test] fn raw_sample_summaries_compile_and_match_their_kernels() { - let exact = RequestedAccuracy::exact(); - let sketch = RequestedAccuracy::epsilon(0.02); + let exact = AccuracyTarget::exact(); + let sketch = AccuracyTarget::epsilon(0.02); // A UnivMon sized for a distinct count at ε = 0.02 holds a heap of // millions of keys on each of its 19 layers, past the default 64 MiB. - let distinct = RequestedAccuracy::epsilon_delta(0.2, 0.1); + let distinct = AccuracyTarget::epsilon_delta(0.2, 0.1); let queries = [ ("sum_over_time(m[5m])", &exact), ("count_over_time(m[5m])", &exact), @@ -483,7 +483,7 @@ fn raw_sample_summaries_compile_and_match_their_kernels() { fn grouped_raw_summary(family: FieldDataType, input: SummaryUpdate) -> (PhysicalASAPDAG, u64, u64) { let candidate = candidates( "sum by (service) (sum_over_time(m[5m]))", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .pop() .unwrap(); diff --git a/crates/integration-tests/tests/promql_numeric_regressions.rs b/crates/integration-tests/tests/promql_numeric_regressions.rs index 7b810ea91..71aae1320 100644 --- a/crates/integration-tests/tests/promql_numeric_regressions.rs +++ b/crates/integration-tests/tests/promql_numeric_regressions.rs @@ -6,12 +6,12 @@ use asap_integration_tests::post_asap::post_asap_dag; use asap_types::ir::{ASAPOp, NonASAPOp, Operator, OperatorNode}; use asap_types::post_asap::{ExactKind, FieldDataType, SummaryInputExpr, SummaryUpdate}; use asap_types::pre_asap::{ColumnRef, Reduction}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use std::rc::Rc; /// The first logical candidate (in enumeration order) with an ASAP operator, /// else the exact one. -fn plan(query: &str, accuracy: RequestedAccuracy) -> Rc { +fn plan(query: &str, accuracy: AccuracyTarget) -> Rc { let pre = lower_promql(query, accuracy.clone()).unwrap(); let candidates = logical_candidates(pre, accuracy); candidates @@ -51,7 +51,7 @@ fn contribution(family: &FieldDataType, update: &SummaryUpdate, value: f64) -> f /// Target count depends on series multiplicity, never distinct values or health. #[test] fn count_up_counts_targets_even_when_values_repeat_or_change_sign() { - let node = plan("count(up)", RequestedAccuracy::exact()); + let node = plan("count(up)", AccuracyTarget::exact()); let (family, update, _) = aggregate(&node); assert!(matches!( family, @@ -76,7 +76,7 @@ fn window_counts_and_sums_distinguish_one_zero_three_and_negative_values() { ("count_over_time(up[5m])", ExactKind::Count, true), ("sum_over_time(up[5m])", ExactKind::Sum, false), ] { - let node = plan(query, RequestedAccuracy::exact()); + let node = plan(query, AccuracyTarget::exact()); let (family, update, reduction) = aggregate(&node); assert!(matches!(family, FieldDataType::ExactAggregate(k, _) if *k == kind)); assert_eq!(*reduction, Reduction::PerEntity); @@ -96,7 +96,7 @@ fn sum_rate_and_increase_have_real_exact_accumulator_nodes() { ("rate(requests_total[5m])", ExactKind::Rate), ("increase(requests_total[5m])", ExactKind::Increase), ] { - let node = plan(query, RequestedAccuracy::exact()); + let node = plan(query, AccuracyTarget::exact()); let (family, _, _) = aggregate(&node); assert!(matches!(family, FieldDataType::ExactAggregate(k, _) if *k == kind)); assert!(node.guarantee.as_ref().unwrap().is_exact()); @@ -109,7 +109,7 @@ fn sum_rate_and_increase_have_real_exact_accumulator_nodes() { fn checked_ratio_must_not_certify_cross_zero_interpolation() { let node = plan( "quantile_over_time(0.5, data[5m]) / quantile_over_time(0.9, data[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ); assert!( matches!(node.operator, Operator::NonASAP(NonASAPOp::BinaryOp { .. })) @@ -151,7 +151,7 @@ fn quantile_over_temporal_average_keeps_a_legal_candidate() { "quantile(0.9, avg_over_time(a[5m]))", "quantile(0.9, avg_over_time(a[5m]) + avg_over_time(b[5m]))", ] { - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let pre = lower_promql(query, accuracy.clone()).unwrap(); let node = logical_candidates(pre, accuracy) .into_iter() @@ -180,7 +180,7 @@ fn sketch_counts_use_unit_weights_and_signed_sums_keep_value_weights() { } else { "topk(1, sum_over_time(up[5m]))" }; - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let pre = lower_promql(query, accuracy.clone()).unwrap(); let candidates = logical_candidates(pre, accuracy); // The heap state each summarized candidate builds. @@ -272,7 +272,7 @@ fn sketch_counts_use_unit_weights_and_signed_sums_keep_value_weights() { #[test] #[should_panic(expected = "does not compose")] fn logical_candidates_fails_on_a_candidate_that_does_not_compose() { - let accuracy = RequestedAccuracy::epsilon(0.01); + let accuracy = AccuracyTarget::epsilon(0.01); let pre = lower_promql( "sum by (job) (topk by (job, instance) (1, rate(m[5m])))", accuracy.clone(), diff --git a/crates/integration-tests/tests/promql_to_post_asap.rs b/crates/integration-tests/tests/promql_to_post_asap.rs index 9ab239347..12f4d5ff2 100644 --- a/crates/integration-tests/tests/promql_to_post_asap.rs +++ b/crates/integration-tests/tests/promql_to_post_asap.rs @@ -33,7 +33,6 @@ use asap_types::pre_asap::expr_ir::ColumnRef; use asap_types::pre_asap::schema::DataType; use asap_types::types::AccuracyTarget; use asap_types::types::ErrorMetric; -use asap_types::types::RequestedAccuracy; /// This crate has no "bind me one tree" public API any more — /// `ASAPStrategies::replacements` always returns every candidate, and @@ -68,7 +67,7 @@ fn distinct_over_time_offers_hll_cardinality_evaluation() { // function-specific post-ASAP node or a sample-count rewrite. let root = lower_promql( "distinct_over_time(cpu_usage{job=\"worker\"}[5m])", - RequestedAccuracy::epsilon(0.02), + AccuracyTarget::epsilon(0.02), ) .unwrap(); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); @@ -87,7 +86,7 @@ fn distinct_over_time_offers_hll_cardinality_evaluation() { } fn lower_search_and_materialize(query: &str) -> Rc { - let pre = lower_promql(query, RequestedAccuracy::exact()).expect("lowering failed"); + let pre = lower_promql(query, AccuracyTarget::exact()).expect("lowering failed"); let space = search_workload(vec![("query", pre)]).unwrap(); let selection = space.global_selection(&DefaultCostModel); selection @@ -185,7 +184,7 @@ fn dtype<'a>(schema: &'a Schema, name: &str) -> &'a FieldDataType { } fn lower_and_realize(query: &str) -> Rc { - let pre = lower_promql(query, RequestedAccuracy::exact()).expect("lowering failed"); + let pre = lower_promql(query, AccuracyTarget::exact()).expect("lowering failed"); realize(&pre).expect("binding failed") } @@ -267,7 +266,7 @@ impl AccuracyEvidenceProvider for SeparatedTopK { fn grouped_rate_topk_consumes_finalized_rate_values() { let root = lower_promql( "topk by(job)(2, sum by(service, job)(rate(m[1m])))", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -330,7 +329,7 @@ fn weighted_topk_keeps_candidates_with_missing_population_evidence() { } let root = lower_promql( "topk by(job)(2, sum by(service, job)(rate(m[1m])))", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -350,7 +349,7 @@ fn weighted_topk_keeps_candidates_with_missing_population_evidence() { fn weighted_topk_exports_symbolic_evidence_requirements() { let root = lower_promql( "topk by(job)(2, sum by(service, job)(rate(m[1m])))", - RequestedAccuracy::epsilon_delta(0.01, 0.01), + AccuracyTarget::epsilon_delta(0.01, 0.01), ) .unwrap(); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); @@ -380,7 +379,7 @@ fn weighted_topk_rejects_invalid_population_evidence() { } let root = lower_promql( "topk by(job)(2, sum by(service, job)(rate(m[1m])))", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -399,7 +398,7 @@ fn rate_and_increase_topk_use_summary_scores_and_grouped_limits() { "topk by(job)(2, sum by(service, job)(rate(m[1m])))", "topk(2, sum by(job)(increase(m[6h])))", ] { - let root = lower_promql(query, RequestedAccuracy::epsilon_delta(0.01, 0.01)).unwrap(); + let root = lower_promql(query, AccuracyTarget::epsilon_delta(0.01, 0.01)).unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( &DefaultCostModel, &DefaultAccuracyModel, @@ -541,7 +540,7 @@ fn promql_binary_arithmetic_preserves_nested_structure_and_rejects_modifiers() { fn promql_binary_arithmetic_never_relabels_approximate_children_as_exact() { let pre = lower_promql( "quantile_over_time(0.9, a[1m]) + quantile_over_time(0.9, b[1m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .expect("lowering failed"); let root = realize(&pre).expect("binding failed"); @@ -561,7 +560,7 @@ fn ddsketch_quantile_ratio_meets_the_shared_relative_error_target() { // Enforced finite, positive, nonempty windows justify both DDSketch // interpolation bounds and a nonzero denominator. Shared state does not // require independence for the deterministic ratio bound. - let requested = RequestedAccuracy::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Relative); + let requested = AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Relative); let target = requested.target_in(ErrorMetric::Relative); let query = lower_promql( "quantile_over_time(0.9, data[5m]) / quantile_over_time(0.5, data[5m])", @@ -632,7 +631,7 @@ fn planner_only_e2e_temporal_topk_preserves_query_update_and_evaluation_contract ), ]; for (source, expected_update, expected_family, excluded_labels) in cases { - let pre = lower_promql(source, RequestedAccuracy::epsilon_delta(0.01, 0.01)) + let pre = lower_promql(source, AccuracyTarget::epsilon_delta(0.01, 0.01)) .expect("lower temporal Top-K"); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( &DefaultCostModel, @@ -794,7 +793,7 @@ fn planner_heap_topk_reference_execution_matches_ground_truth() { vec![("worker", 100.0), ("cron", 30.0)], ), ] { - let pre = lower_promql(query, RequestedAccuracy::epsilon_delta(0.01, 0.01)).unwrap(); + let pre = lower_promql(query, AccuracyTarget::epsilon_delta(0.01, 0.01)).unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( &DefaultCostModel, &DefaultAccuracyModel, @@ -834,7 +833,7 @@ fn planner_heap_topk_reference_execution_matches_ground_truth() { fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { let pre_asap = lower_promql( "quantile(0.99, rate(http_requests_total[5m]))", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .expect("lowering failed"); let root = realize(&pre_asap).expect("binding failed"); @@ -967,7 +966,7 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { fn promql_exact_workload_binds_accumulators_not_sketches() { let pre_asap = lower_promql( "sum by (job) (http_requests_total)", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .expect("lowering failed"); let root = realize(&pre_asap).expect("binding failed"); @@ -992,8 +991,8 @@ fn promql_exact_workload_binds_accumulators_not_sketches() { "group keys pass through verbatim" ); - let pre_asap = lower_promql("avg(http_requests_total)", RequestedAccuracy::exact()) - .expect("lowering failed"); + let pre_asap = + lower_promql("avg(http_requests_total)", AccuracyTarget::exact()).expect("lowering failed"); let root = realize(&pre_asap).expect("binding failed"); assert!( !root.contains_asap(), @@ -1005,7 +1004,7 @@ fn promql_exact_workload_binds_accumulators_not_sketches() { fn promql_sum_of_count_over_time_is_composed_by_default_search() { let original = lower_promql( "sum by (service) (count_over_time(metrics[5m]))", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .expect("lowering failed"); let original_schema = original.schema.clone(); @@ -1053,7 +1052,7 @@ fn nested_summary_explicitly_finalizes_exact_child_at_ingestion_time() { // sketch must not interpret exact accumulator bytes as input samples. let pre = lower_promql( "quantile(0.9, sum_over_time(m[1m]))", - RequestedAccuracy::epsilon(0.05), + AccuracyTarget::epsilon(0.05), ) .unwrap(); let space = search_workload(vec![("query", pre)]).unwrap(); @@ -1116,7 +1115,7 @@ fn physical_node_owns_phase_independently_of_binary_payload() { ExecutionTiming::QueryTime, ), ] { - let input = lower_promql(query, RequestedAccuracy::epsilon(0.05)).unwrap(); + let input = lower_promql(query, AccuracyTarget::epsilon(0.05)).unwrap(); let search = search_workload(vec![("q", input)]).unwrap(); let choice = search.global_selection(&DefaultCostModel); let plan = choice @@ -1150,7 +1149,7 @@ fn physical_node_owns_phase_independently_of_binary_payload() { fn ddsketch_ratio_without_domain_proof_is_uncertified() { let pre = lower_promql( "quantile_over_time(0.9, data[5m]) / quantile_over_time(0.5, data[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let root = realize(&pre).unwrap(); @@ -1224,7 +1223,7 @@ fn ddsketch_ratio_rejects_unsafe_domains() { let evidence = FixtureQuantileDomain { lower, upper }; let pre = lower_promql( "quantile_over_time(0.9, data[5m]) / quantile_over_time(0.5, data[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -1264,7 +1263,7 @@ fn ddsketch_ratio_rejects_one_invalid_domain_when_the_other_is_missing() { let pre = lower_promql( "quantile_over_time(0.9, data[5m]) / quantile_over_time(0.5, data[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -1285,7 +1284,7 @@ fn ddsketch_ratio_bound_holds_for_signed_pinned_sketch_evaluations() { }; let pre = lower_promql( "quantile_over_time(0.9, data[5m]) / quantile_over_time(0.5, data[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -1365,7 +1364,7 @@ fn ddsketch_ratio_requires_a_supported_population_size() { } let pre = lower_promql( "quantile_over_time(0.9, data[5m]) / quantile_over_time(0.5, data[5m])", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .unwrap(); for count in [0, (1u64 << 53) + 1] { @@ -1384,8 +1383,8 @@ fn ddsketch_ratio_requires_a_supported_population_size() { #[test] fn without_aggregation_candidates_export_valid_dags() { for accuracy in [ - RequestedAccuracy::exact(), - RequestedAccuracy::epsilon_delta(0.01, 0.01), + AccuracyTarget::exact(), + AccuracyTarget::epsilon_delta(0.01, 0.01), ] { for query in ["sum without (pod) (m)", "quantile without (pod) (0.5, m)"] { let root = lower_promql(query, accuracy.clone()).unwrap(); diff --git a/crates/integration-tests/tests/scan.rs b/crates/integration-tests/tests/scan.rs index e4abbf69e..e380713e7 100644 --- a/crates/integration-tests/tests/scan.rs +++ b/crates/integration-tests/tests/scan.rs @@ -15,10 +15,10 @@ use asap_types::ir::{ ExprSemantics, NonASAPOp, OperatorNode, Predicate, ScalarExpr, TimeRangeKind, }; use asap_types::pre_asap::{CompareOpKind, ScalarValue, Source}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn lower(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } diff --git a/crates/integration-tests/tests/schema.rs b/crates/integration-tests/tests/schema.rs index bc2b5cf05..6028139b9 100644 --- a/crates/integration-tests/tests/schema.rs +++ b/crates/integration-tests/tests/schema.rs @@ -10,10 +10,10 @@ //! are label-preserving and keep the schema open. use asap_integration_tests::fixtures::lower_promql; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; fn lower(q: &str) -> std::rc::Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } diff --git a/crates/integration-tests/tests/sql_cardinality.rs b/crates/integration-tests/tests/sql_cardinality.rs index df6e76d24..4b8dcf023 100644 --- a/crates/integration-tests/tests/sql_cardinality.rs +++ b/crates/integration-tests/tests/sql_cardinality.rs @@ -4,7 +4,7 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_physical_operators::values::Value; use asap_types::{ pre_asap::{DataType, Field, Schema}, - types::RequestedAccuracy, + types::AccuracyTarget, }; // COUNT(DISTINCT src_ip) skips NULL and keeps the WHERE, Utf8 identities and @@ -21,7 +21,7 @@ async fn sql_distinct_executes_through_raw_scan_and_native_binding() { let root = lower_sql( "SELECT COUNT(DISTINCT src_ip) AS sources FROM flows WHERE keep", &catalog, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap(); diff --git a/crates/integration-tests/tests/sql_frequency.rs b/crates/integration-tests/tests/sql_frequency.rs index 7cb40514b..61224230d 100644 --- a/crates/integration-tests/tests/sql_frequency.rs +++ b/crates/integration-tests/tests/sql_frequency.rs @@ -6,7 +6,7 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_physical_operators::values::Value; use asap_types::{ pre_asap::{DataType, Field, Schema}, - types::RequestedAccuracy, + types::AccuracyTarget, }; const ENTROPY: &str = "SELECT -SUM(p * LN(p)) FROM (\ @@ -29,7 +29,7 @@ fn rows(sources: &[&str]) -> Vec> { } async fn run(sql: &str, sources: &[&str]) -> Option { - let root = lower_sql(sql, &catalog(), RequestedAccuracy::exact()) + let root = lower_sql(sql, &catalog(), AccuracyTarget::exact()) .await .unwrap(); let result = physical_common::execute_raw_rows(&root, rows(sources)); diff --git a/crates/integration-tests/tests/sql_to_physical.rs b/crates/integration-tests/tests/sql_to_physical.rs index 644c47aea..e122c3d12 100644 --- a/crates/integration-tests/tests/sql_to_physical.rs +++ b/crates/integration-tests/tests/sql_to_physical.rs @@ -10,7 +10,7 @@ use asap_physical_operators::{ values::{Batch, Value}, }; use asap_types::ir::physical_export::PhysicalASAPOperatorPayload; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::{ post_asap::FieldDataType, pre_asap::{DataType, Field, Schema}, @@ -34,10 +34,10 @@ async fn sql_filter_grouped_sum_executes_and_rebinds() { "SELECT service, SUM(value) AS total FROM metrics WHERE value > 1 GROUP BY service", "SELECT service, SUM(value) AS total FROM metrics GROUP BY service", ] { - let logical = lower_sql(query, &catalog, RequestedAccuracy::exact()) + let logical = lower_sql(query, &catalog, AccuracyTarget::exact()) .await .unwrap(); - let selected = selected_dag(logical, RequestedAccuracy::exact()); + let selected = selected_dag(logical, AccuracyTarget::exact()); let dag = compile_physical_asap_dag(&selected).unwrap(); let scan = dag .nodes diff --git a/crates/integration-tests/tests/sql_to_post_asap.rs b/crates/integration-tests/tests/sql_to_post_asap.rs index a97c1ad48..633358954 100644 --- a/crates/integration-tests/tests/sql_to_post_asap.rs +++ b/crates/integration-tests/tests/sql_to_post_asap.rs @@ -35,7 +35,7 @@ use asap_types::post_asap::{ }; use asap_types::pre_asap::expr_ir::ColumnRef; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::SqlDialect; /// This crate has no "bind me one tree" public API any more — @@ -118,7 +118,7 @@ fn catalog() -> SqlCatalog { ) } -async fn lower(sql: &str, accuracy: RequestedAccuracy) -> Rc { +async fn lower(sql: &str, accuracy: AccuracyTarget) -> Rc { lower_sql(sql, &catalog(), accuracy) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")) @@ -144,7 +144,7 @@ async fn clickhouse_temporal_sql_reuses_rate_and_increase_physical_summaries() { &sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect("explicit temporal SQL must lower"); @@ -190,7 +190,7 @@ async fn clickhouse_outer_sum_recursively_binds_inner_temporal_aggregate() { &sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect("nested temporal SQL must lower"); @@ -244,7 +244,7 @@ fn inner_aggregate(node: &Rc) -> &Rc { async fn sql_full_query_retains_project_and_binds_inner_aggregate() { let pre_asap = lower( "SELECT approx_percentile_cont(latency, 0.99) AS p99 FROM metrics", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; let Some(NonASAPOp::Project { @@ -297,7 +297,7 @@ async fn sql_join_recursively_binds_both_temporal_aggregate_children() { ON b.service=a.service", &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .expect("two-subquery rate ratio must lower"); @@ -413,7 +413,7 @@ async fn unsupported_sql_join_shapes_remain_fail_closed() { sql, &catalog(), SqlDialect::ClickhouseSQL, - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await .unwrap_or_else(|error| panic!("join must lower before fail-closed mapping: {error}")); @@ -443,7 +443,7 @@ async fn sql_relational_parents_retain_summary_bound_aggregate() { (SELECT service, approx_percentile_cont(latency, 0.9) AS p \ FROM metrics GROUP BY service) t \ WHERE t.p > 100 ORDER BY t.p DESC LIMIT 5", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; let space = search_workload(vec![("query", Rc::clone(&pre_asap))]).unwrap(); @@ -496,7 +496,7 @@ async fn sql_filter_keeps_read_predicate_and_summary_population_selection() { (SELECT service, approx_percentile_cont(latency, 0.9) AS p \ FROM metrics WHERE service = 'api' GROUP BY service) t \ WHERE t.p > 100", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; let expected_read_predicate = { @@ -590,7 +590,7 @@ async fn sql_filter_preserves_local_fallback_boundary_for_unsupported_child() { "SELECT t.service, t.avg_bytes FROM \ (SELECT service, AVG(bytes) AS avg_bytes FROM metrics GROUP BY service) t \ WHERE t.avg_bytes > 100", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await; let space = search_workload(vec![("query", Rc::clone(&pre_asap))]).unwrap(); @@ -639,7 +639,7 @@ async fn sql_filter_preserves_local_fallback_boundary_for_unsupported_child() { async fn sql_quantile_binds_kll_sketch_over_named_column() { let pre_asap = lower( "SELECT approx_percentile_cont(latency, 0.99) FROM metrics", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; let agg = inner_aggregate(&pre_asap); @@ -741,7 +741,7 @@ async fn sql_quantile_binds_kll_sketch_over_named_column() { async fn sql_count_distinct_with_epsilon_binds_hll_over_named_column() { let pre_asap = lower( "SELECT COUNT(DISTINCT service) FROM metrics", - RequestedAccuracy::epsilon(0.01), + AccuracyTarget::epsilon(0.01), ) .await; let agg = inner_aggregate(&pre_asap); @@ -802,7 +802,7 @@ async fn sql_count_distinct_with_epsilon_binds_hll_over_named_column() { async fn sql_exact_workload_binds_accumulators_not_sketches() { let pre_asap = lower( "SELECT service, SUM(bytes) FROM metrics GROUP BY service", - RequestedAccuracy::exact(), + AccuracyTarget::exact(), ) .await; let agg = inner_aggregate(&pre_asap); @@ -828,7 +828,7 @@ async fn sql_exact_workload_binds_accumulators_not_sketches() { "group keys pass through verbatim" ); - let pre_asap = lower("SELECT AVG(bytes) FROM metrics", RequestedAccuracy::exact()).await; + let pre_asap = lower("SELECT AVG(bytes) FROM metrics", AccuracyTarget::exact()).await; let agg = inner_aggregate(&pre_asap); let root = realize(agg).expect("binding failed"); assert!( @@ -845,7 +845,7 @@ async fn sql_exact_workload_binds_accumulators_not_sketches() { async fn map_projection_export_preserves_unsupported_child_boundary() { let pre = lower_sql_dialect( "SELECT map('job', t.service) AS labels, t.avg_bytes FROM (SELECT service, AVG(bytes) AS avg_bytes FROM metrics GROUP BY service) t WHERE t.avg_bytes > 100", - &catalog(), SqlDialect::ClickhouseSQL, RequestedAccuracy::exact(), + &catalog(), SqlDialect::ClickhouseSQL, AccuracyTarget::exact(), ).await.unwrap(); let space = search_workload(vec![("map_query", pre)]).unwrap(); let root = space diff --git a/crates/integration-tests/tests/stage1_logical_asap_optimization.rs b/crates/integration-tests/tests/stage1_logical_asap_optimization.rs index 1f0303cb1..e97046aa8 100644 --- a/crates/integration-tests/tests/stage1_logical_asap_optimization.rs +++ b/crates/integration-tests/tests/stage1_logical_asap_optimization.rs @@ -21,7 +21,7 @@ use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode, QueryRoot, ScalarExpr}; use asap_types::post_asap::{FieldDataType, SketchAlgorithm, SummaryInputExpr, SummaryUpdate}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::{ColumnRef, CompareOpKind, ScalarValue}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Query, QueryLanguage, QueryRequirements, QueryWorkload, @@ -30,8 +30,8 @@ use futures::StreamExt; mod physical_common; -fn accuracy() -> RequestedAccuracy { - RequestedAccuracy::epsilon_delta(0.01, 0.01) +fn accuracy() -> AccuracyTarget { + AccuracyTarget::epsilon_delta(0.01, 0.01) } async fn sql(query: &str) -> QueryRoot { diff --git a/crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs b/crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs index 0e93ab058..aca6d2155 100644 --- a/crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs +++ b/crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs @@ -11,10 +11,10 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_types::ir::{NonASAPOp, OperatorNode, QueryRoot, ScalarExpr}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; use asap_types::pre_asap::AggIntent; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; -fn approximate() -> RequestedAccuracy { - RequestedAccuracy::epsilon_delta(0.01, 0.001) +fn approximate() -> AccuracyTarget { + AccuracyTarget::epsilon_delta(0.01, 0.001) } fn names(node: &OperatorNode) -> Vec { @@ -53,7 +53,7 @@ fn exact_topk_sort(root: &OperatorNode) -> Option<(String, bool)> { /// candidates had an exact top-k. async fn check_every_choice( sql: &str, - accuracy: RequestedAccuracy, + accuracy: AccuracyTarget, ranked: &str, root_names: &[&str], ) -> usize { @@ -131,7 +131,7 @@ async fn check_every_choice( /// Each query under an exact and an approximate target; every exact top-k /// sorts by the ranked measure. async fn check(sql: &str, ranked: &str, root_names: &[&str]) { - for accuracy in [RequestedAccuracy::exact(), approximate()] { + for accuracy in [AccuracyTarget::exact(), approximate()] { let exact = check_every_choice(sql, accuracy.clone(), ranked, root_names).await; assert!( exact > 0, diff --git a/crates/integration-tests/tests/summary_coverage_e2e.rs b/crates/integration-tests/tests/summary_coverage_e2e.rs index c81c9a70b..8023c7c93 100644 --- a/crates/integration-tests/tests/summary_coverage_e2e.rs +++ b/crates/integration-tests/tests/summary_coverage_e2e.rs @@ -17,10 +17,10 @@ use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode, Predicate, ScalarExpr}; use asap_types::post_asap::{FieldDataType, SketchAlgorithm}; use asap_types::pre_asap::expr_ir::ScalarValue; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; -fn accuracy() -> RequestedAccuracy { - RequestedAccuracy::epsilon_delta(0.01, 0.01) +fn accuracy() -> AccuracyTarget { + AccuracyTarget::epsilon_delta(0.01, 0.01) } /// The single KLL `SummaryAgg` among the logical candidates for `pre`. diff --git a/crates/integration-tests/tests/time_range.rs b/crates/integration-tests/tests/time_range.rs index 478ddf5d8..9d826e141 100644 --- a/crates/integration-tests/tests/time_range.rs +++ b/crates/integration-tests/tests/time_range.rs @@ -15,10 +15,9 @@ use asap_integration_tests::fixtures::metric_schema; use asap_types::ir::{NonASAPOp, OperatorNode, TimeRangeKind}; use asap_types::pre_asap::{AggIntent, Reduction, Source}; use asap_types::types::AccuracyTarget; -use asap_types::types::RequestedAccuracy; fn lower(q: &str) -> Rc { - lower_promql(q, RequestedAccuracy::exact()) + lower_promql(q, AccuracyTarget::exact()) .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } diff --git a/crates/integration-tests/tests/tumbling_pane_merge.rs b/crates/integration-tests/tests/tumbling_pane_merge.rs index d5e466af3..6f52d653e 100644 --- a/crates/integration-tests/tests/tumbling_pane_merge.rs +++ b/crates/integration-tests/tests/tumbling_pane_merge.rs @@ -24,14 +24,14 @@ use asap_types::ir::{ NonASAPOp, Operator, OperatorNode, QueryRoot, TimingMemo, }; use asap_types::post_asap::Schema; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; const EVALUATION_MS: i64 = 300_000; const PANE_MS: i64 = 60_000; const PANES: i64 = 5; /// The selected single-build plan for a 5-minute per-series PromQL query. -fn single_build(query: &str, accuracy: RequestedAccuracy) -> Rc { +fn single_build(query: &str, accuracy: AccuracyTarget) -> Rc { let root = lower_promql(query, accuracy.clone()).unwrap(); let root = promql_rows::with_series_identity(&root).unwrap(); selected_dag(root, accuracy) @@ -343,7 +343,7 @@ fn values(rows: &[Vec]) -> Vec { } /// Returns the per-series values, which every execution path agrees on. -fn assert_pane_merge_matches_single_build(query: &str, accuracy: RequestedAccuracy) -> Vec { +fn assert_pane_merge_matches_single_build(query: &str, accuracy: AccuracyTarget) -> Vec { let single = single_build(query, accuracy); let (panes, pane_builds) = tumbling_panes(&single); let expected = execute(&single); @@ -377,7 +377,7 @@ fn assert_pane_merge_matches_single_build(query: &str, accuracy: RequestedAccura #[test] fn exact_sum_over_tumbling_panes_matches_single_build() { let sums = - assert_pane_merge_matches_single_build("sum_over_time(m[5m])", RequestedAccuracy::exact()); + assert_pane_merge_matches_single_build("sum_over_time(m[5m])", AccuracyTarget::exact()); assert_eq!(sums, vec![210., 2210.]); } @@ -387,7 +387,7 @@ fn exact_sum_over_tumbling_panes_matches_single_build() { fn quantile_over_tumbling_panes_matches_single_build() { let p99 = assert_pane_merge_matches_single_build( "quantile_over_time(0.99, m[5m])", - RequestedAccuracy::epsilon(0.05).with_metric(ErrorMetric::Relative), + AccuracyTarget::epsilon(0.05).with_metric(ErrorMetric::Relative), ); let mut p99 = p99; p99.sort_by(f64::total_cmp); @@ -405,7 +405,7 @@ fn quantile_over_tumbling_panes_matches_single_build() { /// by one pane's minute. #[test] fn exact_rate_over_tumbling_panes_matches_single_build() { - let rates = assert_pane_merge_matches_single_build("rate(m[5m])", RequestedAccuracy::exact()); + let rates = assert_pane_merge_matches_single_build("rate(m[5m])", AccuracyTarget::exact()); assert_eq!(rates.len(), 2); // 19 counted over 285 s of samples, extrapolated to the 5-min window. for rate in rates { @@ -420,14 +420,14 @@ fn exact_rate_over_tumbling_panes_matches_single_build() { fn exact_increase_off_the_pane_boundaries_matches_single_build() { PHASE_MS.with(|p| p.set(7_000)); let increases = - assert_pane_merge_matches_single_build("increase(m[5m])", RequestedAccuracy::exact()); + assert_pane_merge_matches_single_build("increase(m[5m])", AccuracyTarget::exact()); assert_eq!(increases.len(), 2); } /// The single build of `query`, evaluated at `evaluation_ms` over every /// sample, as values. fn whole_build_values_at(query: &str, evaluation_ms: i64) -> Vec { - let root = single_build(query, RequestedAccuracy::exact()); + let root = single_build(query, AccuracyTarget::exact()); let exported = export(&root, &MaterializationAssignment::default()); let inputs: BTreeMap = exported .ranges @@ -484,6 +484,6 @@ fn offset_counter_window_ends_at_the_offset() { #[test] fn exact_increase_over_tumbling_panes_matches_single_build() { let increases = - assert_pane_merge_matches_single_build("increase(m[5m])", RequestedAccuracy::exact()); + assert_pane_merge_matches_single_build("increase(m[5m])", AccuracyTarget::exact()); assert_eq!(increases.len(), 2); } diff --git a/crates/planner/tests/e2e_plan.rs b/crates/planner/tests/e2e_plan.rs index dd39f9922..83e0479d8 100644 --- a/crates/planner/tests/e2e_plan.rs +++ b/crates/planner/tests/e2e_plan.rs @@ -10,7 +10,7 @@ use asap_aware_mapping::pass::{ use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_planner::{e2e_plan, FrontendInput, PlanError, UserInput, UserInputError}; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataArrival, DataWorkload, DurationMs, Evidence, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, @@ -21,7 +21,7 @@ const NOW_MS: u64 = 1_700_000_000_000; fn approximate() -> QueryRequirements { QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon(0.01)), + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::epsilon(0.01)), response_latency: LatencyRequirement::Unspecified, } } @@ -32,7 +32,7 @@ fn approximate_for(sql: &str) -> QueryRequirements { let mut requirements = approximate(); if sql.contains("percentile") { requirements.accuracy = AccuracyRequirement::Explicit( - RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Rank), + AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Rank), ); } requirements diff --git a/crates/planner/tests/plan_stages.rs b/crates/planner/tests/plan_stages.rs index 8f409b8c3..9194ae86d 100644 --- a/crates/planner/tests/plan_stages.rs +++ b/crates/planner/tests/plan_stages.rs @@ -3,7 +3,7 @@ use asap_aware_mapping::pass::{run_stage1_to_stage3, PlanningModels, Stage1ToStage3Run}; use asap_frontend_promql::lower_promql_query_workload; -use asap_types::types::RequestedAccuracy; +use asap_types::types::AccuracyTarget; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Query, QueryLanguage, QueryRequirements, QueryWorkload, @@ -12,7 +12,7 @@ use asap_types::workload::{ const NOW_MS: u64 = 1_700_000_000_000; fn promql_stages(query: &str, epsilon: f64) -> Stage1ToStage3Run { - let target = RequestedAccuracy::epsilon(epsilon); + let target = AccuracyTarget::epsilon(epsilon); let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, diff --git a/crates/planner/tests/summary_sharing.rs b/crates/planner/tests/summary_sharing.rs index 696529eed..0ac116360 100644 --- a/crates/planner/tests/summary_sharing.rs +++ b/crates/planner/tests/summary_sharing.rs @@ -17,7 +17,7 @@ use asap_planner::{e2e_plan, FrontendInput, UserInput}; use asap_types::post_asap::{FieldDataType, SketchAlgorithm, SketchParams}; use asap_types::pre_asap::agg_intent::default_quantile; use asap_types::pre_asap::schema::{DataType, Field, Schema}; -use asap_types::types::{ErrorMetric, RequestedAccuracy}; +use asap_types::types::{AccuracyTarget, ErrorMetric}; use asap_types::workload::{ AccuracyRequirement, DataArrival, DataWorkload, DurationMs, Evidence, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, Rate, @@ -28,8 +28,8 @@ const NOW_MS: u64 = 1_700_000_000_000; /// `epsilon` as a query's requested accuracy; a quantile query's target /// names its metric (a rank error). -fn requested(query: &str, epsilon: f64) -> RequestedAccuracy { - let requested = RequestedAccuracy::epsilon(epsilon); +fn requested(query: &str, epsilon: f64) -> AccuracyTarget { + let requested = AccuracyTarget::epsilon(epsilon); if query.contains("quantile") { requested.with_metric(ErrorMetric::Rank) } else { diff --git a/crates/types/src/pre_asap/agg_intent.rs b/crates/types/src/pre_asap/agg_intent.rs index b9f1252e7..7d0f66053 100644 --- a/crates/types/src/pre_asap/agg_intent.rs +++ b/crates/types/src/pre_asap/agg_intent.rs @@ -929,7 +929,7 @@ mod tests { /// with no `col` key — still deserializes, as the sample-value convention `None`. #[test] fn agg_intent_serde_reads_pre_115_payloads() { - let legacy = r#"{"kind":"quantile","q":0.99,"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; + let legacy = r#"{"kind":"quantile","q":0.99,"accuracy":{"error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; assert_eq!( serde_json::from_str::(legacy).unwrap(), AggIntent::Quantile { @@ -939,7 +939,7 @@ mod tests { } ); - let legacy = r#"{"kind":"top_k","k":5,"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; + let legacy = r#"{"kind":"top_k","k":5,"accuracy":{"error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; assert_eq!( serde_json::from_str::(legacy).unwrap(), AggIntent::TopK { @@ -948,7 +948,7 @@ mod tests { } ); - let legacy = r#"{"kind":"cardinality","accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; + let legacy = r#"{"kind":"cardinality","accuracy":{"error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; assert_eq!( serde_json::from_str::(legacy).unwrap(), AggIntent::Cardinality { diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index 09fa74f89..a6ced39a5 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -2,11 +2,11 @@ //! //! A requirement and a guarantee share one outer shape, [`AccuracySpec`]: //! an error metric, an error ε, a failure probability δ and a scope. A -//! requirement ([`AccuracyTarget`]) holds concrete numbers; a guarantee -//! ([`crate::post_asap::AccuracyGuarantee`]) holds symbolic expressions that -//! plan selection binds. Workloads and the query-language frontends take a -//! [`RequestedAccuracy`], whose metric may be omitted; it is resolved to an -//! [`AccuracyTarget`] for each aggregate intent and for each query root. +//! requirement ([`AccuracyTarget`]) holds concrete numbers and may leave its +//! metric unset; a guarantee ([`crate::post_asap::AccuracyGuarantee`]) +//! always states its metric and holds symbolic expressions that plan +//! selection binds. A target without a metric accepts a guarantee in any +//! metric that meets its ε, δ and scope. use serde::{Deserialize, Serialize}; @@ -115,9 +115,11 @@ pub enum AccuracyScope { WholeResult, } -/// The outer shape an accuracy target and an accuracy guarantee share: +/// The outer shape of an accuracy guarantee +/// ([`crate::post_asap::AccuracyGuarantee`]): /// `Pr[err_metric(estimate, truth) > error] ≤ failure_probability` over the -/// values `scope` names. +/// values `scope` names. An [`AccuracyTarget`] has the same fields, with +/// concrete numbers and an optional metric. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AccuracySpec { @@ -129,141 +131,41 @@ pub struct AccuracySpec { pub scope: AccuracyScope, } -/// A concrete accuracy requirement. `failure_probability: None` imposes no -/// δ requirement (sizing then uses the planner's default δ). Exact is -/// `error = 0`, `failure_probability = Some(0)`, whatever the metric. -pub type AccuracyTarget = AccuracySpec>; - -impl AccuracySpec> { - /// No approximation permitted. - pub const fn exact() -> Self { - Self { - metric: ErrorMetric::ABSOLUTE, - error: 0.0, - failure_probability: Some(0.0), - scope: AccuracyScope::WholeResult, - } - } - - /// Error at most `epsilon` in absolute value units, with no δ - /// requirement. [`Self::with_metric`] states another metric. - pub const fn epsilon(epsilon: f64) -> Self { - Self { - metric: ErrorMetric::ABSOLUTE, - error: epsilon, - failure_probability: None, - scope: AccuracyScope::WholeResult, - } - } - - /// Error at most `epsilon` in absolute value units with probability at - /// least `1 − delta`. [`Self::with_metric`] states another metric. - pub const fn epsilon_delta(epsilon: f64, delta: f64) -> Self { - Self { - failure_probability: Some(delta), - ..Self::epsilon(epsilon) - } - } - - /// This target stated in `metric`. - pub fn with_metric(self, metric: ErrorMetric) -> Self { - Self { metric, ..self } - } - - /// This target over `scope`. - pub fn with_scope(self, scope: AccuracyScope) -> Self { - Self { scope, ..self } - } - - /// Whether this target permits no approximation. - pub fn is_exact(&self) -> bool { - self.error == 0.0 && self.failure_probability == Some(0.0) - } -} - -/// An accuracy requirement as a workload or a caller states it: the -/// metric may be omitted. [`Self::for_root`] resolves it for a query root -/// (the target plan selection checks); [`Self::target_in`] states it in an -/// aggregate intent's own metric (the target a summary is sized against). +/// An accuracy requirement, as a workload, a caller or a frontend states +/// it: `Pr[err_metric(estimate, truth) > error] ≤ failure_probability` over +/// the values `scope` names. /// -/// Reads the workload forms `"Exact"`, `{"Epsilon": ε}` and -/// `{"EpsilonDelta": {"epsilon": ε, "delta": δ}}`, as well as -/// `{"metric": …, "error": ε, "failure_probability": δ, "scope": …}`, in -/// which every field but `error` is optional. +/// `metric: None` means the user did not set one: plan selection then +/// accepts a guarantee in any metric whose ε, δ and scope meet this +/// target's, and reports the metric it accepted. A set metric accepts only +/// guarantees in that metric (or convertible to it with evidence). +/// `failure_probability: None` imposes no δ requirement (sizing then uses +/// the planner's default δ). Exact is `error = 0`, +/// `failure_probability = Some(0)`, whatever the metric. +/// +/// JSON: `{"metric": "rank", "error": 0.01, "failure_probability": 0.001, +/// "scope": "whole_result"}`; every field but `error` may be omitted +/// (`scope` defaults to `whole_result`). Exact is +/// `{"error": 0.0, "failure_probability": 0.0}`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(from = "RequestedAccuracyWire")] -pub struct RequestedAccuracy { - #[serde(skip_serializing_if = "Option::is_none")] +#[serde(deny_unknown_fields)] +pub struct AccuracyTarget { + #[serde(default, skip_serializing_if = "Option::is_none")] pub metric: Option, + /// ε, in the metric's units. pub error: f64, - #[serde(skip_serializing_if = "Option::is_none")] + /// δ. + #[serde(default, skip_serializing_if = "Option::is_none")] pub failure_probability: Option, - pub scope: AccuracyScope, -} - -#[derive(Deserialize)] -#[serde(untagged)] -enum RequestedAccuracyWire { - Named(NamedTarget), - Spec(SpecWire), -} - -/// The workload forms that name no metric. -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -enum NamedTarget { - Epsilon(f64), - EpsilonDelta { epsilon: f64, delta: f64 }, - Exact, -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct SpecWire { - #[serde(default)] - metric: Option, - error: f64, - #[serde(default)] - failure_probability: Option, #[serde(default = "whole_result")] - scope: AccuracyScope, + pub scope: AccuracyScope, } fn whole_result() -> AccuracyScope { AccuracyScope::WholeResult } -impl From for RequestedAccuracy { - fn from(wire: RequestedAccuracyWire) -> Self { - match wire { - RequestedAccuracyWire::Named(NamedTarget::Exact) => Self::exact(), - RequestedAccuracyWire::Named(NamedTarget::Epsilon(epsilon)) => Self::epsilon(epsilon), - RequestedAccuracyWire::Named(NamedTarget::EpsilonDelta { epsilon, delta }) => { - Self::epsilon_delta(epsilon, delta) - } - RequestedAccuracyWire::Spec(spec) => Self { - metric: spec.metric, - error: spec.error, - failure_probability: spec.failure_probability, - scope: spec.scope, - }, - } - } -} - -/// A requested accuracy that cannot be resolved for a query root. -#[derive(Debug, Clone, PartialEq, thiserror::Error)] -pub enum AccuracyRequestError { - /// A quantile's error may be a rank or a value error, and which one is - /// the user's choice: the planner never guesses it. - #[error( - "the accuracy target of a {intent} query must name its metric \ - (\"rank\", \"relative\" or \"absolute\")" - )] - MissingQuantileMetric { intent: &'static str }, -} - -impl RequestedAccuracy { +impl AccuracyTarget { /// No approximation permitted. pub const fn exact() -> Self { Self { @@ -274,8 +176,8 @@ impl RequestedAccuracy { } } - /// Error at most `epsilon` in the query's default metric, with no δ - /// requirement. + /// Error at most `epsilon`, with no δ requirement and no metric set. + /// [`Self::with_metric`] sets one. pub const fn epsilon(epsilon: f64) -> Self { Self { metric: None, @@ -285,8 +187,8 @@ impl RequestedAccuracy { } } - /// Error at most `epsilon` in the query's default metric with - /// probability at least `1 − delta`. + /// Error at most `epsilon` with probability at least `1 − delta`, and + /// no metric set. [`Self::with_metric`] sets one. pub const fn epsilon_delta(epsilon: f64, delta: f64) -> Self { Self { failure_probability: Some(delta), @@ -294,7 +196,7 @@ impl RequestedAccuracy { } } - /// This request stated in `metric`. + /// This target with its metric set to `metric`. pub fn with_metric(self, metric: ErrorMetric) -> Self { Self { metric: Some(metric), @@ -302,56 +204,56 @@ impl RequestedAccuracy { } } - /// Whether this request permits no approximation. + /// This target over `scope`. + pub fn with_scope(self, scope: AccuracyScope) -> Self { + Self { scope, ..self } + } + + /// Whether this target permits no approximation. pub fn is_exact(&self) -> bool { self.error == 0.0 && self.failure_probability == Some(0.0) } - /// This request's ε, δ and scope, stated in `metric`. An exact request - /// is [`AccuracyTarget::exact`] whatever `metric`: exact is exact in + /// This target's ε, δ and scope, stated in `metric`: the target an + /// aggregate intent's summary is sized against, in the intent's own + /// metric. An exact target stays [`Self::exact`]: exact is exact in /// every metric. - pub fn target_in(&self, metric: ErrorMetric) -> AccuracyTarget { + pub fn target_in(&self, metric: ErrorMetric) -> Self { if self.is_exact() { - return AccuracyTarget::exact().with_scope(self.scope); - } - AccuracyTarget { - metric, - error: self.error, - failure_probability: self.failure_probability, - scope: self.scope, + return Self::exact().with_scope(self.scope); } + self.clone().with_metric(metric) } - /// The target a quantile intent is sized against: the requested metric, - /// or `Rank` when none is named. A summary is sized in its own native - /// terms (#690 §4), so only ε, δ and the scope matter there; a quantile - /// root's metric must be named ([`Self::for_root`]). + /// The target a quantile intent is sized against: the set metric, or + /// `Rank` when none is set. A summary is sized in its own native terms + /// (#690 §4), so only ε, δ and the scope matter there; the query's own + /// metric, set or not, is checked at its root. /// /// The other intents with a target are stated in their own metric with /// [`Self::target_in`]: an L1-scaled absolute error for counts and top-k /// frequencies, a relative error for distinct counts, and an absolute - /// value error for frequency moments. The query's own metric is checked - /// at its root. - pub fn for_quantile(&self) -> AccuracyTarget { + /// value error for frequency moments. + pub fn for_quantile(&self) -> Self { self.target_in(self.metric.unwrap_or(ErrorMetric::Rank)) } +} - /// The target plan selection checks at `root`: the requested metric, or - /// the one derived from the root's computation — an L1-scaled absolute - /// error for counts and top-k frequencies, a relative error for - /// distinct counts and L2 frequency moments, and an absolute value - /// error for anything else (sums, arithmetic, rates, averages, extrema, - /// entropy). A - /// quantile root has no default: its request must name the metric. An - /// exact request needs none. - pub fn for_root(&self, root: &QueryRoot) -> Result { - if let Some(metric) = self.metric { - return Ok(self.target_in(metric)); - } - if self.is_exact() { - return Ok(self.target_in(ErrorMetric::ABSOLUTE)); - } - root_metric(root).map(|metric| self.target_in(metric)) +/// The metric a query's result is most naturally stated in, from the +/// computation at `root`: an L1-scaled absolute error for counts and top-k +/// frequencies, a relative error for distinct counts and L2 frequency +/// moments, and an absolute value error for anything else (sums, +/// arithmetic, rates, averages, extrema, entropy). `None` for a quantile +/// root, whose error is as naturally a rank as a relative one. +/// +/// Used only to size and to display: a target whose metric is not set +/// accepts a guarantee in any metric (#690). +pub fn default_metric_for_root(root: &QueryRoot) -> Option { + match root { + QueryRoot::Operator(node) => node_metric(node), + QueryRoot::Scalar(ScalarExpr::PromqlScalarFromVector(node)) + | QueryRoot::Scalar(ScalarExpr::ScalarSubquery(node)) => node_metric(node), + QueryRoot::Scalar(_) => Some(ErrorMetric::ABSOLUTE), } } @@ -367,20 +269,11 @@ fn intent_metric(intent: &AggIntent) -> Option { } } -fn root_metric(root: &QueryRoot) -> Result { - match root { - QueryRoot::Operator(node) => node_metric(node), - QueryRoot::Scalar(ScalarExpr::PromqlScalarFromVector(node)) - | QueryRoot::Scalar(ScalarExpr::ScalarSubquery(node)) => node_metric(node), - QueryRoot::Scalar(_) => Ok(ErrorMetric::ABSOLUTE), - } -} - /// The metric of `node`'s values, looking through operators that only /// select, order or rename rows. -fn node_metric(node: &OperatorNode) -> Result { +fn node_metric(node: &OperatorNode) -> Option { let Operator::NonASAP(op) = &node.operator else { - return Ok(ErrorMetric::ABSOLUTE); + return Some(ErrorMetric::ABSOLUTE); }; match op { NonASAPOp::Filter { child, .. } @@ -399,7 +292,7 @@ fn node_metric(node: &OperatorNode) -> Result // count: the metric of the columns its value branches read. NonASAPOp::Project { .. } => (0..node.schema.fields.len()) .map(|column| column_metric(node, column)) - .collect::, _>>() + .collect::>>() .map(|metrics| match metrics.split_first() { Some((first, rest)) if rest.iter().all(|metric| metric == first) => *first, _ => ErrorMetric::ABSOLUTE, @@ -408,27 +301,20 @@ fn node_metric(node: &OperatorNode) -> Result let mut metrics = Vec::new(); for intent in measures { match intent { - AggIntent::Quantile { .. } => { - return Err(AccuracyRequestError::MissingQuantileMetric { - intent: "quantile", - }) - } - AggIntent::HistogramQuantile { .. } => { - return Err(AccuracyRequestError::MissingQuantileMetric { - intent: "histogram_quantile", - }) + AggIntent::Quantile { .. } | AggIntent::HistogramQuantile { .. } => { + return None } intent => metrics.push(intent_metric(intent).unwrap_or(ErrorMetric::ABSOLUTE)), } } // Measures with different default metrics share one value-unit // statement. - Ok(match metrics.split_first() { + Some(match metrics.split_first() { Some((first, rest)) if rest.iter().all(|metric| metric == first) => *first, _ => ErrorMetric::ABSOLUTE, }) } - _ => Ok(ErrorMetric::ABSOLUTE), + _ => Some(ErrorMetric::ABSOLUTE), } } @@ -436,10 +322,10 @@ fn node_metric(node: &OperatorNode) -> Result /// projections, `CASE` value branches, joins and operators that only /// select, order or rename rows, down to an aggregate's measure. A group /// key or any other expression is absolute. -fn column_metric(node: &OperatorNode, column: usize) -> Result { +fn column_metric(node: &OperatorNode, column: usize) -> Option { use crate::ir::operator_properties::JoinKind; let Operator::NonASAP(op) = &node.operator else { - return Ok(ErrorMetric::ABSOLUTE); + return Some(ErrorMetric::ABSOLUTE); }; match op { NonASAPOp::Filter { child, .. } @@ -465,15 +351,15 @@ fn column_metric(node: &OperatorNode, column: usize) -> Result {} ScalarExpr::Column(id) => metrics.push(column_metric(child, *id)?), - _ => return Ok(ErrorMetric::ABSOLUTE), + _ => return Some(ErrorMetric::ABSOLUTE), } } - Ok(match metrics.split_first() { + Some(match metrics.split_first() { Some((first, rest)) if rest.iter().all(|metric| metric == first) => *first, _ => ErrorMetric::ABSOLUTE, }) } - _ => Ok(ErrorMetric::ABSOLUTE), + _ => Some(ErrorMetric::ABSOLUTE), }, NonASAPOp::Join { kind: @@ -493,19 +379,12 @@ fn column_metric(node: &OperatorNode, column: usize) -> Result { - Err(AccuracyRequestError::MissingQuantileMetric { intent: "quantile" }) - } - Some(AggIntent::HistogramQuantile { .. }) => { - Err(AccuracyRequestError::MissingQuantileMetric { - intent: "histogram_quantile", - }) - } - Some(intent) => Ok(intent_metric(intent).unwrap_or(ErrorMetric::ABSOLUTE)), - None => Ok(ErrorMetric::ABSOLUTE), + Some(AggIntent::Quantile { .. } | AggIntent::HistogramQuantile { .. }) => None, + Some(intent) => Some(intent_metric(intent).unwrap_or(ErrorMetric::ABSOLUTE)), + None => Some(ErrorMetric::ABSOLUTE), } } - _ => Ok(ErrorMetric::ABSOLUTE), + _ => Some(ErrorMetric::ABSOLUTE), } } @@ -551,40 +430,45 @@ mod tests { } } - /// The old workload forms still read, and name no metric; the spec - /// form may name one. + /// A target reads and writes as a plain struct: every field but + /// `error` may be omitted (no metric, no δ, the whole result), an unset + /// metric or δ writes nothing, and the old named forms (`"Exact"`, + /// `{"Epsilon": ε}`, `{"EpsilonDelta": …}`) and unknown fields are + /// rejected. #[test] - fn requested_accuracy_reads_old_and_spec_forms() { - let read = |value: serde_json::Value| -> RequestedAccuracy { - serde_json::from_value(value).unwrap() - }; - assert_eq!(read(serde_json::json!("Exact")), RequestedAccuracy::exact()); + fn accuracy_target_json_is_a_plain_struct() { + use serde_json::json; + let read = |value: serde_json::Value| serde_json::from_value::(value); assert_eq!( - read(serde_json::json!({"Epsilon": 0.01})), - RequestedAccuracy::epsilon(0.01) + read(json!({"error": 0.01})).unwrap(), + AccuracyTarget::epsilon(0.01) ); assert_eq!( - read(serde_json::json!({"EpsilonDelta": {"epsilon": 0.01, "delta": 0.001}})), - RequestedAccuracy::epsilon_delta(0.01, 0.001) + read(json!({"error": 0.0, "failure_probability": 0.0})).unwrap(), + AccuracyTarget::exact() ); - let rank = RequestedAccuracy::epsilon_delta(0.01, 0.001).with_metric(ErrorMetric::Rank); + let rank = AccuracyTarget::epsilon_delta(0.01, 0.001).with_metric(ErrorMetric::Rank); assert_eq!( - read( - serde_json::json!({"metric": "rank", "error": 0.01, "failure_probability": 0.001}) - ), + read(json!({"metric": "rank", "error": 0.01, "failure_probability": 0.001})).unwrap(), rank ); - assert_eq!(read(serde_json::to_value(&rank).unwrap()), rank); - assert!( - serde_json::from_value::(serde_json::json!({ - "EpsilonDelta": {"epsilon": 0.1, "delta": 0.01, "typo": true} - })) - .is_err() + assert_eq!( + serde_json::to_value(&rank).unwrap(), + json!({"metric": "rank", "error": 0.01, "failure_probability": 0.001, + "scope": "whole_result"}) ); - assert!(serde_json::from_value::( - serde_json::json!({"error": 0.1, "typo": true}) - ) - .is_err()); + assert_eq!( + serde_json::to_value(AccuracyTarget::epsilon(0.01)).unwrap(), + json!({"error": 0.01, "scope": "whole_result"}) + ); + for old in [ + json!("Exact"), + json!({"Epsilon": 0.01}), + json!({"EpsilonDelta": {"epsilon": 0.01, "delta": 0.001}}), + json!({"error": 0.1, "typo": true}), + ] { + assert!(read(old.clone()).is_err(), "{old}"); + } } /// Metrics have stable wire names. @@ -611,8 +495,7 @@ mod tests { /// among them) absolute value errors. #[test] fn root_metric_defaults_follow_the_query_intent() { - let request = RequestedAccuracy::epsilon(0.01); - let metric = |intent: AggIntent| request.for_root(&aggregate(vec![intent])).unwrap().metric; + let metric = |intent: AggIntent| default_metric_for_root(&aggregate(vec![intent])).unwrap(); let exact = AccuracyTarget::exact(); assert_eq!( metric(AggIntent::Count { @@ -651,11 +534,8 @@ mod tests { ); assert_eq!(metric(AggIntent::Avg { col: None }), ErrorMetric::ABSOLUTE); assert_eq!( - request - .for_root(&QueryRoot::Scalar(ScalarExpr::literal_f64(1.0))) - .unwrap() - .metric, - ErrorMetric::ABSOLUTE + default_metric_for_root(&QueryRoot::Scalar(ScalarExpr::literal_f64(1.0))), + Some(ErrorMetric::ABSOLUTE) ); } @@ -712,12 +592,7 @@ mod tests { child: joined.clone(), })) }; - let metric = |root: QueryRoot| { - RequestedAccuracy::epsilon(0.01) - .for_root(&root) - .unwrap() - .metric - }; + let metric = |root: QueryRoot| default_metric_for_root(&root).unwrap(); assert_eq!(metric(guard(ScalarExpr::Column(0))), ErrorMetric::Relative); // The count's column, or arithmetic over the statistic. assert_eq!( @@ -733,27 +608,12 @@ mod tests { ); } - /// A quantile root's request must name its metric; a named metric or an - /// exact request needs no default. + /// A quantile root has no default metric: its error is as naturally a + /// rank as a relative one, and a target that sets none accepts either. + /// It is not an input error. #[test] - fn a_quantile_root_must_name_its_metric() { - let root = aggregate(vec![quantile()]); - assert_eq!( - RequestedAccuracy::epsilon(0.01).for_root(&root), - Err(AccuracyRequestError::MissingQuantileMetric { intent: "quantile" }) - ); - assert_eq!( - RequestedAccuracy::epsilon(0.01) - .with_metric(ErrorMetric::Relative) - .for_root(&root) - .unwrap() - .metric, - ErrorMetric::Relative - ); - assert!(RequestedAccuracy::exact() - .for_root(&root) - .unwrap() - .is_exact()); + fn a_quantile_root_has_no_default_metric() { + assert_eq!(default_metric_for_root(&aggregate(vec![quantile()])), None); } /// Exact means zero error with certainty, whatever the metric; an @@ -761,6 +621,7 @@ mod tests { #[test] fn exact_targets() { assert!(AccuracyTarget::exact().is_exact()); + assert_eq!(AccuracyTarget::exact().metric, None); assert!(AccuracyTarget::exact() .with_metric(ErrorMetric::Rank) .is_exact()); diff --git a/crates/types/src/workload.rs b/crates/types/src/workload.rs index 98f14caae..8d6ee920c 100644 --- a/crates/types/src/workload.rs +++ b/crates/types/src/workload.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use crate::types::RequestedAccuracy; +use crate::types::AccuracyTarget; use serde::{Deserialize, Serialize}; // ── Query surface ───────────────────────────────────────────────────────────── @@ -58,19 +58,19 @@ pub enum QueryLanguage { /// Whether the caller explicitly requested an accuracy target or inherited /// the normalized exact default. An explicit request may omit its metric -/// ([`RequestedAccuracy`]). +/// ([`AccuracyTarget`]). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AccuracyRequirement { - Explicit(RequestedAccuracy), + Explicit(AccuracyTarget), ImplicitExact, } impl AccuracyRequirement { - pub fn requested(&self) -> RequestedAccuracy { + pub fn requested(&self) -> AccuracyTarget { match self { Self::Explicit(requested) => requested.clone(), - Self::ImplicitExact => RequestedAccuracy::exact(), + Self::ImplicitExact => AccuracyTarget::exact(), } } } @@ -940,7 +940,7 @@ mod tests { query: Query("rate(requests_total[5m])".into()), demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon_delta( + accuracy: AccuracyRequirement::Explicit(AccuracyTarget::epsilon_delta( 0.01, 0.001, )), response_latency: LatencyRequirement::ExplicitMaxMs(100.0), @@ -1012,12 +1012,10 @@ mod tests { "predictable": {"known_at": null, "typo": true} })) .is_err()); - assert!( - serde_json::from_value::(serde_json::json!({ - "EpsilonDelta": {"epsilon": 0.1, "delta": 0.01, "typo": true} - })) - .is_err() - ); + assert!(serde_json::from_value::(serde_json::json!({ + "error": 0.1, "failure_probability": 0.01, "typo": true + })) + .is_err()); } #[test] diff --git a/docs/user_guide_docs/stage-viewer.md b/docs/user_guide_docs/stage-viewer.md index 11ab86a25..295aa0ef2 100644 --- a/docs/user_guide_docs/stage-viewer.md +++ b/docs/user_guide_docs/stage-viewer.md @@ -89,8 +89,11 @@ resource use. One box per query, with its language and text and the requirements the planner must meet: -- **accuracy**: `exact`, or `ε=…, δ=…` (relative error ε with probability at - least 1 − δ); +- **accuracy**: `exact`, or `ε=…, δ=… (metric)` (error ε in that metric + with probability at least 1 − δ); `(metric not set; default …)` when the + query leaves the metric to the planner, which then accepts a plan in any + metric that meets ε and δ (the default is the metric the query's result is + naturally stated in, shown for reference); - **latency**: the maximum time an evaluation may take at query time; - **every N s**: how often a repeating query runs. @@ -124,6 +127,9 @@ it (plus once for each query that also reads them directly). - **✓ selected**: the plan ASAPPlanner chooses. - **valid · costlier**: meets every requirement, but costs more. + +A selected or costlier plan also says, per query, which metric its accuracy +was accepted in, e.g. `q1: metric not set; accepted rank (Kll)`. - **✗ invalid**: fails a check, with the reason. Examples: an accuracy target the sketch cannot guarantee, an evaluation over the latency bound, or a summary the deployment cannot build. diff --git a/tools/dag-viewer/README.md b/tools/dag-viewer/README.md index 9e4a341ba..7fb11c473 100644 --- a/tools/dag-viewer/README.md +++ b/tools/dag-viewer/README.md @@ -62,7 +62,8 @@ time column). The server runs `stage_pipeline` with `--sql`, `--promql`, { "format": "asap-stage-pipeline/v1", "workload": { "queries": [{ "id": "q1", "language": "promql", "text": "...", - "requirements": { "accuracy": "exact", "repeat_interval_ms": 10000 } }] }, + "requirements": { "accuracy": { "error": 0.0, "failure_probability": 0.0, "scope": "whole_result" }, + "repeat_interval_ms": 10000 } }] }, "stage0_query_language_frontends": { "dag": "" }, "stage1_logical_asap_optimization": { "combinations": 9, "capped": false, @@ -76,6 +77,8 @@ time column). The server runs `stage_pipeline` with `--sql`, `--promql`, "stage3_plan_selection": { "costs": { "P1": { "total": 7.9, "unit": "cost_per_second", "source": "...", "per_node": { "": { "cost": 2.3, "multiplier": 1, "detail": "..." } } } }, + "accepted": { "P1": [{ "target_metric": null, "accepted_metric": "rank", "sketches": ["Kll"], + "summary": "metric not set; accepted rank (Kll)" }] }, "selected": "P1", "rejected": [{ "id": "P2", "valid": true, "reason": "..." }] } @@ -88,6 +91,14 @@ time column). The server runs `stage_pipeline` with `--sql`, `--promql`, - A `PhysicalASAPDAG` has node ids, an edge list, operator roots in `roots`, and standalone scalar queries in `scalar_roots` as `[query position, expr]`. +- A query's `requirements.accuracy` is its accuracy target's own JSON + (`metric` omitted when not set, `failure_probability` omitted without a + δ). An approximate target without a metric also has + `requirements.default_metric`, the metric the query's result is naturally + stated in (null for a quantile), for display only. +- `stage3_plan_selection.accepted` has, per shown valid candidate and query, + the metric the target set (null when not set) and the metric Stage 3 + accepted (null for an exact output). - When Pass 1 fails, Stage 1 has `error` and no candidates. When selection fails, `selected` is null, with the rejections or with `error`. diff --git a/tools/dag-viewer/app.js b/tools/dag-viewer/app.js index 99f06be09..2189ea0d7 100644 --- a/tools/dag-viewer/app.js +++ b/tools/dag-viewer/app.js @@ -113,7 +113,7 @@ $('rank').innerHTML = rows.map((r) => `
  • `).join(''); + ${statusChip(r.status)}${r.reason ? `${esc(r.reason)}` : ''}${r.accepted ? `${esc(r.accepted)}` : ''}`).join(''); $('rank').querySelectorAll('button').forEach((b) => b.addEventListener('click', () => { const [kind, id] = [b.dataset.key.slice(0, 1), b.dataset.key.slice(2)]; if (kind === 'P') selectPhysical(id); @@ -161,7 +161,8 @@ const priced = rows.filter((r) => r.rank !== null).length; $('lane2-meta').innerHTML = `${esc(c.display_name)} · from ${esc(c.from_logical)} · ` + (cost ? `total ${fmtCost(cost.total)} ${esc(cost.unit.replace(/_/g, ' '))} · rank ${row.rank} of ${priced}` : 'not priced') + - ` · ${statusChip(row.status)}`; + ` · ${statusChip(row.status)}` + + (row.accepted ? ` · ${esc(row.accepted)}` : ''); if (c.from_logical !== current.logical) showLogical(c.from_logical); renderRanking(); } @@ -190,7 +191,7 @@ $('queries').innerHTML = doc.workload.queries.map((q) => { const r = q.requirements || {}; - const acc = r.accuracy === 'exact' ? 'exact' : r.accuracy ? `ε=${r.accuracy.epsilon}${r.accuracy.delta !== undefined ? `, δ=${r.accuracy.delta}` : ''}${r.accuracy.metric ? ` (${r.accuracy.metric})` : ''}` : ''; + const acc = accuracyText(r.accuracy, r.default_metric); const req = [acc && `accuracy ${acc}`, r.latency_ms && `latency ≤ ${r.latency_ms} ms`, r.repeat_interval_ms && `every ${r.repeat_interval_ms / 1000} s`].filter(Boolean).join(' · '); return `
    ${esc(q.id)} · ${esc(q.language)}${esc(q.text)}${req ? `${esc(req)}` : ''}
    `; }).join(''); diff --git a/tools/dag-viewer/examples/planner-layering-example1.json b/tools/dag-viewer/examples/planner-layering-example1.json index b60f07a76..446a95142 100644 --- a/tools/dag-viewer/examples/planner-layering-example1.json +++ b/tools/dag-viewer/examples/planner-layering-example1.json @@ -59910,6 +59910,134 @@ "pruned": [] }, "stage3_plan_selection": { + "accepted": { + "P1": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P2": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P3": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P4": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P5": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P6": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P7": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P8": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ], + "P9": [ + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + }, + { + "accepted_metric": null, + "sketches": [], + "summary": "metric not set; accepted exact", + "target_metric": null + } + ] + }, "costs": { "P1": { "per_node": { @@ -60533,92 +60661,92 @@ "rejected": [ { "id": "P10", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P11", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P12", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P13", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P14", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P15", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P16", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P17", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P18", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P19", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P20", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P21", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P22", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P23", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P24", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P25", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P26", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { "id": "P27", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracySpec { metric: Absolute { scale: Some(StreamL1) }, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", + "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { metric: None, error: 0.01, failure_probability: Some(0.001), scope: WholeResult }", "valid": false }, { @@ -60670,7 +60798,11 @@ "id": "q1", "language": "promql", "requirements": { - "accuracy": "exact", + "accuracy": { + "error": 0.0, + "failure_probability": 0.0, + "scope": "whole_result" + }, "repeat_interval_ms": 10000 }, "text": "sum by (job) (rate(http_requests_total[1m]))" @@ -60680,9 +60812,11 @@ "language": "promql", "requirements": { "accuracy": { - "delta": 0.001, - "epsilon": 0.01 + "error": 0.01, + "failure_probability": 0.001, + "scope": "whole_result" }, + "default_metric": "absolute_stream_l1", "latency_ms": 100.0, "repeat_interval_ms": 10000 }, diff --git a/tools/dag-viewer/index.html b/tools/dag-viewer/index.html index 365fac965..1ceb4656f 100644 --- a/tools/dag-viewer/index.html +++ b/tools/dag-viewer/index.html @@ -58,6 +58,7 @@ .query code { font-family: var(--font-mono); font-size: 12.5px; overflow-wrap: anywhere; } .qid { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; font-size: 12px; color: var(--accent); } .req { color: var(--muted); font-size: 12px; } +.accepted { color: var(--muted); font-size: 12px; } .stages { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; color: var(--muted); font-size: 12.5px; } .stages b { color: var(--fg); font-family: var(--font-mono); font-weight: 500; } .main { display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr); gap: 16px; align-items: start; } diff --git a/tools/dag-viewer/server.py b/tools/dag-viewer/server.py index 8519e313a..c6d358182 100644 --- a/tools/dag-viewer/server.py +++ b/tools/dag-viewer/server.py @@ -127,8 +127,8 @@ def plan_args(payload: object, out: str) -> list[str]: if not 0 < epsilon < 1: raise ValueError("epsilon must be between 0 and 1") args.extend(["--epsilon", str(epsilon)]) - # The target's error metric; empty means the query's default metric. A - # quantile query's target must name one. + # The target's error metric; empty leaves it unset, and Stage 3 then + # accepts a guarantee in any metric and reports the one it accepted. metric = payload.get("metric") if metric not in (None, ""): if metric not in ("absolute", "relative", "rank"): diff --git a/tools/dag-viewer/stages.js b/tools/dag-viewer/stages.js index 7caa80b3e..6c7962e9b 100644 --- a/tools/dag-viewer/stages.js +++ b/tools/dag-viewer/stages.js @@ -249,6 +249,29 @@ function validateStagePipeline(doc) { return validateSelection(stage3, physicalById); } +// A query's accuracy target (`AccuracyTarget` JSON: `metric`, `error`, +// `failure_probability`, `scope`) as one line, e.g. "ε=0.01, δ=0.001 +// (rank)" or "ε=0.01 (metric not set; default relative)"; "exact" for +// zero error with certainty; "" without a target. `defaultMetric` is the +// query's default metric the document shows for a target without one. +function accuracyText(target, defaultMetric) { + if (!target || typeof target !== 'object') return ''; + if (target.error === 0 && target.failure_probability === 0) return 'exact'; + const delta = target.failure_probability !== undefined && target.failure_probability !== null + ? `, δ=${target.failure_probability}` : ''; + const metric = target.metric || `metric not set${defaultMetric ? `; default ${defaultMetric}` : ''}`; + return `ε=${target.error}${delta} (${metric})`; +} + +// What Stage 3 accepted for a valid candidate's queries (`accepted[id]`: +// one entry per query, null without a target), as "q1: …; q2: …"; null +// when the document has none. +function acceptedText(queries) { + if (!Array.isArray(queries)) return null; + const parts = queries.map((q, index) => (q && q.summary ? `q${index + 1}: ${q.summary}` : null)).filter(Boolean); + return parts.length ? parts.join('; ') : null; +} + // Stage 3's view of every candidate the document shows: the selected one, // the other valid ones cheapest first ("costlier"), the rejected ones with // their reasons ("not judged" when selection failed with an error), then @@ -259,6 +282,7 @@ function stageCandidateRows(doc) { const selection = doc.stage3_plan_selection || {}; const costs = selection.costs || {}; const rejected = new Map((selection.rejected || []).map((r) => [r.id, r])); + const accepted = selection.accepted || {}; const physical = doc.stage2_physical_asap_optimization || {}; const rows = (physical.candidates || []).map((candidate) => { const cost = costs[candidate.id]; @@ -275,6 +299,7 @@ function stageCandidateRows(doc) { rank: null, status, reason: status === 'rejected' && rejection ? rejection.reason : null, + accepted: acceptedText(accepted[candidate.id]), }; }); (physical.pruned || []).forEach((p) => rows.push({ diff --git a/tools/dag-viewer/test_server.py b/tools/dag-viewer/test_server.py index a2fa8a2a9..a890dae51 100644 --- a/tools/dag-viewer/test_server.py +++ b/tools/dag-viewer/test_server.py @@ -245,17 +245,20 @@ def test_plan_endpoint_returns_the_stage_pipeline_document(self): self.assertEqual(document["workload"]["queries"][0]["id"], "job_rate") self.assertIsNotNone(document["stage3_plan_selection"]["selected"]) - def test_a_quantile_query_plans_with_the_metric_it_names(self): - """A quantile target must name its metric: with one, a plan is - selected; without one, plan selection reports the missing metric.""" + def test_a_quantile_query_plans_with_or_without_a_metric(self): + """A quantile target may set its metric or leave it unset: either + way a plan is selected, and Stage 3 reports the metric it accepted.""" query = {"name": "p99", "language": "promql", "text": "quantile_over_time(0.99, m[1m])"} named = post_plan({"queries": [query], "epsilon": 0.01, "metric": "rank"}) self.assertEqual(named["workload"]["queries"][0]["requirements"]["accuracy"]["metric"], "rank") - self.assertIsNotNone(named["stage3_plan_selection"]["selected"]) + selected = named["stage3_plan_selection"]["selected"] + self.assertIsNotNone(selected) + self.assertTrue(named["stage3_plan_selection"]["accepted"][selected][0]["summary"].startswith("metric rank; accepted ")) unnamed = post_plan({"queries": [query], "epsilon": 0.01}) - self.assertIsNone(unnamed["stage3_plan_selection"]["selected"]) - self.assertIn("must name its metric", unnamed["stage3_plan_selection"]["error"]) - + self.assertNotIn("metric", unnamed["workload"]["queries"][0]["requirements"]["accuracy"]) + selected = unnamed["stage3_plan_selection"]["selected"] + self.assertIsNotNone(selected) + self.assertTrue(unnamed["stage3_plan_selection"]["accepted"][selected][0]["summary"].startswith("metric not set; accepted ")) if __name__ == "__main__": unittest.main() diff --git a/tools/dag-viewer/test_viewer.py b/tools/dag-viewer/test_viewer.py index b77801cd2..47b995aed 100644 --- a/tools/dag-viewer/test_viewer.py +++ b/tools/dag-viewer/test_viewer.py @@ -180,6 +180,24 @@ def test_failed_selection_leaves_candidates_not_judged(self): self.assertEqual({row["status"] for row in rows}, {"not judged"}) self.assertTrue(all(row["reason"] is None for row in rows)) + def test_valid_rows_show_the_accepted_metric(self): + """Every valid candidate's row says, per query, which metric Stage 3 + accepted and whether the target set one; invalid rows say nothing.""" + rows = self.js.call("stageCandidateRows", self.doc) + for row in rows: + if row["status"] in ("selected", "costlier"): + self.assertEqual(row["accepted"], "q1: metric not set; accepted exact; q2: metric not set; accepted exact") + else: + self.assertIsNone(row["accepted"]) + + def test_accuracy_text_reads_the_target_json(self): + text = lambda target: self.js.call("accuracyText", target) + self.assertEqual(text({"error": 0.0, "failure_probability": 0.0, "scope": "whole_result"}), "exact") + self.assertEqual(text({"error": 0.01, "scope": "whole_result"}), "ε=0.01 (metric not set)") + self.assertEqual(text({"metric": "rank", "error": 0.01, "failure_probability": 0.001, "scope": "whole_result"}), "ε=0.01, δ=0.001 (rank)") + self.assertEqual(self.js.call("accuracyText", {"error": 0.01}, "relative"), "ε=0.01 (metric not set; default relative)") + self.assertEqual(text(None), "") + def test_header_counts_candidates_and_shows_a_selection_error(self): combinations = self.doc["stage1_logical_asap_optimization"]["combinations"] self.assertEqual(self.js.call("stageCandidateHeader", self.doc), f"{combinations} candidates") @@ -443,7 +461,9 @@ def test_doc_parameter_opens_one_document_with_the_selected_plan(self): self.assertIn("✓ selected", self.text(js, "lane2-meta")) self.assertEqual(js.eval("__els.pick2.value"), selected) self.assertEqual(js.eval("__els.pick1.value"), "L1") - self.assertIn("9 candidates", js.eval("__els['rank-meta'].textContent")) + combinations = example1()["stage1_logical_asap_optimization"]["combinations"] + self.assertIn(f"{combinations} candidates", js.eval("__els['rank-meta'].textContent")) + self.assertIn("metric not set; accepted exact", self.text(js, "lane2-meta")) self.assertGreater(js.eval("__nodes"), 0) def test_rank_list_has_composition_error_rows(self): diff --git a/tools/verify_metricsql_external_consumer.sh b/tools/verify_metricsql_external_consumer.sh index 74cdc6944..f4e9d89ac 100644 --- a/tools/verify_metricsql_external_consumer.sh +++ b/tools/verify_metricsql_external_consumer.sh @@ -21,7 +21,7 @@ cat > "$consumer_dir/src/main.rs" <<'EOF' fn main() { asap_frontend_metricsql::lower_metricsql( "sum(rate(requests_total[5m]))", - asap_types::types::RequestedAccuracy::exact(), + asap_types::types::AccuracyTarget::exact(), std::time::Duration::from_secs(15), ) .expect("a downstream stable crate can parse and lower MetricsQL");