diff --git a/crates/asap-aware-mapping/src/accuracy/allocation.rs b/crates/asap-aware-mapping/src/accuracy/allocation.rs index 77849d9c8..30002ed14 100644 --- a/crates/asap-aware-mapping/src/accuracy/allocation.rs +++ b/crates/asap-aware-mapping/src/accuracy/allocation.rs @@ -32,19 +32,16 @@ impl AccuracyAllocation { } let (eps, delta): (Vec, Vec>) = inner .iter() - .map(|t| match t { - AccuracyTarget::Exact => (0.0, Some(0.0)), - AccuracyTarget::Epsilon(e) => (*e, None), - AccuracyTarget::EpsilonDelta { epsilon, delta } => (*epsilon, Some(*delta)), - }) + .map(|t| (t.error, t.failure_probability)) .unzip(); let epsilon = match shape.metric { - ErrorMetric::RelativeValue => eps.iter().map(|e| 1.0 + e).product::() - 1.0, + ErrorMetric::Relative => eps.iter().map(|e| 1.0 + e).product::() - 1.0, _ => eps.iter().sum(), }; - Some(match delta.iter().copied().sum::>() { - Some(delta) => AccuracyTarget::EpsilonDelta { epsilon, delta }, - None => AccuracyTarget::Epsilon(epsilon), + Some(AccuracyTarget { + error: epsilon, + failure_probability: delta.iter().copied().sum::>(), + ..inner[0].clone() }) } } @@ -65,7 +62,7 @@ pub trait AccuracyBudgetAllocator { /// equal share — `ε_i = ε / n`, `δ_i = δ / n` for an additively composed /// metric, and `ε_i = (1 + ε)^{1/n} − 1` for a multiplicatively composed /// one — so the composed bound meets the target exactly with no slack. -/// `AccuracyTarget::Exact` yields no allocation: no approximate layer can +/// `AccuracyTarget::exact()` yields no allocation: no approximate layer can /// meet it. #[derive(Debug, Default, Clone, Copy)] pub struct EqualSplitAllocator; @@ -77,24 +74,21 @@ impl AccuracyBudgetAllocator for EqualSplitAllocator { composition: &CompositionShape, ) -> Vec { let n = composition.approximate_layer_count.max(1); - let (epsilon, delta) = match target { - AccuracyTarget::Exact => return Vec::new(), - AccuracyTarget::Epsilon(e) => (*e, None), - AccuracyTarget::EpsilonDelta { epsilon, delta } => (*epsilon, Some(*delta)), - }; + if target.is_exact() { + return Vec::new(); + } + let (epsilon, delta) = (target.error, target.failure_probability); if !(epsilon.is_finite() && epsilon > 0.0) { return Vec::new(); } let local_epsilon = match composition.metric { - ErrorMetric::RelativeValue => (1.0 + epsilon).powf(1.0 / n as f64) - 1.0, + ErrorMetric::Relative => (1.0 + epsilon).powf(1.0 / n as f64) - 1.0, _ => epsilon / n as f64, }; - let layer = match delta { - Some(delta) => AccuracyTarget::EpsilonDelta { - epsilon: local_epsilon, - delta: delta / n as f64, - }, - None => AccuracyTarget::Epsilon(local_epsilon), + let layer = AccuracyTarget { + error: local_epsilon, + failure_probability: delta.map(|delta| delta / n as f64), + ..target.clone() }; vec![AccuracyAllocation { allocator: "EqualSplitAllocator", @@ -108,12 +102,9 @@ mod tests { use super::*; #[test] fn equal_split_respects_the_root_epsilon_and_delta() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.1, - delta: 0.02, - }; + let target = AccuracyTarget::epsilon_delta(0.1, 0.02); let shape = CompositionShape { - metric: ErrorMetric::AbsoluteValue, + metric: ErrorMetric::ABSOLUTE, approximate_layer_count: 2, }; let allocations = EqualSplitAllocator.allocations(&target, &shape); @@ -122,35 +113,27 @@ mod tests { assert_eq!(layers.len(), 2); let (eps, deltas): (Vec, Vec) = layers .iter() - .map(|t| match t { - AccuracyTarget::EpsilonDelta { epsilon, delta } => (*epsilon, *delta), - other => panic!("unexpected {other:?}"), - }) + .map(|t| (t.error, t.failure_probability.unwrap())) .unzip(); assert!((eps.iter().sum::() - 0.1).abs() < 1e-12); assert!((deltas.iter().sum::() - 0.02).abs() < 1e-12); assert_eq!( allocations[0].inner_target(&shape), - Some(AccuracyTarget::EpsilonDelta { - epsilon: 0.05, - delta: 0.01 - }) + Some(AccuracyTarget::epsilon_delta(0.05, 0.01)) ); // Multiplicative composition: (1+ε_i)^2 = 1+ε, not 2ε_i = ε. let rel_shape = CompositionShape { - metric: ErrorMetric::RelativeValue, + metric: ErrorMetric::Relative, approximate_layer_count: 2, }; let allocations = - EqualSplitAllocator.allocations(&AccuracyTarget::Epsilon(0.21), &rel_shape); - let AccuracyTarget::Epsilon(e) = allocations[0].layers[0] else { - panic!() - }; + EqualSplitAllocator.allocations(&AccuracyTarget::epsilon(0.21), &rel_shape); + let e = allocations[0].layers[0].error; assert!((e - 0.1).abs() < 1e-12); assert!(EqualSplitAllocator - .allocations(&AccuracyTarget::Exact, &shape) + .allocations(&AccuracyTarget::exact(), &shape) .is_empty()); } } diff --git a/crates/asap-aware-mapping/src/accuracy/composition.rs b/crates/asap-aware-mapping/src/accuracy/composition.rs index b0b738c4f..710ebe8bc 100644 --- a/crates/asap-aware-mapping/src/accuracy/composition.rs +++ b/crates/asap-aware-mapping/src/accuracy/composition.rs @@ -85,19 +85,19 @@ impl DefaultAccuracyModel { local: &AccuracyGuarantee, rule: &str, ) -> AccuracyGuarantee { - let mut terms: Vec = inputs.iter().map(|g| g.bound.clone()).collect(); - terms.push(local.bound.clone()); + let mut terms: Vec = inputs.iter().map(|g| g.spec.error.clone()).collect(); + terms.push(local.spec.error.clone()); let mut deltas: Vec = inputs .iter() - .map(|g| g.failure_probability.clone()) + .map(|g| g.spec.failure_probability.clone()) .collect(); - deltas.push(local.failure_probability.clone()); - AccuracyGuarantee { - metric: local.metric, - bound: BoundExpr::Sum { terms }, - failure_probability: ProbabilityExpr::UnionBound { terms: deltas }, - provenance: composed_provenance(op, inputs, local, rule), - } + deltas.push(local.spec.failure_probability.clone()); + AccuracyGuarantee::new( + local.spec.metric, + BoundExpr::Sum { terms }, + ProbabilityExpr::UnionBound { terms: deltas }, + composed_provenance(op, inputs, local, rule), + ) } /// `(1 + ε_total) = Π (1 + ε_i)` ⇒ for two factors @@ -110,8 +110,8 @@ impl DefaultAccuracyModel { ) -> AccuracyGuarantee { let factors: Vec<&BoundExpr> = inputs .iter() - .map(|g| &g.bound) - .chain(std::iter::once(&local.bound)) + .map(|g| &g.spec.error) + .chain(std::iter::once(&local.spec.error)) .collect(); // Every non-empty subset's product: Π(1+ε_i) − 1 = Σ_{S≠∅} Π_{i∈S} ε_i. let mut terms = Vec::new(); @@ -130,15 +130,15 @@ impl DefaultAccuracyModel { } let mut deltas: Vec = inputs .iter() - .map(|g| g.failure_probability.clone()) + .map(|g| g.spec.failure_probability.clone()) .collect(); - deltas.push(local.failure_probability.clone()); - AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Sum { terms }, - failure_probability: ProbabilityExpr::UnionBound { terms: deltas }, - provenance: composed_provenance(op, inputs, local, "relative_cross_term_union_bound"), - } + deltas.push(local.spec.failure_probability.clone()); + AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Sum { terms }, + ProbabilityExpr::UnionBound { terms: deltas }, + composed_provenance(op, inputs, local, "relative_cross_term_union_bound"), + ) } fn lipschitz( @@ -150,34 +150,34 @@ impl DefaultAccuracyModel { let input = &inputs[0]; let scaled = BoundExpr::Scaled { factor: constant, - inner: Box::new(input.bound.clone()), + inner: Box::new(input.spec.error.clone()), }; let (bound, delta) = match local { Some(local) => ( BoundExpr::Sum { - terms: vec![scaled, local.bound.clone()], + terms: vec![scaled, local.spec.error.clone()], }, ProbabilityExpr::UnionBound { terms: vec![ - input.failure_probability.clone(), - local.failure_probability.clone(), + input.spec.failure_probability.clone(), + local.spec.failure_probability.clone(), ], }, ), - None => (scaled, input.failure_probability.clone()), + None => (scaled, input.spec.failure_probability.clone()), }; let exact_local = AccuracyGuarantee::exact("deterministic Lipschitz transformation"); - AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, + AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, bound, - failure_probability: delta, - provenance: composed_provenance( + delta, + composed_provenance( op, inputs, local.unwrap_or(&exact_local), "lipschitz_union_bound", ), - } + ) } /// Exact `sum` over approximate inputs: `B ≤ Σ B_i`, `δ ≤ Σ δ_i`. The @@ -202,12 +202,12 @@ impl DefaultAccuracyModel { let absolute = absolute_bound(input).ok_or_else(|| AccuracyError::UnsupportedComposition { operator: op.clone(), - input_metrics: inputs.iter().map(|g| g.metric).collect(), + input_metrics: inputs.iter().map(|g| g.spec.metric).collect(), local_metric: None, reason: format!( "input {i} carries a {:?} guarantee, which has no registered \ conversion to an absolute value error", - input.metric + input.spec.metric ), })?; if let BoundExpr::Product { factors } = &absolute { @@ -220,7 +220,7 @@ impl DefaultAccuracyModel { } } terms.push(absolute); - deltas.push(input.failure_probability.clone()); + deltas.push(input.spec.failure_probability.clone()); } let count = row_count(stats, &mut provenance); let exact_local = AccuracyGuarantee::exact("ExactAggregate(Sum)"); @@ -230,17 +230,17 @@ impl DefaultAccuracyModel { &exact_local, "exact_sum_union_bound", )); - Ok(AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Product { + Ok(AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Product { factors: vec![count.clone(), BoundExpr::Sum { terms }], }, - failure_probability: ProbabilityExpr::Scaled { + ProbabilityExpr::Scaled { count, inner: Box::new(ProbabilityExpr::UnionBound { terms: deltas }), }, provenance, - }) + )) } /// Exact arithmetic mean over values with absolute-error guarantees. @@ -257,11 +257,11 @@ impl DefaultAccuracyModel { } if inputs .iter() - .any(|input| input.metric != ErrorMetric::AbsoluteValue) + .any(|input| input.spec.metric != ErrorMetric::ABSOLUTE) { return Err(AccuracyError::UnsupportedComposition { operator: op.clone(), - input_metrics: inputs.iter().map(|g| g.metric).collect(), + input_metrics: inputs.iter().map(|g| g.spec.metric).collect(), local_metric: None, reason: "exact average requires AbsoluteValue input guarantees".into(), }); @@ -275,22 +275,22 @@ impl DefaultAccuracyModel { &exact_local, "exact_average_union_bound", )); - Ok(AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Max { - terms: inputs.iter().map(|g| g.bound.clone()).collect(), + Ok(AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Max { + terms: inputs.iter().map(|g| g.spec.error.clone()).collect(), }, - failure_probability: ProbabilityExpr::Scaled { + ProbabilityExpr::Scaled { count, inner: Box::new(ProbabilityExpr::UnionBound { terms: inputs .iter() - .map(|g| g.failure_probability.clone()) + .map(|g| g.spec.failure_probability.clone()) .collect(), }), }, provenance, - }) + )) } /// Exact `max`/`min` over approximate inputs of one shared metric: the @@ -306,14 +306,14 @@ impl DefaultAccuracyModel { if all_relative(inputs) { return Self::relative_fold(op, inputs, stats, false, "exact_extremum_relative"); } - let metric = inputs[0].metric; + let metric = inputs[0].spec.metric; if inputs .iter() - .any(|g| g.metric != ErrorMetric::AbsoluteValue) + .any(|g| g.spec.metric != ErrorMetric::ABSOLUTE) { return Err(AccuracyError::UnsupportedComposition { operator: op.clone(), - input_metrics: inputs.iter().map(|g| g.metric).collect(), + input_metrics: inputs.iter().map(|g| g.spec.metric).collect(), local_metric: None, reason: "exact max/min requires AbsoluteValue input guarantees".into(), }); @@ -327,22 +327,22 @@ impl DefaultAccuracyModel { &exact_local, "exact_extremum_union_bound", )); - Ok(AccuracyGuarantee { + Ok(AccuracyGuarantee::new( metric, - bound: BoundExpr::Max { - terms: inputs.iter().map(|g| g.bound.clone()).collect(), + BoundExpr::Max { + terms: inputs.iter().map(|g| g.spec.error.clone()).collect(), }, - failure_probability: ProbabilityExpr::Scaled { + ProbabilityExpr::Scaled { count, inner: Box::new(ProbabilityExpr::UnionBound { terms: inputs .iter() - .map(|g| g.failure_probability.clone()) + .map(|g| g.spec.failure_probability.clone()) .collect(), }), }, provenance, - }) + )) } /// An exact sum, mean, max or min of values each within relative `α_i` @@ -361,7 +361,7 @@ impl DefaultAccuracyModel { if needs_sign && stats.values_non_negative == Some(false) { return Err(AccuracyError::UnsupportedComposition { operator: op.clone(), - input_metrics: inputs.iter().map(|g| g.metric).collect(), + input_metrics: inputs.iter().map(|g| g.spec.metric).collect(), local_metric: None, reason: "relative error of a sum over signed values is unbounded".into(), }); @@ -378,7 +378,7 @@ impl DefaultAccuracyModel { BoundExpr::Product { factors: vec![ BoundExpr::Max { - terms: inputs.iter().map(|g| g.bound.clone()).collect(), + terms: inputs.iter().map(|g| g.spec.error.clone()).collect(), }, BoundExpr::Unknown { statistic: statistic.clone(), @@ -397,7 +397,7 @@ impl DefaultAccuracyModel { } } else { BoundExpr::Max { - terms: inputs.iter().map(|g| g.bound.clone()).collect(), + terms: inputs.iter().map(|g| g.spec.error.clone()).collect(), } }; provenance.extend(composed_provenance( @@ -406,20 +406,20 @@ impl DefaultAccuracyModel { &AccuracyGuarantee::exact("exact aggregation"), rule, )); - Ok(AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, + Ok(AccuracyGuarantee::new( + ErrorMetric::Relative, bound, - failure_probability: ProbabilityExpr::Scaled { + ProbabilityExpr::Scaled { count, inner: Box::new(ProbabilityExpr::UnionBound { terms: inputs .iter() - .map(|g| g.failure_probability.clone()) + .map(|g| g.spec.failure_probability.clone()) .collect(), }), }, provenance, - }) + )) } /// Exact division of two relative-value estimates. If the numerator is @@ -432,25 +432,25 @@ impl DefaultAccuracyModel { ) -> Result { let unsupported = |reason: String| AccuracyError::UnsupportedComposition { operator: op.clone(), - input_metrics: inputs.iter().map(|g| g.metric).collect(), + input_metrics: inputs.iter().map(|g| g.spec.metric).collect(), local_metric: None, reason, }; if inputs.len() != 2 || inputs .iter() - .any(|input| input.metric != ErrorMetric::RelativeValue && !input.is_exact()) + .any(|input| input.spec.metric != ErrorMetric::Relative && !input.is_exact()) { return Err(unsupported( "division needs two relative-value or exact guarantees".into(), )); } - let Some(numerator) = inputs[0].bound.evaluate() else { + let Some(numerator) = inputs[0].spec.error.evaluate() else { return Err(unsupported( "numerator relative bound is unavailable".into(), )); }; - let Some(denominator) = inputs[1].bound.evaluate() else { + let Some(denominator) = inputs[1].spec.error.evaluate() else { return Err(unsupported( "denominator relative bound is unavailable".into(), )); @@ -509,18 +509,18 @@ impl DefaultAccuracyModel { } } } - Ok(AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Constant { + Ok(AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Constant { value: (numerator + denominator) / (1.0 - denominator), }, - failure_probability: ProbabilityExpr::UnionBound { + ProbabilityExpr::UnionBound { terms: inputs .iter() - .map(|input| input.failure_probability.clone()) + .map(|input| input.spec.failure_probability.clone()) .collect(), }, - provenance: inputs + inputs .iter() .enumerate() .map(|(input_index, guarantee)| GuaranteeSource::ChildGuarantee { @@ -541,16 +541,24 @@ impl DefaultAccuracyModel { rule: "relative_division".into(), })) .collect(), - }) + )) } } +/// Whether `guarantee` is a sketch's distinct-count estimate. +fn estimates_distinct_count(guarantee: &AccuracyGuarantee) -> bool { + let cardinality = format!("{:?}", asap_types::post_asap::SketchStatistic::Cardinality); + guarantee.provenance.iter().any(|source| { + matches!(source, GuaranteeSource::SketchEvaluation { query, .. } if *query == cardinality) + }) +} + /// Every input carries a relative-value guarantee. fn all_relative(inputs: &[AccuracyGuarantee]) -> bool { !inputs.is_empty() && inputs .iter() - .all(|g| g.metric == ErrorMetric::RelativeValue) + .all(|g| g.spec.metric == ErrorMetric::Relative) } /// `stats.input_row_count` as a bound factor, or an `Unknown` leaf named by @@ -570,8 +578,8 @@ fn row_count(stats: &PropagationStats, provenance: &mut Vec) -> } } -/// `input` with a Count-Min ([`ErrorMetric::Frequency`]) or Count Sketch -/// ([`ErrorMetric::L2Frequency`]) bound restated as an additive absolute +/// `input` with a Count-Min ([`ErrorMetric::ABSOLUTE_STREAM_L1`]) or Count Sketch +/// ([`ErrorMetric::ABSOLUTE_STREAM_L2`]) bound restated as an additive absolute /// error: the normalized bound times its stream norm, a symbolic statistic /// until bound. The norm is the one `norm` names ([`StreamNorm`]), which /// plan selection can bind; without one (a weight computed from the @@ -582,8 +590,8 @@ pub(crate) fn additive_as_absolute( norm: Option<&StreamNorm>, ) -> AccuracyGuarantee { if !matches!( - input.metric, - ErrorMetric::Frequency | ErrorMetric::L2Frequency + input.spec.metric, + ErrorMetric::ABSOLUTE_STREAM_L1 | ErrorMetric::ABSOLUTE_STREAM_L2 ) || input.is_exact() { return input; @@ -616,12 +624,13 @@ pub(crate) fn additive_as_absolute( statistic: statistic.into(), }) }); - AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound, + let mut absolute = AccuracyGuarantee { provenance, ..input - } + }; + absolute.spec.metric = ErrorMetric::ABSOLUTE; + absolute.spec.error = bound; + absolute } /// Every `Unknown` statistic in `bound`. @@ -642,24 +651,23 @@ fn unknown_statistics(bound: &BoundExpr, found: &mut dyn FnMut(&str)) { /// `input`'s bound converted to absolute value units, multiplying a /// normalized metric by the (unknown) statistic it is normalized by. `None` -/// for a metric with no such conversion (`Rank`, `TopKMembership`). +/// for a metric with no such conversion (`Rank`). fn absolute_bound(input: &AccuracyGuarantee) -> Option { - let normalizer = match input.metric { - ErrorMetric::AbsoluteValue => return Some(input.bound.clone()), - ErrorMetric::RelativeValue => "true_value_magnitude", - ErrorMetric::Cardinality => "true_cardinality", - ErrorMetric::Frequency => STREAM_L1_NORM, - ErrorMetric::L2Frequency => STREAM_L2_NORM, + let normalizer = match input.spec.metric { + ErrorMetric::ABSOLUTE => return Some(input.spec.error.clone()), + ErrorMetric::Relative => "true_value_magnitude", + ErrorMetric::ABSOLUTE_STREAM_L1 => STREAM_L1_NORM, + ErrorMetric::ABSOLUTE_STREAM_L2 => STREAM_L2_NORM, // `Rank` has no distribution-free conversion to a value error; a // metric this crate does not know has no registered conversion. - ErrorMetric::Rank | ErrorMetric::TopKMembership | _ => return None, + ErrorMetric::Rank | _ => return None, }; - if input.bound.is_zero() { + if input.spec.error.is_zero() { return Some(BoundExpr::Zero); } Some(BoundExpr::Product { factors: vec![ - input.bound.clone(), + input.spec.error.clone(), BoundExpr::Unknown { statistic: normalizer.into(), }, @@ -736,73 +744,83 @@ pub(super) fn propagate( }); } - let input_metrics: Vec = inputs.iter().map(|g| g.metric).collect(); + let input_metrics: Vec = inputs.iter().map(|g| g.spec.metric).collect(); let unsupported = |reason: String| AccuracyError::UnsupportedComposition { operator: op.clone(), input_metrics: input_metrics.clone(), - local_metric: local.map(|g| g.metric), + local_metric: local.map(|g| g.spec.metric), reason, }; // An exact input is compatible with every metric; only approximate // inputs constrain the rule. let approximate: Vec<&AccuracyGuarantee> = inputs.iter().filter(|g| !g.is_exact()).collect(); - let same_metric = |metric: ErrorMetric| approximate.iter().all(|g| g.metric == metric); + let same_metric = |metric: ErrorMetric| approximate.iter().all(|g| g.spec.metric == metric); match op { CompositionOperator::CheckedRelativeDivision => { - if inputs.len() != 2 || local.is_some() || !same_metric(ErrorMetric::RelativeValue) { + if inputs.len() != 2 || local.is_some() || !same_metric(ErrorMetric::Relative) { return Err(unsupported( "checked division requires two exact/relative-value operands".into(), )); } let a = inputs[0] - .bound + .spec + .error .evaluate() .ok_or_else(|| unsupported("unknown numerator bound".into()))?; let b = inputs[1] - .bound + .spec + .error .evaluate() .ok_or_else(|| unsupported("unknown denominator bound".into()))?; if !(0.0..1.0).contains(&b) || a < 0.0 || !a.is_finite() { return Err(unsupported("invalid relative division bounds".into())); } - Ok(AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Constant { + Ok(AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Constant { value: (a + b) / (1.0 - b) + 4.0 * f64::EPSILON, }, - failure_probability: ProbabilityExpr::UnionBound { + ProbabilityExpr::UnionBound { terms: inputs .iter() - .map(|g| g.failure_probability.clone()) + .map(|g| g.spec.failure_probability.clone()) .collect(), }, - provenance: composed_provenance( + composed_provenance( op, inputs, &AccuracyGuarantee::exact("checked floating-point division"), "checked_relative_division_union_bound", ), - }) + )) } CompositionOperator::ApproximateAggregate => { let local = local.ok_or_else(|| { unsupported("approximate operator has no local guarantee to compose".into()) })?; - if !same_metric(local.metric) { + if !same_metric(local.spec.metric) { return Err(unsupported(format!( "no registered cross-metric rule from {input_metrics:?} to {:?}", - local.metric + local.spec.metric ))); } - match local.metric { - ErrorMetric::AbsoluteValue => Ok(DefaultAccuracyModel::additive( + // A distinct count over approximate values is not within the + // inputs' relative error: no rule, as for any summary whose + // estimate is not a value of its inputs. + if estimates_distinct_count(local) { + return Err(unsupported( + "no registered rule for a distinct count over approximate values".into(), + )); + } + match local.spec.metric { + ErrorMetric::ABSOLUTE => Ok(DefaultAccuracyModel::additive( op, inputs, local, "additive_union_bound", )), - ErrorMetric::RelativeValue => { + ErrorMetric::Relative => { if stats.values_non_negative == Some(false) { return Err(unsupported( "relative-error composition cannot use a known signed input".into(), @@ -819,27 +837,25 @@ pub(super) fn propagate( provenance.push(GuaranteeSource::UnavailableStatistic { statistic: VALUES_NON_NEGATIVE.into(), }); - return Ok(AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Unknown { + return Ok(AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Unknown { statistic: VALUES_NON_NEGATIVE.into(), }, - failure_probability: ProbabilityExpr::Unknown { + ProbabilityExpr::Unknown { statistic: VALUES_NON_NEGATIVE.into(), }, provenance, - }); + )); } Ok(DefaultAccuracyModel::multiplicative(op, inputs, local)) } ErrorMetric::Rank - | ErrorMetric::Cardinality - | ErrorMetric::Frequency - | ErrorMetric::L2Frequency - | ErrorMetric::TopKMembership + | ErrorMetric::ABSOLUTE_STREAM_L1 + | ErrorMetric::ABSOLUTE_STREAM_L2 | _ => Err(unsupported(format!( "no registered same-metric composition rule for {:?} over {:?}", - local.metric, local.metric + local.spec.metric, local.spec.metric ))), } } @@ -849,12 +865,12 @@ pub(super) fn propagate( "Lipschitz constant {constant} is not a finite non-negative number" ))); } - if inputs.len() != 1 || !same_metric(ErrorMetric::AbsoluteValue) { + if inputs.len() != 1 || !same_metric(ErrorMetric::ABSOLUTE) { return Err(unsupported( "Lipschitz rule is registered for exactly one AbsoluteValue input".into(), )); } - if local.is_some_and(|g| g.metric != ErrorMetric::AbsoluteValue) { + if local.is_some_and(|g| g.spec.metric != ErrorMetric::ABSOLUTE) { return Err(unsupported( "Lipschitz rule needs an AbsoluteValue local guarantee".into(), )); @@ -921,23 +937,37 @@ pub(super) fn propagate( rule: "topk_membership_margin_certificate".into(), }); let certified = selected.is_some() && excluded.is_some() && delta.is_some(); - Ok(AccuracyGuarantee { - metric: ErrorMetric::TopKMembership, - bound: if certified { - BoundExpr::Zero - } else { - BoundExpr::Unknown { - statistic: "topk_membership_margin".into(), - } - }, - failure_probability: delta.map_or_else( + // A certified margin makes the returned key set the true one, so + // each returned value keeps its score's error; without one, + // membership and hence the values are unknown. + let metric = approximate + .first() + .map_or(ErrorMetric::ABSOLUTE, |score| score.spec.metric); + if !same_metric(metric) { + return Err(unsupported( + "top-k selection needs scores of one metric".into(), + )); + } + let error = if certified { + BoundExpr::Max { + terms: inputs.iter().map(|g| g.spec.error.clone()).collect(), + } + } else { + BoundExpr::Unknown { + statistic: "topk_membership_margin".into(), + } + }; + Ok(AccuracyGuarantee::new( + metric, + error, + delta.map_or_else( || ProbabilityExpr::Unknown { statistic: "topk_interval_failure_probability".into(), }, |value| ProbabilityExpr::Constant { value }, ), provenance, - }) + )) } // An operator this crate does not know has no registered rule. _ => Err(unsupported("no registered rule for this operator".into())), @@ -972,20 +1002,20 @@ mod tests { } use asap_types::pre_asap::AggIntent; fn abs(bound: f64, delta: f64) -> AccuracyGuarantee { - AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Constant { value: bound }, - failure_probability: ProbabilityExpr::Constant { value: delta }, - provenance: vec![], - } + AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Constant { value: bound }, + ProbabilityExpr::Constant { value: delta }, + vec![], + ) } fn rel(bound: f64) -> AccuracyGuarantee { - AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Constant { value: bound }, - failure_probability: ProbabilityExpr::Zero, - provenance: vec![], - } + AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Constant { value: bound }, + ProbabilityExpr::Zero, + vec![], + ) } fn domain(lower: f64, upper: f64) -> OperandDomain { OperandDomain { @@ -996,10 +1026,9 @@ mod tests { } } fn with_metric(metric: ErrorMetric, bound: f64) -> AccuracyGuarantee { - AccuracyGuarantee { - metric, - ..abs(bound, 0.0) - } + let mut g = abs(bound, 0.0); + g.spec.metric = metric; + g } #[test] fn checked_division_propagates_value_bounds_and_rejects_rank_bounds() { @@ -1008,9 +1037,9 @@ mod tests { let g = DefaultAccuracyModel .propagate(&op, &inputs, None, &Default::default()) .unwrap(); - assert!((g.bound.evaluate().unwrap() - 0.02 / 0.99).abs() < 1e-14); + assert!((g.spec.error.evaluate().unwrap() - 0.02 / 0.99).abs() < 1e-14); let mut rank = inputs[0].clone(); - rank.metric = ErrorMetric::Rank; + rank.spec.metric = ErrorMetric::Rank; assert!(DefaultAccuracyModel .propagate(&op, &[rank.clone(), rank], None, &Default::default()) .is_err()); @@ -1044,8 +1073,8 @@ mod tests { &stats, ) .unwrap(); - assert!((got.bound.evaluate().unwrap() - 0.05 / 0.97).abs() < 1e-14); - assert_eq!(got.failure_probability.evaluate(), Some(0.)); + assert!((got.spec.error.evaluate().unwrap() - 0.05 / 0.97).abs() < 1e-14); + assert_eq!(got.spec.failure_probability.evaluate(), Some(0.)); assert_eq!( got.provenance .iter() @@ -1104,9 +1133,9 @@ mod tests { &PropagationStats::default(), ) .unwrap(); - assert_eq!(out.bound.evaluate(), Some(0.05)); - assert_eq!(out.failure_probability.evaluate(), Some(0.01)); - assert_eq!(out.metric, ErrorMetric::AbsoluteValue); + assert_eq!(out.spec.error.evaluate(), Some(0.05)); + assert_eq!(out.spec.failure_probability.evaluate(), Some(0.01)); + assert_eq!(out.spec.metric, ErrorMetric::ABSOLUTE); } #[test] fn additive_bounds_and_delta_union_bound_compose() { @@ -1118,9 +1147,9 @@ mod tests { &PropagationStats::default(), ) .unwrap(); - assert!((out.bound.evaluate().unwrap() - 0.05).abs() < 1e-12); + assert!((out.spec.error.evaluate().unwrap() - 0.05).abs() < 1e-12); // Union bound, not 1 − (1−0.01)(1−0.02) = 0.0298. - assert!((out.failure_probability.evaluate().unwrap() - 0.03).abs() < 1e-12); + assert!((out.spec.failure_probability.evaluate().unwrap() - 0.03).abs() < 1e-12); assert!(out.provenance.iter().any(|s| matches!( s, GuaranteeSource::CompositionStep { rule, .. } if rule == "additive_union_bound" @@ -1141,8 +1170,8 @@ mod tests { ) .unwrap(); // 0.1 + 0.2 + 0.1·0.2 = 0.32, not 0.3. - assert!((out.bound.evaluate().unwrap() - 0.32).abs() < 1e-12); - assert_eq!(out.metric, ErrorMetric::RelativeValue); + assert!((out.spec.error.evaluate().unwrap() - 0.32).abs() < 1e-12); + assert_eq!(out.spec.metric, ErrorMetric::Relative); } #[test] fn relative_error_without_sign_knowledge_remains_symbolic() { @@ -1172,8 +1201,8 @@ mod tests { let err = DefaultAccuracyModel .propagate( &CompositionOperator::ApproximateAggregate, - &[with_metric(ErrorMetric::Cardinality, 0.01)], - Some(&with_metric(ErrorMetric::Frequency, 0.01)), + &[with_metric(ErrorMetric::Relative, 0.01)], + Some(&with_metric(ErrorMetric::ABSOLUTE_STREAM_L1, 0.01)), &PropagationStats::default(), ) .unwrap_err(); @@ -1181,9 +1210,9 @@ mod tests { err, AccuracyError::UnsupportedComposition { input_metrics, - local_metric: Some(ErrorMetric::Frequency), + local_metric: Some(ErrorMetric::ABSOLUTE_STREAM_L1), .. - } if input_metrics == vec![ErrorMetric::Cardinality] + } if input_metrics == vec![ErrorMetric::Relative] )); // Quantile rank error under value-additive logic. let err = DefaultAccuracyModel @@ -1218,8 +1247,8 @@ mod tests { &PropagationStats::default(), ) .unwrap(); - assert!((out.bound.evaluate().unwrap() - 0.35).abs() < 1e-12); - assert!((out.failure_probability.evaluate().unwrap() - 0.03).abs() < 1e-12); + assert!((out.spec.error.evaluate().unwrap() - 0.35).abs() < 1e-12); + assert!((out.spec.failure_probability.evaluate().unwrap() - 0.03).abs() < 1e-12); } #[test] fn exact_sum_over_approximate_sums_bounds_and_keeps_unknown_row_count_unknown() { @@ -1231,9 +1260,9 @@ mod tests { &PropagationStats::default(), ) .unwrap(); - assert_eq!(out.metric, ErrorMetric::AbsoluteValue); + assert_eq!(out.spec.metric, ErrorMetric::ABSOLUTE); assert_eq!( - out.bound.evaluate(), + out.spec.error.evaluate(), None, "unknown row count stays unknown" ); @@ -1241,7 +1270,7 @@ mod tests { s, GuaranteeSource::UnavailableStatistic { statistic } if statistic == "input_row_count" ))); - assert!(!DefaultAccuracyModel.satisfies(&out, &AccuracyTarget::Epsilon(1.0))); + assert!(!DefaultAccuracyModel.satisfies(&out, &AccuracyTarget::epsilon(1.0))); let known = PropagationStats { input_row_count: Some(4), @@ -1255,8 +1284,8 @@ mod tests { &known, ) .unwrap(); - assert!((out.bound.evaluate().unwrap() - 0.4).abs() < 1e-12); - assert!((out.failure_probability.evaluate().unwrap() - 0.04).abs() < 1e-12); + assert!((out.spec.error.evaluate().unwrap() - 0.4).abs() < 1e-12); + assert!((out.spec.failure_probability.evaluate().unwrap() - 0.04).abs() < 1e-12); } #[test] fn exact_extremum_takes_the_max_bound() { @@ -1272,8 +1301,8 @@ mod tests { &known, ) .unwrap(); - assert!((out.bound.evaluate().unwrap() - 0.3).abs() < 1e-12); - assert!((out.failure_probability.evaluate().unwrap() - 0.04).abs() < 1e-12); + assert!((out.spec.error.evaluate().unwrap() - 0.3).abs() < 1e-12); + assert!((out.spec.failure_probability.evaluate().unwrap() - 0.04).abs() < 1e-12); } /// `max`/`min` of values each within relative `α_i` is within relative /// `max α_i` of the true extremum, whatever the signs. @@ -1290,9 +1319,9 @@ mod tests { }, ) .unwrap(); - assert_eq!(out.metric, ErrorMetric::RelativeValue); - assert_eq!(out.bound.evaluate(), Some(0.02)); - assert_eq!(out.failure_probability.evaluate(), Some(0.0)); + assert_eq!(out.spec.metric, ErrorMetric::Relative); + assert_eq!(out.spec.error.evaluate(), Some(0.02)); + assert_eq!(out.spec.failure_probability.evaluate(), Some(0.0)); } /// A sum or mean of relative-error values keeps the relative bound only /// over values known non-negative (cancellation otherwise); with the sign @@ -1316,12 +1345,12 @@ mod tests { ) }; let known = with(Some(true)).unwrap(); - assert_eq!(known.metric, ErrorMetric::RelativeValue); - assert_eq!(known.bound.evaluate(), Some(0.01), "{op:?}"); + assert_eq!(known.spec.metric, ErrorMetric::Relative); + assert_eq!(known.spec.error.evaluate(), Some(0.01), "{op:?}"); let unknown = with(None).unwrap(); - assert_eq!(unknown.metric, ErrorMetric::RelativeValue); + assert_eq!(unknown.spec.metric, ErrorMetric::Relative); assert_eq!( - unknown.bound, + unknown.spec.error, BoundExpr::Unknown { statistic: VALUES_NON_NEGATIVE.into() } @@ -1354,11 +1383,11 @@ mod tests { }, ) .unwrap(); - assert_eq!(out.bound.evaluate(), None, "{op:?}"); + assert_eq!(out.spec.error.evaluate(), None, "{op:?}"); let proven = out.bind(&|s| (s == statistic).then_some(1.0)); - assert_eq!(proven.bound.evaluate(), Some(0.01), "{op:?}"); + assert_eq!(proven.spec.error.evaluate(), Some(0.01), "{op:?}"); let other = out.bind(&|s| (s == samples_sign_statistic("n")).then_some(1.0)); - assert_eq!(other.bound.evaluate(), None, "{op:?}"); + assert_eq!(other.spec.error.evaluate(), None, "{op:?}"); } } @@ -1375,9 +1404,9 @@ mod tests { }, ) .unwrap(); - assert_eq!(out.metric, ErrorMetric::AbsoluteValue); - assert_eq!(out.bound.evaluate(), Some(0.25)); - assert_eq!(out.failure_probability.evaluate(), Some(0.04)); + assert_eq!(out.spec.metric, ErrorMetric::ABSOLUTE); + assert_eq!(out.spec.error.evaluate(), Some(0.25)); + assert_eq!(out.spec.failure_probability.evaluate(), Some(0.04)); } #[test] fn counter_functions_have_distinct_definition_rules() { @@ -1426,9 +1455,9 @@ mod tests { }, ) .unwrap(); - assert_eq!(certified.metric, ErrorMetric::TopKMembership); - assert_eq!(certified.bound.evaluate(), Some(0.0)); - assert_eq!(certified.failure_probability.evaluate(), Some(0.005)); + assert_eq!(certified.spec.metric, ErrorMetric::ABSOLUTE); + assert_eq!(certified.spec.error.evaluate(), Some(0.1)); + assert_eq!(certified.spec.failure_probability.evaluate(), Some(0.005)); let overlapping = DefaultAccuracyModel.propagate( &CompositionOperator::TopKSelection, @@ -1456,7 +1485,7 @@ mod tests { ) .unwrap(); assert!(partial.has_unknown()); - assert_eq!(partial.failure_probability.evaluate(), Some(0.005)); + assert_eq!(partial.spec.failure_probability.evaluate(), Some(0.005)); let invalid_partial = DefaultAccuracyModel.propagate( &CompositionOperator::TopKSelection, diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/cardinality.rs b/crates/asap-aware-mapping/src/accuracy/estimators/cardinality.rs index 0c9f30c48..35207ac51 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/cardinality.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/cardinality.rs @@ -13,7 +13,7 @@ pub(super) fn guarantee( algorithm, params, query, - ErrorMetric::Cardinality, + ErrorMetric::Relative, 10.0 / f64::from(k.saturating_sub(2).max(1)).sqrt(), ProbabilityExpr::Constant { value: 0.01 }, match params { diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/cms.rs b/crates/asap-aware-mapping/src/accuracy/estimators/cms.rs index 52af4215c..ddcc4afb4 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/cms.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/cms.rs @@ -15,7 +15,7 @@ pub(super) fn guarantee( algorithm, params, query, - ErrorMetric::Frequency, + ErrorMetric::ABSOLUTE_STREAM_L1, std::f64::consts::E / f64::from(*width), ProbabilityExpr::Constant { value: (-f64::from(*depth)).exp(), @@ -54,13 +54,11 @@ mod tests { &SketchStatistic::Cardinality, ) .unwrap(); - assert_eq!(g.metric, ErrorMetric::Frequency); + assert_eq!(g.spec.metric, ErrorMetric::ABSOLUTE_STREAM_L1); assert!(DefaultAccuracyModel.satisfies( &g, - &AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001 - } + &AccuracyTarget::epsilon_delta(0.01, 0.001) + .with_metric(ErrorMetric::ABSOLUTE_STREAM_L1) )); } @@ -81,6 +79,6 @@ mod tests { &SketchStatistic::TopK { k: 10 }, ) .expect("heap sketch still provides per-key frequency intervals"); - assert_eq!(topk_frequency.metric, ErrorMetric::Frequency); + assert_eq!(topk_frequency.spec.metric, ErrorMetric::ABSOLUTE_STREAM_L1); } } diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/count_sketch.rs b/crates/asap-aware-mapping/src/accuracy/estimators/count_sketch.rs index a6faa0954..f0293dcd8 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/count_sketch.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/count_sketch.rs @@ -16,7 +16,7 @@ pub(super) fn guarantee( algorithm, params, query, - ErrorMetric::L2Frequency, + ErrorMetric::ABSOLUTE_STREAM_L2, (3.0 / f64::from(*width)).sqrt(), ProbabilityExpr::Constant { value: count_sketch_failure_probability(*width, *depth)?, @@ -87,13 +87,10 @@ mod tests { }, ) .expect("CountSketch has a parameter-derived L2 guarantee"); - assert_eq!(guarantee.metric, ErrorMetric::L2Frequency); + assert_eq!(guarantee.spec.metric, ErrorMetric::ABSOLUTE_STREAM_L2); assert!(DefaultAccuracyModel.satisfies( &guarantee, - &AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - } + &AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::ABSOLUTE_STREAM_L2) )); } diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/ddsketch.rs b/crates/asap-aware-mapping/src/accuracy/estimators/ddsketch.rs index 890b5b747..eafd6b3ce 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/ddsketch.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/ddsketch.rs @@ -13,7 +13,7 @@ pub(super) fn guarantee( algorithm, params, query, - ErrorMetric::RelativeValue, + ErrorMetric::Relative, *alpha, ProbabilityExpr::Zero, "ddsketch_relative_error_alpha_v1", diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/hll.rs b/crates/asap-aware-mapping/src/accuracy/estimators/hll.rs index 459c5e892..51ff31969 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/hll.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/hll.rs @@ -18,7 +18,7 @@ pub(super) fn generic_guarantee( algorithm, params, query, - ErrorMetric::Cardinality, + ErrorMetric::Relative, relative_standard_error(*precision), ProbabilityExpr::Unknown { statistic: "hll_estimator_failure_probability".into(), @@ -68,15 +68,15 @@ pub(crate) fn classic_guarantee(precision: u8, source: &str) -> Option Option Option { let delta = self.failure_probability(precision)?; - Some(AccuracyGuarantee { - metric: ErrorMetric::Cardinality, - bound: BoundExpr::Constant { + Some(AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Constant { value: self.relative_error, }, - failure_probability: ProbabilityExpr::Constant { value: delta }, - provenance: vec![GuaranteeSource::SketchEvaluation { + ProbabilityExpr::Constant { value: delta }, + vec![GuaranteeSource::SketchEvaluation { algorithm: "Hll".into(), contract: CLASSIC_CONTRACT.into(), params: serde_json::json!({"precision": precision, @@ -141,7 +141,7 @@ impl ClassicHllConfidence { "population_scope": "complete_evaluation_including_merged_panes"}), query: "Cardinality".into(), }], - }) + )) } pub fn precision(&self, delta: f64) -> Option { @@ -225,7 +225,7 @@ mod tests { #[test] fn classic_guarantee_binds_the_distinct_bound() { let guarantee = classic_guarantee(12, "m").unwrap(); - assert_eq!(guarantee.failure_probability.evaluate(), None); + assert_eq!(guarantee.spec.failure_probability.evaluate(), None); let rse = relative_standard_error(12); let expected = ClassicHllConfidence::new(9, rse) .unwrap() @@ -235,7 +235,7 @@ mod tests { let (precision, source) = parse_classic_failure_statistic(statistic)?; (source == "m").then(|| classic_failure_probability(precision, 9)) }); - assert_eq!(bound.failure_probability.evaluate(), Some(expected)); + assert_eq!(bound.spec.failure_probability.evaluate(), Some(expected)); assert!(expected <= 0.01); assert!(classic_failure_probability(12, 10) > 0.01); for too_large in [4097, u64::MAX] { @@ -251,8 +251,8 @@ mod tests { let precision = model.precision(0.01).expect("finite confidence-sized HLL"); let guarantee = model.guarantee(precision).unwrap(); assert!(!guarantee.has_unknown()); - assert!(guarantee.failure_probability.evaluate().unwrap() <= 0.01); - assert_eq!(guarantee.bound.evaluate(), Some(0.05)); + assert!(guarantee.spec.failure_probability.evaluate().unwrap() <= 0.01); + assert_eq!(guarantee.spec.error.evaluate(), Some(0.05)); } /// Tighter confidence must increase precision or explicitly become unavailable. #[test] @@ -315,6 +315,7 @@ mod tests { model .guarantee(16) .unwrap() + .spec .failure_probability .evaluate() .unwrap() @@ -363,15 +364,15 @@ mod tests { &SketchStatistic::Cardinality, ) .unwrap(); - assert_eq!(g.metric, ErrorMetric::Cardinality); - assert_eq!(g.failure_probability.evaluate(), None); - assert!(DefaultAccuracyModel.satisfies(&g, &AccuracyTarget::Epsilon(0.01))); + assert_eq!(g.spec.metric, ErrorMetric::Relative); + assert_eq!(g.spec.failure_probability.evaluate(), None); + assert!(DefaultAccuracyModel.satisfies( + &g, + &AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Relative) + )); assert!(!DefaultAccuracyModel.satisfies( &g, - &AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - } + &AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Relative) )); } } diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/kll.rs b/crates/asap-aware-mapping/src/accuracy/estimators/kll.rs index 4def67dd6..ca84f5532 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/kll.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/kll.rs @@ -170,15 +170,15 @@ mod tests { &SketchStatistic::Quantile { q: 0.99 }, ) .unwrap(); - assert_eq!(g.metric, ErrorMetric::Rank); - assert!(DefaultAccuracyModel.satisfies(&g, &AccuracyTarget::Epsilon(0.01))); - assert_eq!(g.failure_probability.evaluate(), Some(0.01)); + assert_eq!(g.spec.metric, ErrorMetric::Rank); assert!(DefaultAccuracyModel.satisfies( &g, - &AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - } + &AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Rank) + )); + assert_eq!(g.spec.failure_probability.evaluate(), Some(0.01)); + assert!(DefaultAccuracyModel.satisfies( + &g, + &AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank) )); assert_eq!(g.approximate_layer_count(), 1); assert!(g.provenance.iter().any(|source| matches!( @@ -209,12 +209,9 @@ mod tests { .unwrap(); assert!(DefaultAccuracyModel.satisfies( &g, - &AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - } + &AccuracyTarget::epsilon_delta(0.01, 0.001).with_metric(ErrorMetric::Rank) )); - assert_eq!(g.failure_probability.evaluate(), Some(0.001)); + assert_eq!(g.spec.failure_probability.evaluate(), Some(0.001)); } /// A KLL sized below δ = 0.01 names its own contract, not the empirical diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/mod.rs b/crates/asap-aware-mapping/src/accuracy/estimators/mod.rs index 8d15ef490..922385dcd 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/mod.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/mod.rs @@ -41,17 +41,17 @@ fn bounded_guarantee( delta: ProbabilityExpr, contract: &str, ) -> AccuracyGuarantee { - AccuracyGuarantee { + AccuracyGuarantee::new( metric, - bound: BoundExpr::Constant { value: bound }, - failure_probability: delta, - provenance: vec![GuaranteeSource::SketchEvaluation { + BoundExpr::Constant { value: bound }, + delta, + vec![GuaranteeSource::SketchEvaluation { algorithm: format!("{algorithm:?}"), contract: contract.into(), params: serde_json::to_value(params).unwrap_or(serde_json::Value::Null), query: format!("{query:?}"), }], - } + ) } pub(super) fn local_guarantee( diff --git a/crates/asap-aware-mapping/src/accuracy/estimators/univmon.rs b/crates/asap-aware-mapping/src/accuracy/estimators/univmon.rs index 5b6ad3280..39537cb34 100644 --- a/crates/asap-aware-mapping/src/accuracy/estimators/univmon.rs +++ b/crates/asap-aware-mapping/src/accuracy/estimators/univmon.rs @@ -77,7 +77,7 @@ pub(super) fn guarantee( algorithm, params, query, - ErrorMetric::RelativeValue, + ErrorMetric::Relative, l2_relative_error(*sketch_cols)?, ProbabilityExpr::Constant { value: l2_failure_probability(*sketch_rows), @@ -263,18 +263,18 @@ pub(crate) fn distinct_guarantee(params: &SketchParams, source: &str) -> Option< } let relative_error = (DISTINCT_VARIANCE / (f64::from(*heap_size) * chebyshev)).sqrt(); let statistic = incomplete_top_layer_statistic(*heap_size, *layers, source); - Some(AccuracyGuarantee { - metric: ErrorMetric::Cardinality, - bound: BoundExpr::Constant { + Some(AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Constant { value: relative_error, }, - failure_probability: ProbabilityExpr::UnionBound { + ProbabilityExpr::UnionBound { terms: vec![ ProbabilityExpr::Constant { value: chebyshev }, ProbabilityExpr::Unknown { statistic }, ], }, - provenance: vec![GuaranteeSource::SketchEvaluation { + vec![GuaranteeSource::SketchEvaluation { algorithm: "UnivMon".into(), contract: DISTINCT_CONTRACT.into(), params: serde_json::json!({"heap_size": heap_size, "layers": layers, @@ -286,7 +286,7 @@ pub(crate) fn distinct_guarantee(params: &SketchParams, source: &str) -> Option< "population_scope": "complete_evaluation_including_merged_panes"}), query: "Cardinality".into(), }], - }) + )) } /// Size a UnivMon for one readout's `(eps, delta)`: @@ -330,7 +330,7 @@ mod tests { fn l2_intent() -> AggIntent { AggIntent::FrequencyL2 { col: None, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), } } @@ -359,7 +359,13 @@ mod tests { } fn meets(guarantee: &AccuracyGuarantee, epsilon: f64, delta: f64) -> bool { - DefaultAccuracyModel.satisfies(guarantee, &AccuracyTarget::EpsilonDelta { epsilon, delta }) + DefaultAccuracyModel.satisfies( + guarantee, + &crate::accuracy::native_target( + &AccuracyTarget::epsilon_delta(epsilon, delta), + guarantee, + ), + ) } fn with(params: &SketchParams, f: impl FnOnce(&mut u32, &mut u32, &mut u32)) -> SketchParams { @@ -393,7 +399,7 @@ mod tests { for (eps, delta) in TARGETS { let params = size_params(&l2_intent(), eps, delta); let guarantee = l2(¶ms).expect("an L2 guarantee"); - assert_eq!(guarantee.metric, ErrorMetric::RelativeValue); + assert_eq!(guarantee.spec.metric, ErrorMetric::Relative); assert!( meets(&guarantee, eps, delta), "({eps}, {delta}): {params:?}" @@ -476,7 +482,7 @@ mod tests { }; for max_distinct in [9, 10_000_000] { let guarantee = bind(¶ms, max_distinct); - assert_eq!(guarantee.metric, ErrorMetric::Cardinality); + assert_eq!(guarantee.spec.metric, ErrorMetric::Relative); assert!( meets(&guarantee, eps, delta), "({eps}, {delta}): {params:?}" @@ -507,7 +513,7 @@ mod tests { fn distinct_guarantee_needs_the_source_bound_and_enough_layers() { let params = size_params(&default_cardinality(), 0.05, 0.01); let guarantee = distinct_guarantee(¶ms, "m").unwrap(); - assert_eq!(guarantee.failure_probability.evaluate(), None); + assert_eq!(guarantee.spec.failure_probability.evaluate(), None); assert!(!meets(&guarantee, 0.05, 0.01)); let SketchParams::UnivMon { heap_size: k, .. } = params else { unreachable!() diff --git a/crates/asap-aware-mapping/src/accuracy/mod.rs b/crates/asap-aware-mapping/src/accuracy/mod.rs index c12f316f8..c82baa749 100644 --- a/crates/asap-aware-mapping/src/accuracy/mod.rs +++ b/crates/asap-aware-mapping/src/accuracy/mod.rs @@ -64,8 +64,11 @@ pub trait AccuracyModel { stats: &PropagationStats, ) -> Result; - /// Compare the dimensions requested by `target`. Unknown required - /// dimensions fail; selection separately excludes missing accuracy evidence. + /// 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. A bound in another metric does not satisfy, + /// however small. Unknown required dimensions fail; selection + /// separately excludes missing accuracy evidence. fn satisfies(&self, guarantee: &AccuracyGuarantee, target: &AccuracyTarget) -> bool; } @@ -113,42 +116,135 @@ impl AccuracyModel for DefaultAccuracyModel { let within = |value: Option, limit: f64| { value.is_some_and(|v| v <= limit * (1.0 + SATISFACTION_TOLERANCE) + f64::EPSILON) }; - match target { - AccuracyTarget::Exact => guarantee.is_exact(), - AccuracyTarget::Epsilon(eps) => within(guarantee.bound.evaluate(), *eps), - AccuracyTarget::EpsilonDelta { epsilon, delta } => { - within(guarantee.bound.evaluate(), *epsilon) - && within(guarantee.failure_probability.evaluate(), *delta) - } + if target.is_exact() || guarantee.is_exact() { + return guarantee.is_exact(); } + metric_meets(guarantee.spec.metric, target.metric) + && within(guarantee.spec.error.evaluate(), target.error) + && target + .failure_probability + .is_none_or(|delta| within(guarantee.spec.failure_probability.evaluate(), delta)) } } +/// Whether an error bound stated in `guarantee` bounds the error `target` +/// states with the same ε, so the two numbers compare directly. The same +/// metric does; so does an L2-scaled bound for an L1-scaled target, since +/// both are scaled by the norm of the one stream the root reads and +/// `‖f‖₂ ≤ ‖f‖₁`. Any other pair needs a conversion backed by evidence +/// (Absolute ↔ Relative needs a bound on the true value's magnitude; a rank +/// error has no distribution-free value error), which the default model +/// does not make: it does not satisfy. The scope is not compared yet: a +/// per-value guarantee is checked against a whole-result target directly +/// until the union bound over the output values (#669b). +fn metric_meets(guarantee: ErrorMetric, target: ErrorMetric) -> bool { + guarantee == target + || (guarantee == ErrorMetric::ABSOLUTE_STREAM_L2 + && target == ErrorMetric::ABSOLUTE_STREAM_L1) +} + +/// `target` restated in `guarantee`'s metric: the check of one summary +/// against the target it was sized for. A summary is sized in its own +/// native terms (#690 §4) and an aggregate intent's metric is not checked: +/// only a query root's metric is, by plan selection. +pub fn native_target(target: &AccuracyTarget, guarantee: &AccuracyGuarantee) -> AccuracyTarget { + target.clone().with_metric(guarantee.spec.metric) +} + pub(crate) use estimators::topk_capacity; #[cfg(test)] mod tests { use super::*; + use asap_types::types::AccuracyScope; fn abs(bound: f64, delta: f64) -> AccuracyGuarantee { - AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Constant { value: bound }, - failure_probability: ProbabilityExpr::Constant { value: delta }, - provenance: vec![], - } + AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Constant { value: bound }, + ProbabilityExpr::Constant { value: delta }, + vec![], + ) } #[test] fn satisfies_is_fail_closed_on_unknowns_and_exact() { - let unknown = AccuracyGuarantee { - bound: BoundExpr::Unknown { - statistic: "x".into(), - }, - ..abs(0.0, 0.0) + let mut unknown = abs(0.0, 0.0); + unknown.spec.error = BoundExpr::Unknown { + statistic: "x".into(), }; - assert!(!DefaultAccuracyModel.satisfies(&unknown, &AccuracyTarget::Epsilon(1.0))); - assert!(!DefaultAccuracyModel.satisfies(&abs(0.0, 0.01), &AccuracyTarget::Exact)); - assert!( - DefaultAccuracyModel.satisfies(&AccuracyGuarantee::exact("x"), &AccuracyTarget::Exact) + assert!(!DefaultAccuracyModel.satisfies(&unknown, &AccuracyTarget::epsilon(1.0))); + assert!(!DefaultAccuracyModel.satisfies(&abs(0.0, 0.01), &AccuracyTarget::exact())); + assert!(DefaultAccuracyModel + .satisfies(&AccuracyGuarantee::exact("x"), &AccuracyTarget::exact())); + } + + fn guarantee(metric: ErrorMetric, bound: f64, delta: f64) -> AccuracyGuarantee { + AccuracyGuarantee::new( + metric, + BoundExpr::Constant { value: bound }, + ProbabilityExpr::Constant { value: delta }, + vec![], + ) + } + + /// A bound in another metric does not satisfy a target, however small + /// its number: e.g. a Count-Min L1-scaled frequency bound never meets a + /// distinct count's relative target, and a rank bound never meets a + /// value target. An L2-scaled bound meets an L1-scaled target of the + /// same stream (`‖f‖₂ ≤ ‖f‖₁`), not the reverse; and an exact value + /// meets every metric. + #[test] + fn satisfies_requires_the_targets_metric() { + let model = DefaultAccuracyModel; + let target = |metric| AccuracyTarget::epsilon_delta(0.5, 0.5).with_metric(metric); + let (l1, l2) = ( + ErrorMetric::ABSOLUTE_STREAM_L1, + ErrorMetric::ABSOLUTE_STREAM_L2, ); + for (bound, wanted, meets) in [ + (l1, l1, true), + (l2, l2, true), + (l2, l1, true), + (l1, l2, false), + (l1, ErrorMetric::Relative, false), + (l1, ErrorMetric::ABSOLUTE, false), + (ErrorMetric::ABSOLUTE, l1, false), + (ErrorMetric::Relative, ErrorMetric::ABSOLUTE, false), + (ErrorMetric::ABSOLUTE, ErrorMetric::Relative, false), + (ErrorMetric::Rank, ErrorMetric::Relative, false), + (ErrorMetric::Relative, ErrorMetric::Rank, false), + (ErrorMetric::Rank, ErrorMetric::Rank, true), + ] { + assert_eq!( + model.satisfies(&guarantee(bound, 0.01, 0.01), &target(wanted)), + meets, + "{bound:?} against {wanted:?}" + ); + } + assert!(model.satisfies(&AccuracyGuarantee::exact("x"), &target(ErrorMetric::Rank))); + } + + /// A target without a δ requirement checks only the error, even when + /// the guarantee's failure probability is unknown. + #[test] + fn a_target_without_delta_ignores_the_failure_probability() { + let mut g = guarantee(ErrorMetric::ABSOLUTE, 0.01, 0.0); + g.spec.failure_probability = ProbabilityExpr::Unknown { + statistic: "x".into(), + }; + assert!(DefaultAccuracyModel.satisfies(&g, &AccuracyTarget::epsilon(0.01))); + assert!(!DefaultAccuracyModel.satisfies(&g, &AccuracyTarget::epsilon_delta(0.01, 0.5))); + } + + /// Pins the current scope behavior: a per-value guarantee is compared + /// with a whole-result target directly. The union bound over the output + /// values (δ_whole = G · δ_per) arrives with #669b, which changes this + /// test. + #[test] + fn per_value_guarantees_meet_whole_result_targets_directly_until_669b() { + let g = guarantee(ErrorMetric::ABSOLUTE, 0.01, 0.01); + assert_eq!(g.spec.scope, AccuracyScope::PerValue); + let target = AccuracyTarget::epsilon_delta(0.01, 0.01); + assert_eq!(target.scope, AccuracyScope::WholeResult); + assert!(DefaultAccuracyModel.satisfies(&g, &target)); } } diff --git a/crates/asap-aware-mapping/src/accuracy/reconciliation.rs b/crates/asap-aware-mapping/src/accuracy/reconciliation.rs index bbeaddf24..8efb2992d 100644 --- a/crates/asap-aware-mapping/src/accuracy/reconciliation.rs +++ b/crates/asap-aware-mapping/src/accuracy/reconciliation.rs @@ -45,7 +45,7 @@ //! sibling-reuse candidates to. //! 3. The one measure is identical **except** for `accuracy` — same variant, //! same `col`/`q`/`k` (see [`same_intent_except_accuracy`]). -//! 4. Neither side's `accuracy` is [`AccuracyTarget::Exact`] (see +//! 4. Neither side's `accuracy` is [`AccuracyTarget::exact()`] (see //! [`dominates`]'s doc for why exact accuracy is excluded rather than //! trivially "always tightest"). //! 5. The tighter candidate's own **output** schema carries a provable @@ -84,7 +84,7 @@ //! a literal Pareto-domination check on the exact numbers a build would be //! sized with. //! -//! `AccuracyTarget::Exact` is deliberately excluded from both sides (see +//! `AccuracyTarget::exact()` is deliberately excluded from both sides (see //! [`dominates`]): `realizations_for_intent` routes it to `exact_realization` //! instead of `sketch_realizations` — a different `Realization` family //! entirely, not just a tighter budget within the same one — so "build once @@ -233,7 +233,7 @@ fn same_intent_except_accuracy(a: &AggIntent, b: &AggIntent) -> bool { /// numbers, and `tighter` dominates `looser` iff neither of its two numbers /// is larger. /// -/// `AccuracyTarget::Exact` on either side always returns `false` — never a +/// `AccuracyTarget::exact()` on either side always returns `false` — never a /// dominator, never dominated. Numerically, `accuracy_budget(Exact)` /// resolves to a budget that would Pareto-dominate everything (zero error), /// but `realizations_for_intent` realizes `Exact` through a wholly @@ -242,7 +242,7 @@ fn same_intent_except_accuracy(a: &AggIntent, b: &AggIntent) -> bool { /// curve — so the numeric comparison alone does not mean what it means for /// two approximate targets. See the module docs for the full reasoning. fn dominates(tighter: &AccuracyTarget, looser: &AccuracyTarget) -> bool { - if matches!(tighter, AccuracyTarget::Exact) || matches!(looser, AccuracyTarget::Exact) { + if tighter.is_exact() || looser.is_exact() { return false; } let (tighter_eps, tighter_delta) = accuracy_budget(tighter); @@ -491,26 +491,20 @@ mod tests { #[test] fn a_smaller_epsilon_dominates_a_larger_one() { assert!(dominates( - &AccuracyTarget::Epsilon(0.01), - &AccuracyTarget::Epsilon(0.05) + &AccuracyTarget::epsilon(0.01), + &AccuracyTarget::epsilon(0.05) )); assert!(!dominates( - &AccuracyTarget::Epsilon(0.05), - &AccuracyTarget::Epsilon(0.01) + &AccuracyTarget::epsilon(0.05), + &AccuracyTarget::epsilon(0.01) )); } #[test] fn epsilon_delta_needs_both_dimensions_at_least_as_tight() { // Smaller epsilon but larger delta: neither Pareto-dominates. - let a = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.1, - }; - let b = AccuracyTarget::EpsilonDelta { - epsilon: 0.05, - delta: 0.01, - }; + let a = AccuracyTarget::epsilon_delta(0.01, 0.1); + let b = AccuracyTarget::epsilon_delta(0.05, 0.01); assert!(!dominates(&a, &b)); assert!(!dominates(&b, &a)); } @@ -518,24 +512,25 @@ mod tests { #[test] fn exact_never_dominates_or_is_dominated() { assert!(!dominates( - &AccuracyTarget::Exact, - &AccuracyTarget::Epsilon(0.5) + &AccuracyTarget::exact(), + &AccuracyTarget::epsilon(0.5) )); assert!(!dominates( - &AccuracyTarget::Epsilon(0.5), - &AccuracyTarget::Exact + &AccuracyTarget::epsilon(0.5), + &AccuracyTarget::exact() + )); + assert!(!dominates( + &AccuracyTarget::exact(), + &AccuracyTarget::exact() )); - assert!(!dominates(&AccuracyTarget::Exact, &AccuracyTarget::Exact)); } #[test] fn equal_resolved_budgets_are_not_strictly_tighter_either_way() { // Same numeric (eps, delta) budget, different AccuracyTarget spelling. - let epsilon_only = AccuracyTarget::Epsilon(0.01); - let equivalent_epsilon_delta = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: crate::realization::DEFAULT_DELTA, - }; + let epsilon_only = AccuracyTarget::epsilon(0.01); + let equivalent_epsilon_delta = + AccuracyTarget::epsilon_delta(0.01, crate::realization::DEFAULT_DELTA); assert!(dominates(&epsilon_only, &equivalent_epsilon_delta)); assert!(dominates(&equivalent_epsilon_delta, &epsilon_only)); assert!(!strictly_tighter(&epsilon_only, &equivalent_epsilon_delta)); @@ -547,8 +542,8 @@ mod tests { #[test] fn looser_quantile_proposes_reading_the_tighter_sibling() { let scan = metric_scan(); - let tight = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let loose = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let tight = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let loose = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let strategy = AccuracyReconciliationStrategy::new(&[Rc::clone(&tight), Rc::clone(&loose)]); @@ -575,8 +570,8 @@ mod tests { // isolation — proves this is actually wired into the round loop // alongside RollupStrategy/TopKLimitReuseStrategy. let scan = metric_scan(); - let tight = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let loose = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let tight = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let loose = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let space = crate::replacement::search_workload(vec![("tight", tight), ("loose", loose)]).unwrap(); @@ -608,7 +603,7 @@ mod tests { AggIntent::Quantile { col: Some(1), q: 0.99, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, &scan, ); @@ -617,7 +612,7 @@ mod tests { AggIntent::Quantile { col: Some(0), q: 0.99, - accuracy: AccuracyTarget::Epsilon(0.05), + accuracy: AccuracyTarget::epsilon(0.05), }, &scan, ); @@ -629,11 +624,11 @@ mod tests { fn topk_intents_differing_only_in_accuracy_are_near_duplicates() { let tight = AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; let loose = AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.02), + accuracy: AccuracyTarget::epsilon(0.02), }; assert!(same_intent_except_accuracy(&tight, &loose)); } @@ -641,13 +636,13 @@ mod tests { #[test] fn different_grouping_is_not_a_near_duplicate() { let scan = metric_scan(); - let tight = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); + let tight = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); let loose = agg( vec![], AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Epsilon(0.05), + accuracy: AccuracyTarget::epsilon(0.05), }, &scan, ); @@ -658,8 +653,8 @@ mod tests { #[test] fn exact_accuracy_is_never_reconciled() { let scan = metric_scan(); - let exact = quantile(0.99, AccuracyTarget::Exact, &scan); - let approx = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let exact = quantile(0.99, AccuracyTarget::exact(), &scan); + let approx = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let strategy = AccuracyReconciliationStrategy::new(&[Rc::clone(&exact), Rc::clone(&approx)]); assert!(!strategy.matches(&TargetSubDAG::new(&approx))); @@ -677,8 +672,8 @@ mod tests { // is the *only* place cross-accuracy sharing gets proposed, never // `share_common_sub_dags` itself. let scan = metric_scan(); - let a = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let b = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let a = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let b = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let roots = share_common_sub_dags(vec![("a", a), ("b", b)]); assert!( @@ -708,8 +703,8 @@ mod tests { // exact equality — unrelated to this module, but pins the contrast // with the test above. let scan = metric_scan(); - let a = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let b = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); + let a = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let b = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); let roots = share_common_sub_dags(vec![("a", a), ("b", b)]); assert!(Rc::ptr_eq(&roots[0].1, &roots[1].1)); @@ -725,8 +720,8 @@ mod tests { // `share_common_sub_dags`/`RollupStrategy` apply, which this // strategy must not bypass (module docs, point 5). let scan = metric_scan(); - let tight = ungrouped_quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let loose = ungrouped_quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let tight = ungrouped_quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let loose = ungrouped_quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); assert!( !tight.schema.clone().has_unique_key(), @@ -745,8 +740,8 @@ mod tests { // `without(...)` grouping never carries a provable unique key, // regardless of the excluded set. let scan = metric_scan(); - let tight = without_quantile(0.99, AccuracyTarget::Epsilon(0.01), vec![2], &scan); - let loose = without_quantile(0.99, AccuracyTarget::Epsilon(0.05), vec![2], &scan); + let tight = without_quantile(0.99, AccuracyTarget::epsilon(0.01), vec![2], &scan); + let loose = without_quantile(0.99, AccuracyTarget::epsilon(0.05), vec![2], &scan); assert!( !tight.schema.clone().has_unique_key(), @@ -772,8 +767,8 @@ mod tests { // `rc`'s own build," which must not scale with `target`'s own // `consumer_count`. let scan = metric_scan(); - let tight = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let loose = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let tight = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let loose = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let strategy = AccuracyReconciliationStrategy::new(&[Rc::clone(&tight), Rc::clone(&loose)]); let candidate = strategy .replacements(&TargetSubDAG::new(&loose)) @@ -806,8 +801,8 @@ mod tests { #[test] fn cost_sorted_and_global_selection_handle_a_single_consumer_looser_target() { let scan = metric_scan(); - let tight = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let loose = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let tight = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let loose = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let space = crate::replacement::search_workload(vec![("tight", tight), ("loose", loose)]).unwrap(); let loose_root = &space.roots[1].1; @@ -862,9 +857,9 @@ mod tests { // own 2 consumers at the same time as this strategy's "read the // tight sibling instead" candidate. let scan = metric_scan(); - let loose_a = (*quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan)).clone(); - let loose_b = (*quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan)).clone(); - let tight = (*quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan)).clone(); + let loose_a = (*quantile(0.99, AccuracyTarget::epsilon(0.05), &scan)).clone(); + let loose_b = (*quantile(0.99, AccuracyTarget::epsilon(0.05), &scan)).clone(); + let tight = (*quantile(0.99, AccuracyTarget::epsilon(0.01), &scan)).clone(); let space = crate::replacement::search_workload(vec![ ("loose_a", Rc::new(loose_a)), @@ -957,8 +952,8 @@ mod tests { } let scan = metric_scan(); - let tight = quantile(0.99, AccuracyTarget::Epsilon(0.01), &scan); - let loose = quantile(0.99, AccuracyTarget::Epsilon(0.05), &scan); + let tight = quantile(0.99, AccuracyTarget::epsilon(0.01), &scan); + let loose = quantile(0.99, AccuracyTarget::epsilon(0.05), &scan); let space = crate::replacement::search_workload(vec![ ("tight", Rc::clone(&tight)), ("loose", Rc::clone(&loose)), diff --git a/crates/asap-aware-mapping/src/asap_aware_cse.rs b/crates/asap-aware-mapping/src/asap_aware_cse.rs index 5278ccb1d..359dccf1a 100644 --- a/crates/asap-aware-mapping/src/asap_aware_cse.rs +++ b/crates/asap-aware-mapping/src/asap_aware_cse.rs @@ -291,13 +291,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::AccuracyTarget; + use asap_types::types::RequestedAccuracy; fn roots(queries: &[&str]) -> Vec { queries .iter() .map(|q| { - let root = lower_promql(q, AccuracyTarget::Exact); + let root = lower_promql(q, RequestedAccuracy::exact()); QueryRoot::Operator(with_promql_series_identity(&root).unwrap()) }) .collect() diff --git a/crates/asap-aware-mapping/src/empirical_cost.rs b/crates/asap-aware-mapping/src/empirical_cost.rs index 3a4e54447..7f044a4bc 100644 --- a/crates/asap-aware-mapping/src/empirical_cost.rs +++ b/crates/asap-aware-mapping/src/empirical_cost.rs @@ -468,10 +468,7 @@ mod tests { implementation_version: "test-v1".into(), }; let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let records = [ (SketchAlgorithm::Cms, 20.0), diff --git a/crates/asap-aware-mapping/src/exact_composition.rs b/crates/asap-aware-mapping/src/exact_composition.rs index 73856f84b..d001cdb85 100644 --- a/crates/asap-aware-mapping/src/exact_composition.rs +++ b/crates/asap-aware-mapping/src/exact_composition.rs @@ -75,7 +75,6 @@ use asap_types::post_asap::{ ExecutionDataStateError, Schema, }; use asap_types::pre_asap::agg_intent::AggIntent; -use asap_types::types::AccuracyTarget; use crate::cost_model::CostModel; use crate::replacement::{ @@ -278,18 +277,16 @@ impl ExactComposition { /// `Count` only at `Exact` accuracy (an approximate count is a sketch /// target, not an exact fold). fn is_query_time_reducer(intent: &AggIntent) -> bool { - matches!( - intent, + match intent { AggIntent::Sum { .. } - | AggIntent::Min { .. } - | AggIntent::Max { .. } - | AggIntent::Avg { .. } - | AggIntent::StdDev { .. } - | AggIntent::Variance { .. } - | AggIntent::Count { - accuracy: AccuracyTarget::Exact - } - ) + | AggIntent::Min { .. } + | AggIntent::Max { .. } + | AggIntent::Avg { .. } + | AggIntent::StdDev { .. } + | AggIntent::Variance { .. } => true, + AggIntent::Count { accuracy } => accuracy.is_exact(), + _ => false, + } } /// Does `implementation` need a `SummaryEstimate` evaluation to yield a value diff --git a/crates/asap-aware-mapping/src/explanation.rs b/crates/asap-aware-mapping/src/explanation.rs index 8297a98dd..ec1227e42 100644 --- a/crates/asap-aware-mapping/src/explanation.rs +++ b/crates/asap-aware-mapping/src/explanation.rs @@ -645,7 +645,7 @@ mod tests { AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, metric_scan(&["job"]), ); diff --git a/crates/asap-aware-mapping/src/grouping.rs b/crates/asap-aware-mapping/src/grouping.rs index 560becc39..1c9eba931 100644 --- a/crates/asap-aware-mapping/src/grouping.rs +++ b/crates/asap-aware-mapping/src/grouping.rs @@ -267,7 +267,7 @@ impl<'a> HydraGroupingStrategy<'a> { input_metrics: node .guarantee .as_ref() - .map_or_else(Vec::new, |g| vec![g.metric]), + .map_or_else(Vec::new, |g| vec![g.spec.metric]), local_metric: None, reason: "invalid Hydra shared-grid collision or failure-probability evidence" .into(), @@ -277,18 +277,17 @@ impl<'a> HydraGroupingStrategy<'a> { } let patched = with_grouping(node, grouping, &stats); if let (Some(target), Some(guarantee)) = (accuracy_target(intent), &patched.guarantee) { - if !self - .planning_inputs - .accuracy - .satisfies(&guarantee.optimistic_floor(), target) - { + if !self.planning_inputs.accuracy.satisfies( + &guarantee.optimistic_floor(), + &crate::accuracy::native_target(target, guarantee), + ) { rejected.push(RejectedCandidate { strategy: "HydraGroupingStrategy", description: format!("{hydra_kind:?} over {sketch_kind:?}"), error: AccuracyError::TargetNotSatisfied { - metric: guarantee.metric, - bound: guarantee.bound.evaluate(), - failure_probability: guarantee.failure_probability.evaluate(), + metric: guarantee.spec.metric, + bound: guarantee.spec.error.evaluate(), + failure_probability: guarantee.spec.failure_probability.evaluate(), target: target.clone(), }, }); @@ -466,11 +465,11 @@ fn hydra_guarantee(inner: &AccuracyGuarantee, stats: &PropagationStats) -> Accur operator: CompositionOperator::ApproximateAggregate, rule: "hydra_shared_grid_union_bound".into(), }); - AccuracyGuarantee { - metric: inner.metric, - bound: BoundExpr::Sum { + AccuracyGuarantee::new( + inner.spec.metric, + BoundExpr::Sum { terms: vec![ - inner.bound.clone(), + inner.spec.error.clone(), stats.hydra_shared_grid_collision_bound.map_or_else( || BoundExpr::Unknown { statistic: "hydra_shared_grid_collision_bound".into(), @@ -479,9 +478,9 @@ fn hydra_guarantee(inner: &AccuracyGuarantee, stats: &PropagationStats) -> Accur ), ], }, - failure_probability: ProbabilityExpr::UnionBound { + ProbabilityExpr::UnionBound { terms: vec![ - inner.failure_probability.clone(), + inner.spec.failure_probability.clone(), stats.hydra_shared_grid_failure_probability.map_or_else( || ProbabilityExpr::Unknown { statistic: "hydra_shared_grid_failure_probability".into(), @@ -491,7 +490,7 @@ fn hydra_guarantee(inner: &AccuracyGuarantee, stats: &PropagationStats) -> Accur ], }, provenance, - } + ) } #[cfg(test)] @@ -532,17 +531,17 @@ mod tests { #[test] fn hydra_composes_inner_and_shared_grid_error_symbolically() { - let inner = AccuracyGuarantee { - metric: ErrorMetric::Frequency, - bound: BoundExpr::Constant { value: 0.01 }, - failure_probability: ProbabilityExpr::Constant { value: 0.02 }, - provenance: vec![], - }; + let inner = AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE_STREAM_L1, + BoundExpr::Constant { value: 0.01 }, + ProbabilityExpr::Constant { value: 0.02 }, + vec![], + ); let composed = hydra_guarantee(&inner, &PropagationStats::default()); - assert_eq!(composed.metric, ErrorMetric::Frequency); + assert_eq!(composed.spec.metric, ErrorMetric::ABSOLUTE_STREAM_L1); assert!(matches!( - composed.bound, + composed.spec.error, BoundExpr::Sum { ref terms } if matches!(terms.as_slice(), [ BoundExpr::Constant { value }, @@ -550,7 +549,7 @@ mod tests { ] if *value == 0.01 && statistic == "hydra_shared_grid_collision_bound") )); assert!(matches!( - composed.failure_probability, + composed.spec.failure_probability, ProbabilityExpr::UnionBound { ref terms } if matches!(terms.as_slice(), [ ProbabilityExpr::Constant { value }, @@ -564,10 +563,7 @@ mod tests { #[test] fn matches_a_grouped_count_with_an_unprovable_accuracy_target() { let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let q = agg(vec![2], intent, metric_scan(&["job"])); let target = TargetSubDAG::new(&q); @@ -611,10 +607,7 @@ mod tests { #[test] fn count_with_an_accuracy_target_keeps_uncertified_hydra_candidates() { let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let q = agg(vec![2], intent, metric_scan(&["job"])); let target = TargetSubDAG::new(&q); @@ -624,8 +617,8 @@ mod tests { &candidate.replacement, Replacement::SubDAG(node) if node.guarantee.as_ref().is_some_and(|guarantee| - guarantee.bound.evaluate().is_none() - && guarantee.failure_probability.evaluate().is_none()) + guarantee.spec.error.evaluate().is_none() + && guarantee.spec.failure_probability.evaluate().is_none()) ))); } @@ -649,10 +642,7 @@ mod tests { #[test] fn hydra_shared_grid_evidence_is_consumed_by_candidate_construction() { let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let q = agg(vec![2], intent, metric_scan(&["job"])); let strategy = HydraGroupingStrategy::new_with_planning_inputs_and_evidence( @@ -667,8 +657,8 @@ mod tests { &candidate.replacement, Replacement::SubDAG(node) if node.guarantee.as_ref().is_some_and(|g| - g.bound.evaluate().is_some() - && g.failure_probability.evaluate().is_some()) + g.spec.error.evaluate().is_some() + && g.spec.failure_probability.evaluate().is_some()) ))); } @@ -689,10 +679,7 @@ mod tests { } } let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let q = agg(vec![2], intent, metric_scan(&["job"])); let strategy = HydraGroupingStrategy::new_with_planning_inputs_and_evidence( @@ -738,10 +725,7 @@ mod tests { let q = agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }, metric_scan(&["job"]), ); @@ -774,12 +758,12 @@ mod tests { #[test] fn exact_accuracy_target_has_no_hydra_candidate() { - // AccuracyTarget::Exact never binds a sketch at all — nothing for + // AccuracyTarget::exact() never binds a sketch at all — nothing for // this axis to offer a shared-structure alternative to. let intent = AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; let q = agg(vec![2], intent, metric_scan(&["job"])); let target = TargetSubDAG::new(&q); diff --git a/crates/asap-aware-mapping/src/maintained_population.rs b/crates/asap-aware-mapping/src/maintained_population.rs index 258915b7c..05b3cc48b 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::AccuracyTarget::Exact) + lower_promql(q, asap_types::types::RequestedAccuracy::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 4292b703e..458e0fce8 100644 --- a/crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs +++ b/crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs @@ -30,7 +30,7 @@ use std::rc::Rc; use asap_types::ir::schema_support::with_promql_series_identity; use asap_types::ir::{OperatorNode, QueryRoot}; use asap_types::post_asap::SketchAlgorithm; -use asap_types::types::AccuracyTarget; +use asap_types::types::{AccuracyTarget, RequestedAccuracy}; use asap_types::workload::{DataWorkload, QueryLanguage, QueryRecurrence}; use super::{OptimizationInput, OptimizationPass, OptimizeError, PlanOutput, QueryPlan}; @@ -135,7 +135,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<'_>, @@ -233,7 +233,7 @@ pub fn stage1_logical_asap_optimization( /// enumerating. pub fn run_stage1_to_stage3_from_pass1( per_computation: PerComputationCandidates, - targets: &[Option], + targets: &[Option], recurrence: &[QueryRecurrence], data: &DataWorkload, models: PlanningModels<'_>, @@ -248,7 +248,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<'_>, @@ -289,7 +289,8 @@ pub fn run_stage2_and_stage3( roots: candidate.roots, }); } - let selection = select_plan(&physical, targets, recurrence, data, models); + let selection = resolve_targets(&per_computation.roots, targets) + .and_then(|targets| select_plan(&physical, &targets, recurrence, data, models)); let selected_logical = selection.as_ref().ok().and_then(|selection| { let from = &physical .iter() @@ -317,6 +318,30 @@ 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 { @@ -467,7 +492,7 @@ impl OptimizationPass for Stage1ToStage3Pass { let targets: Vec<_> = workload .query_workload() .entries() - .map(|entry| Some(entry.requirements.accuracy.target())) + .map(|entry| Some(entry.requirements.accuracy.requested())) .collect(); let recurrence: Vec<_> = workload .query_workload() diff --git a/crates/asap-aware-mapping/src/per_computation_candidates.rs b/crates/asap-aware-mapping/src/per_computation_candidates.rs index 8879644f4..71457a40d 100644 --- a/crates/asap-aware-mapping/src/per_computation_candidates.rs +++ b/crates/asap-aware-mapping/src/per_computation_candidates.rs @@ -335,7 +335,7 @@ pub fn computation_candidates_for_intent( pruned: vec![], }; if let Some(target) = accuracy_target(intent) { - if *target != AccuracyTarget::Exact { + if !target.is_exact() { let (epsilon, delta) = accuracy_budget(target); let unit = |value: f64| value > 0.0 && value < 1.0; if !unit(epsilon) || !unit(delta) { @@ -346,7 +346,7 @@ pub fn computation_candidates_for_intent( } } let summaries = summary_candidates(intent).iter().cloned(); - if *target == AccuracyTarget::Exact { + if target.is_exact() { choices.pruned.extend(summaries.map(|algorithm| { ( ComputationCandidate::Summary(algorithm), @@ -1480,7 +1480,7 @@ pub(crate) fn arithmetic_guarantee( additive_as_absolute(left.0?, None), additive_as_absolute(right.0?, None), ); - let once = same_operand.then(|| l.failure_probability.clone()); + let once = same_operand.then(|| l.spec.failure_probability.clone()); let statistic = operand_domain_statistic(&op, &l, &r); let composed = match (op, l.is_exact(), r.is_exact()) { (_, true, true) => return Some(AccuracyGuarantee::exact("arithmetic over exact operands")), @@ -1501,13 +1501,11 @@ pub(crate) fn arithmetic_guarantee( (ArithmeticOpKind::Mul | ArithmeticOpKind::Div, _, _) => symbolic(l, r, statistic?), _ => None, }?; - Some(match once { - Some(failure_probability) => AccuracyGuarantee { - failure_probability, - ..composed - }, - None => composed, - }) + let mut composed = composed; + if let Some(failure_probability) = once { + composed.spec.failure_probability = failure_probability; + } + Some(composed) } /// The statistic a product or quotient of `l` and `r` leaves symbolic on @@ -1525,13 +1523,9 @@ pub(crate) fn operand_domain_statistic( r: &AccuracyGuarantee, ) -> Option<&'static str> { let valued = |g: &AccuracyGuarantee| { - g.is_exact() - || matches!( - g.metric, - ErrorMetric::AbsoluteValue | ErrorMetric::RelativeValue | ErrorMetric::Cardinality - ) + g.is_exact() || matches!(g.spec.metric, ErrorMetric::ABSOLUTE | ErrorMetric::Relative) }; - let relative = |g: &AccuracyGuarantee| g.is_exact() || g.metric == ErrorMetric::RelativeValue; + let relative = |g: &AccuracyGuarantee| g.is_exact() || g.spec.metric == ErrorMetric::Relative; if !(valued(l) && valued(r)) { return None; } @@ -1539,8 +1533,8 @@ pub(crate) fn operand_domain_statistic( ArithmeticOpKind::Mul if !l.is_exact() && !r.is_exact() - && !(l.metric == ErrorMetric::RelativeValue - && r.metric == ErrorMetric::RelativeValue) => + && !(l.spec.metric == ErrorMetric::Relative + && r.spec.metric == ErrorMetric::Relative) => { Some(PRODUCT_OPERAND_DOMAINS) } @@ -1557,20 +1551,20 @@ pub(crate) fn operand_domain_statistic( /// `(1 + a)(1 + b) − 1 = a + b + a·b`, whatever the signs. `None` for any /// other pair. fn relative_product(l: AccuracyGuarantee, r: AccuracyGuarantee) -> Option { - if l.metric != ErrorMetric::RelativeValue || r.metric != ErrorMetric::RelativeValue { + if l.spec.metric != ErrorMetric::Relative || r.spec.metric != ErrorMetric::Relative { return None; } let bound = BoundExpr::Sum { terms: vec![ - l.bound.clone(), - r.bound.clone(), + l.spec.error.clone(), + r.spec.error.clone(), BoundExpr::Product { - factors: vec![l.bound.clone(), r.bound.clone()], + factors: vec![l.spec.error.clone(), r.spec.error.clone()], }, ], }; Some(binary_guarantee( - ErrorMetric::RelativeValue, + ErrorMetric::Relative, bound, l, r, @@ -1583,8 +1577,8 @@ fn relative_product(l: AccuracyGuarantee, r: AccuracyGuarantee) -> Option) -> Option { - match (input.metric, factor) { - (ErrorMetric::RelativeValue, _) => Some(input), + match (input.spec.metric, factor) { + (ErrorMetric::Relative, _) => Some(input), (_, Some(constant)) => compose( &CompositionOperator::Lipschitz { constant }, vec![input], @@ -1614,17 +1608,17 @@ fn symbolic( ) -> Option { let (metric, operator, rule) = match statistic { DIVISION_OPERAND_DOMAINS => ( - ErrorMetric::RelativeValue, + ErrorMetric::Relative, CompositionOperator::ExactDivision, "relative_division_needs_operand_domains", ), QUOTIENT_OPERAND_DOMAINS => ( - ErrorMetric::AbsoluteValue, + ErrorMetric::ABSOLUTE, CompositionOperator::ExactDivision, "absolute_division_needs_operand_domains", ), _ => ( - ErrorMetric::AbsoluteValue, + ErrorMetric::ABSOLUTE, CompositionOperator::ExactProduct, "absolute_product_needs_operand_domains", ), @@ -1654,7 +1648,10 @@ fn binary_guarantee( rule: &str, ) -> AccuracyGuarantee { let failure_probability = ProbabilityExpr::UnionBound { - terms: vec![l.failure_probability.clone(), r.failure_probability.clone()], + terms: vec![ + l.spec.failure_probability.clone(), + r.spec.failure_probability.clone(), + ], }; let provenance = [l, r] .into_iter() @@ -1673,12 +1670,7 @@ fn binary_guarantee( rule: rule.into(), }]) .collect(); - AccuracyGuarantee { - metric, - bound, - failure_probability, - provenance, - } + AccuracyGuarantee::new(metric, bound, failure_probability, provenance) } /// Whether the default accuracy model's bound for `family` answers `query` @@ -1720,18 +1712,16 @@ fn estimate_bound_over_samples( if !estimate_has_bound(family, &proven, query) { return None; } - let local = DefaultAccuracyModel.local_guarantee(family, query)?; - Some(AccuracyGuarantee { - bound: asap_types::post_asap::BoundExpr::Product { - factors: vec![ - local.bound, - asap_types::post_asap::BoundExpr::Unknown { - statistic: samples_sign_statistic(metric), - }, - ], - }, - ..local - }) + let mut local = DefaultAccuracyModel.local_guarantee(family, query)?; + local.spec.error = asap_types::post_asap::BoundExpr::Product { + factors: vec![ + local.spec.error, + asap_types::post_asap::BoundExpr::Unknown { + statistic: samples_sign_statistic(metric), + }, + ], + }; + Some(local) } /// Replace an aggregate whose measures are `targets`' (or exact, for a @@ -1958,7 +1948,7 @@ fn substitute_measure( ExactKind::Count, ExactParams::Count, &AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, read, )?; @@ -2182,7 +2172,7 @@ fn key_items(keys: usize) -> Vec { /// `algorithm` sized for `intent`'s accuracy requirement. fn sized(algorithm: &SketchAlgorithm, intent: &AggIntent) -> SketchKind { let (epsilon, delta) = - accuracy_budget(accuracy_target(intent).unwrap_or(&AccuracyTarget::Exact)); + accuracy_budget(accuracy_target(intent).unwrap_or(&AccuracyTarget::exact())); SketchKind::new( algorithm.clone(), default_size_params(algorithm.clone(), intent, epsilon, delta), @@ -2383,15 +2373,13 @@ fn statistic(intent: &AggIntent) -> Result AccuracyGuarantee { - AccuracyGuarantee { + AccuracyGuarantee::new( metric, - bound: BoundExpr::Constant { value: bound }, - failure_probability: ProbabilityExpr::Constant { value: delta }, - provenance: vec![], - } + BoundExpr::Constant { value: bound }, + ProbabilityExpr::Constant { value: delta }, + vec![], + ) } fn abs(bound: f64, delta: f64) -> AccuracyGuarantee { - with_bound(ErrorMetric::AbsoluteValue, bound, delta) + with_bound(ErrorMetric::ABSOLUTE, bound, delta) } fn rel(bound: f64) -> AccuracyGuarantee { - with_bound(ErrorMetric::RelativeValue, bound, 0.0) + with_bound(ErrorMetric::Relative, bound, 0.0) } /// `sum(m)` carrying `guarantee`. fn node(guarantee: AccuracyGuarantee) -> Rc { @@ -2431,7 +2420,7 @@ mod tests { } /// `query` carrying `guarantee`. fn node_over(query: &str, guarantee: AccuracyGuarantee) -> Rc { - let node = lower_promql(query, AccuracyTarget::Exact); + let node = lower_promql(query, RequestedAccuracy::exact()); Rc::new((*node).clone().with_guarantee(Some(guarantee))) } fn scalar(guarantee: AccuracyGuarantee) -> ScalarExpr { @@ -2458,10 +2447,11 @@ mod tests { scalar_guarantee(expr, &|_| None) } fn bound(g: &AccuracyGuarantee) -> f64 { - g.bound.evaluate().expect("a resolved bound") + g.spec.error.evaluate().expect("a resolved bound") } fn delta(g: &AccuracyGuarantee) -> f64 { - g.failure_probability + g.spec + .failure_probability .evaluate() .expect("a resolved probability") } @@ -2479,7 +2469,7 @@ mod tests { let (x, y) = (scalar(abs(0.01, 0.001)), scalar(abs(0.02, 0.002))); for op in [Add, Sub] { let g = of(&arith(op.clone(), x.clone(), y.clone())).unwrap(); - assert_eq!(g.metric, ErrorMetric::AbsoluteValue, "{op:?}"); + assert_eq!(g.spec.metric, ErrorMetric::ABSOLUTE, "{op:?}"); assert!(close(bound(&g), 0.03) && close(delta(&g), 0.003), "{op:?}"); for shifted in [ arith(op.clone(), x.clone(), lit(5.0)), @@ -2521,8 +2511,8 @@ mod tests { /// over each source's raw value column, for plan selection to bind. #[test] fn normalized_additive_errors_compose_as_absolute_errors() { - let cms = with_bound(ErrorMetric::Frequency, 0.01, 0.001); - let count_sketch = with_bound(ErrorMetric::L2Frequency, 0.02, 0.002); + let cms = with_bound(ErrorMetric::ABSOLUTE_STREAM_L1, 0.01, 0.001); + let count_sketch = with_bound(ErrorMetric::ABSOLUTE_STREAM_L2, 0.02, 0.002); // Summaries of each source's raw value column. let raw = |query: &str, guarantee: AccuracyGuarantee| { ScalarExpr::PromqlScalarFromVector(crate::test_support::estimate_over( @@ -2539,22 +2529,25 @@ mod tests { raw("count by (job) (n)", count_sketch), )) .unwrap(); - assert_eq!(g.metric, ErrorMetric::AbsoluteValue); + assert_eq!(g.spec.metric, ErrorMetric::ABSOLUTE); assert!(close(delta(&g), 0.003)); let norms = |statistic: &str| match statistic { "stream_l1_norm[m]" => Some(100.0), "stream_l2_norm[n]" => Some(10.0), _ => None, }; - assert!(g.bound.has_unknown()); - assert!(close(g.bind(&norms).bound.evaluate().unwrap(), 1.2)); + assert!(g.spec.error.has_unknown()); + assert!(close(g.bind(&norms).spec.error.evaluate().unwrap(), 1.2)); let doubled = of(&arith( ArithmeticOpKind::Mul, lit(2.0), raw("count by (job) (m)", cms), )) .unwrap(); - assert!(close(doubled.bind(&norms).bound.evaluate().unwrap(), 2.0)); + assert!(close( + doubled.bind(&norms).spec.error.evaluate().unwrap(), + 2.0 + )); } /// A product of two estimates has an absolute bound @@ -2573,8 +2566,8 @@ mod tests { }; let (x, y) = (scalar(abs(0.01, 0.001)), scalar(abs(0.02, 0.002))); let product = of(&arith(Mul, x.clone(), y.clone())).unwrap(); - assert_eq!(product.metric, ErrorMetric::AbsoluteValue); - assert_eq!(product.bound, unknown(PRODUCT_OPERAND_DOMAINS)); + assert_eq!(product.spec.metric, ErrorMetric::ABSOLUTE); + assert_eq!(product.spec.error, unknown(PRODUCT_OPERAND_DOMAINS)); assert!(close(delta(&product), 0.003)); for quotient in [ arith(Div, x.clone(), y.clone()), @@ -2582,14 +2575,18 @@ mod tests { arith(Div, x.clone(), scalar(rel(0.01))), ] { let g = of("ient).unwrap(); - assert_eq!(g.metric, ErrorMetric::AbsoluteValue, "{quotient:?}"); - assert_eq!(g.bound, unknown(QUOTIENT_OPERAND_DOMAINS), "{quotient:?}"); + assert_eq!(g.spec.metric, ErrorMetric::ABSOLUTE, "{quotient:?}"); + assert_eq!( + g.spec.error, + unknown(QUOTIENT_OPERAND_DOMAINS), + "{quotient:?}" + ); } let ratio = of(&arith(Div, scalar(rel(0.01)), scalar(rel(0.02)))).unwrap(); - assert_eq!(ratio.metric, ErrorMetric::RelativeValue); - assert_eq!(ratio.bound, unknown(DIVISION_OPERAND_DOMAINS)); + assert_eq!(ratio.spec.metric, ErrorMetric::Relative); + assert_eq!(ratio.spec.error, unknown(DIVISION_OPERAND_DOMAINS)); let relative = of(&arith(Mul, scalar(rel(0.01)), scalar(rel(0.02)))).unwrap(); - assert_eq!(relative.metric, ErrorMetric::RelativeValue); + assert_eq!(relative.spec.metric, ErrorMetric::Relative); assert!(close(bound(&relative), 0.01 + 0.02 + 0.0002)); let rank = scalar(with_bound(ErrorMetric::Rank, 0.01, 0.001)); assert_eq!(of(&arith(Mul, rank.clone(), x.clone())), None); @@ -2613,7 +2610,7 @@ mod tests { arith(Div, x.clone(), ScalarExpr::EvalTimestamp), ] { let g = of(&kept).unwrap(); - assert_eq!(g.metric, ErrorMetric::RelativeValue, "{kept:?}"); + assert_eq!(g.spec.metric, ErrorMetric::Relative, "{kept:?}"); assert!(close(bound(&g), 0.01), "{kept:?}"); } for none in [ @@ -2647,7 +2644,7 @@ mod tests { call("abs", vec![x.clone()]), ] { let g = of(&expr).unwrap(); - assert_eq!(g.metric, ErrorMetric::AbsoluteValue, "{expr:?}"); + assert_eq!(g.spec.metric, ErrorMetric::ABSOLUTE, "{expr:?}"); assert!( close(bound(&g), 0.01) && close(delta(&g), 0.001), "{expr:?}" @@ -2664,7 +2661,7 @@ mod tests { vec![scalar(rel(0.01)), scalar(rel(0.02)), lit(10.0)], )) .unwrap(); - assert_eq!(relative.metric, ErrorMetric::RelativeValue); + assert_eq!(relative.spec.metric, ErrorMetric::Relative); assert!(close(bound(&relative), 0.02)); let mixed = call("promql_clamp_min", vec![x, scalar(rel(0.02))]); assert_eq!(of(&mixed), None); @@ -2780,12 +2777,12 @@ mod tests { /// column, the weight times the source's row count for a count. #[test] fn the_absolute_bound_names_the_weighted_norm() { - let frequency = AccuracyGuarantee { - metric: ErrorMetric::Frequency, - bound: BoundExpr::Constant { value: 0.01 }, - failure_probability: ProbabilityExpr::Constant { value: 0.001 }, - provenance: vec![], - }; + let frequency = AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE_STREAM_L1, + BoundExpr::Constant { value: 0.01 }, + ProbabilityExpr::Constant { value: 0.001 }, + vec![], + ); let estimate = |summary: Rc| { operand_guarantee(&crate::test_support::estimate_over( summary, @@ -2799,19 +2796,24 @@ mod tests { )); let l1 = source_norm_statistic(STREAM_L1_NORM, "m"); assert_eq!( - raw.bind(&|s| (s == l1).then_some(100.0)).bound.evaluate(), + raw.bind(&|s| (s == l1).then_some(100.0)) + .spec + .error + .evaluate(), Some(1.0) ); let count = estimate(Rc::new(cms_summary("count by (job) (m)"))); assert!(count .bind(&|s| (s == l1).then_some(100.0)) - .bound + .spec + .error .has_unknown()); let rows = source_norm_statistic(STREAM_ROW_COUNT, "m"); assert_eq!( count .bind(&|s| (s == rows).then_some(100.0)) - .bound + .spec + .error .evaluate(), Some(1.0) ); @@ -2819,13 +2821,8 @@ mod tests { } fn inventory(query: &str) -> PerComputationCandidates { - let root = crate::test_support::lower_promql( - query, - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }, - ); + let root = + crate::test_support::lower_promql(query, RequestedAccuracy::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 = @@ -2938,7 +2935,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])))", - AccuracyTarget::Epsilon(0.1), + RequestedAccuracy::epsilon(0.1), ); let inventory = generate_per_computation_candidates(vec![(0, QueryRoot::Operator(root))]).unwrap(); @@ -3020,7 +3017,7 @@ mod tests { ("rate(m[1m])", None), ("max_over_time(m[1m])", None), ] { - let root = crate::test_support::lower_promql(query, AccuracyTarget::Exact); + let root = crate::test_support::lower_promql(query, RequestedAccuracy::exact()); assert_eq!(samples_of_metric(&root).as_deref(), metric, "{query}"); } } @@ -3190,19 +3187,20 @@ mod tests { weight_domain: WeightDomain::SamplesOfMetric { metric: "m".into() }, }; let gated = estimate_bound_over_samples(&family, &update, &query).expect("gated"); - assert_eq!(gated.bound.evaluate(), None); + assert_eq!(gated.spec.error.evaluate(), None); let local = DefaultAccuracyModel .local_guarantee(&family, &query) .unwrap(); let statistic = samples_sign_statistic("m"); // Every other statistic (a stream norm) bound alike. let others = |s: &str| Some(if s == statistic { 1.0 } else { 1_000.0 }); - let bound = gated.bind(&others).bound.evaluate(); + let bound = gated.bind(&others).spec.error.evaluate(); assert!(bound.is_some()); - assert_eq!(bound, local.bind(&others).bound.evaluate()); + assert_eq!(bound, local.bind(&others).spec.error.evaluate()); let unconfirmed = gated .bind(&|s| (s != statistic).then_some(1_000.0)) - .bound + .spec + .error .evaluate(); assert_eq!(unconfirmed, None); } diff --git a/crates/asap-aware-mapping/src/physical_plan_cost_model.rs b/crates/asap-aware-mapping/src/physical_plan_cost_model.rs index defabfb01..670f77882 100644 --- a/crates/asap-aware-mapping/src/physical_plan_cost_model.rs +++ b/crates/asap-aware-mapping/src/physical_plan_cost_model.rs @@ -418,7 +418,7 @@ mod tests { OperatorNode::new_shared(asap_types::ir::Operator::NonASAP(NonASAPOp::Aggregate { reduction: Reduction::by(vec![]), measures: vec![AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }], output_names: vec![], filters: vec![], diff --git a/crates/asap-aware-mapping/src/query_physical_lowering.rs b/crates/asap-aware-mapping/src/query_physical_lowering.rs index 150104a04..bb7f914e5 100644 --- a/crates/asap-aware-mapping/src/query_physical_lowering.rs +++ b/crates/asap-aware-mapping/src/query_physical_lowering.rs @@ -1315,6 +1315,7 @@ mod tests { PromqlEdgeStatistics, PromqlUnaryEdgeStatistics, PromqlValueKind, UnaryEdgeStatistics, }; use asap_types::ir::{BinaryOperator, ExprSemantics, Predicate, SortKey, TimeRangeKind}; + use asap_types::types::RequestedAccuracy; use asap_types::workload::{ DataArrival, DurationMs, QueryRecurrence, QueryTimeScope, TimeSelection, TimestampMs, }; @@ -2341,7 +2342,7 @@ mod tests { measures: vec![AggIntent::Quantile { col: Some(0), q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }], output_names: vec![], filters: vec![], @@ -2797,7 +2798,6 @@ mod tests { scalar_inputs: usize, ) { use crate::physical_operator_statistics::ScalarInputStatistics; - use asap_types::types::AccuracyTarget; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, @@ -2810,7 +2810,7 @@ mod tests { query_batch: Some(vec![BatchEntry { query: Query(query.into()), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::Exact), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), ..Default::default() }, predictability: Predictability::Unknown, diff --git a/crates/asap-aware-mapping/src/realization.rs b/crates/asap-aware-mapping/src/realization.rs index 0ab6b942e..98d890f7f 100644 --- a/crates/asap-aware-mapping/src/realization.rs +++ b/crates/asap-aware-mapping/src/realization.rs @@ -22,7 +22,7 @@ use std::rc::Rc; use thiserror::Error; /// Confidence δ assumed when the target carries only an ε -/// (`AccuracyTarget::Epsilon`): the (ε, δ)-parameterised sketches (CMS) need +/// (`AccuracyTarget::epsilon`): the (ε, δ)-parameterised sketches (CMS) need /// one. `ln(1/0.01) → depth 5`, matching the conventional CMS sizing. pub const DEFAULT_DELTA: f64 = 0.01; @@ -93,11 +93,13 @@ pub fn accuracy_target(intent: &AggIntent) -> Option<&AccuracyTarget> { /// `Exact` to [`exact_realization`] instead); degrades to the tightest /// parameters for a caller that resolves it directly anyway. pub fn accuracy_budget(accuracy: &AccuracyTarget) -> (f64, f64) { - match accuracy { - AccuracyTarget::Exact => (f64::MIN_POSITIVE, DEFAULT_DELTA), - AccuracyTarget::Epsilon(e) => (*e, DEFAULT_DELTA), - AccuracyTarget::EpsilonDelta { epsilon, delta } => (*epsilon, *delta), + if accuracy.is_exact() { + return (f64::MIN_POSITIVE, DEFAULT_DELTA); } + ( + accuracy.error, + accuracy.failure_probability.unwrap_or(DEFAULT_DELTA), + ) } /// `asap-plan`'s built-in `SketchParams` sizing, keyed off the resolved diff --git a/crates/asap-aware-mapping/src/replacement.rs b/crates/asap-aware-mapping/src/replacement.rs index cce6f97e8..139050f5b 100644 --- a/crates/asap-aware-mapping/src/replacement.rs +++ b/crates/asap-aware-mapping/src/replacement.rs @@ -793,14 +793,14 @@ pub(crate) fn realizations_for_intent( | AggIntent::FrequencyEntropy { accuracy, .. } | AggIntent::Count { accuracy } | AggIntent::TopK { accuracy, .. } => match accuracy { - AccuracyTarget::Exact if matches!(intent, AggIntent::Count { .. }) => vec![ + accuracy if accuracy.is_exact() && matches!(intent, AggIntent::Count { .. }) => vec![ exact_realization(intent), Realization::Sketch(SketchKind::new( SketchAlgorithm::UnivMon, default_size_params(SketchAlgorithm::UnivMon, intent, 0.0, DEFAULT_DELTA), )), ], - AccuracyTarget::Exact => vec![exact_realization(intent)], + accuracy if accuracy.is_exact() => vec![exact_realization(intent)], _ if matches!(intent, AggIntent::Count { .. }) => { let mut candidates = sketch_realizations(intent, accuracy, cost_model); candidates.push(exact_realization(intent)); @@ -887,7 +887,7 @@ pub(crate) fn realizations_for_intent( } /// Exact realization of an approximate-capable intent whose target is -/// `AccuracyTarget::Exact`. `Count` has a mergeable exact accumulator; exact +/// `AccuracyTarget::exact()`. `Count` has a mergeable exact accumulator; exact /// quantile / top-k / cardinality have no single-value summary form (they /// need the full multiset / heap / set) and pass through. fn exact_realization(intent: &AggIntent) -> Realization { @@ -1536,7 +1536,7 @@ impl<'a> ASAPStrategies<'a> { continue; }; let shape = CompositionShape { - metric: local.metric, + metric: local.spec.metric, approximate_layer_count: 1 + child_layers, }; let allocations = planning_inputs.allocator.allocations(target, &shape); @@ -1833,7 +1833,7 @@ fn exact_topk_over_temporal_values( if !matches!(input.non_asap(), Some(NonASAPOp::TimeRange { .. })) { return Ok(None); } - let values = realize_child_with(child, planning_inputs, Some(&AccuracyTarget::Exact))?; + let values = realize_child_with(child, planning_inputs, Some(&AccuracyTarget::exact()))?; if !values.contains_asap() || !values .guarantee @@ -1915,10 +1915,9 @@ pub(crate) fn realize_child_with( } let overridden = end_to_end_target.and_then(|target| { let declared = bindable_intent(root)?; - match accuracy_target(declared) { - Some(AccuracyTarget::Exact) | None => None, - Some(_) => Some(override_accuracy(declared, target)), - } + accuracy_target(declared) + .filter(|accuracy| !accuracy.is_exact()) + .map(|_| override_accuracy(declared, target)) }); match ASAPStrategies::from_planning_inputs(planning_inputs) .propose_with(root, overridden.as_ref(), None) @@ -2038,7 +2037,7 @@ fn realize_binary( .filter_map(|(index, guarantee)| { guarantee .filter(|guarantee| !guarantee.is_exact()) - .map(|guarantee| (index, guarantee.metric)) + .map(|guarantee| (index, guarantee.spec.metric)) }) .collect::>(); if !approximate.is_empty() { @@ -2302,16 +2301,13 @@ fn shared_quantile_target(lhs: &OperatorNode, rhs: &OperatorNode) -> Option Option { let tighten = |epsilon: f64| (epsilon.is_finite() && epsilon > 0.0).then_some(epsilon / (2.0 + epsilon)); - match target { - AccuracyTarget::Exact => None, - AccuracyTarget::Epsilon(epsilon) => tighten(*epsilon).map(AccuracyTarget::Epsilon), - AccuracyTarget::EpsilonDelta { epsilon, delta } => { - tighten(*epsilon).map(|epsilon| AccuracyTarget::EpsilonDelta { - epsilon, - delta: *delta, - }) - } + if target.is_exact() { + return None; } + tighten(target.error).map(|error| AccuracyTarget { + error, + ..target.clone() + }) } fn ddsketch_quantile_alpha(node: &OperatorNode) -> Option { @@ -2962,11 +2958,12 @@ fn construct_summary_agg( } } + let exact = AccuracyTarget::exact(); let bound_child = realize_child_with( &input.child, planning_inputs, if snapshot_weighted { - Some(&AccuracyTarget::Exact) + Some(&exact) } else { child_target }, @@ -3043,13 +3040,16 @@ fn construct_summary_agg( operator: CompositionOperator::ApproximateAggregate, rule: "simultaneous_score_bounds_over_distinct_partition_item_identities".into(), }); - score.failure_probability = ProbabilityExpr::Scaled { + score.spec.failure_probability = ProbabilityExpr::Scaled { count, - inner: Box::new(score.failure_probability), + inner: Box::new(score.spec.failure_probability), }; // #455: missing evidence preserves a logical candidate. Only known // contributions that already violate the target reject it here. - if !estimator.satisfies(&score.optimistic_floor(), target) { + if !estimator.satisfies( + &score.optimistic_floor(), + &crate::accuracy::native_target(target, &score), + ) { return Err(RealizationError::PhysicalRealization( "weighted TopK scores miss accuracy target", )); @@ -3065,10 +3065,16 @@ fn construct_summary_agg( None, &stats, )?; - joint.failure_probability = ProbabilityExpr::UnionBound { - terms: vec![joint.failure_probability, score.failure_probability], + joint.spec.failure_probability = ProbabilityExpr::UnionBound { + terms: vec![ + joint.spec.failure_probability, + score.spec.failure_probability, + ], }; - if !estimator.satisfies(&joint.optimistic_floor(), target) { + if !estimator.satisfies( + &joint.optimistic_floor(), + &crate::accuracy::native_target(target, &joint), + ) { return Err(RealizationError::PhysicalRealization( "weighted TopK joint guarantee misses target", )); @@ -3489,11 +3495,14 @@ fn compose_guarantee( // Check even over an exact input: parameter clamps or a conservative // confidence conversion can make the tightest available sketch miss // its requested target. - if !accuracy.satisfies(&guarantee.optimistic_floor(), target) { + if !accuracy.satisfies( + &guarantee.optimistic_floor(), + &crate::accuracy::native_target(target, &guarantee), + ) { return Err(AccuracyError::TargetNotSatisfied { - metric: guarantee.metric, - bound: guarantee.bound.evaluate(), - failure_probability: guarantee.failure_probability.evaluate(), + metric: guarantee.spec.metric, + bound: guarantee.spec.error.evaluate(), + failure_probability: guarantee.spec.failure_probability.evaluate(), target: target.clone(), }); } @@ -6143,7 +6152,7 @@ pub fn search_workload_with_targets<'s, Id>( .partition(|candidate| match &candidate.replacement { Replacement::SubDAG(_) if candidate.is_logical_rewrite() => true, Replacement::SubDAG(node) => node.guarantee.as_ref().map_or_else( - || !matches!(target, AccuracyTarget::Exact), + || !target.is_exact(), |g| accuracy_model.satisfies(&g.optimistic_floor(), &target), ), // A composition's guarantee depends on the concrete child; @@ -6158,21 +6167,15 @@ pub fn search_workload_with_targets<'s, Id>( .as_ref() .map(|g| { ( - g.metric, - g.bound.evaluate(), - g.failure_probability.evaluate(), + g.spec.metric, + g.spec.error.evaluate(), + g.spec.failure_probability.evaluate(), ) }) - .unwrap_or(( - asap_types::post_asap::ErrorMetric::AbsoluteValue, - None, - None, - )), - Replacement::ExactComposition(_) => ( - asap_types::post_asap::ErrorMetric::AbsoluteValue, - None, - None, - ), + .unwrap_or((asap_types::post_asap::ErrorMetric::ABSOLUTE, None, None)), + Replacement::ExactComposition(_) => { + (asap_types::post_asap::ErrorMetric::ABSOLUTE, None, None) + } }; RejectedCandidate { strategy: candidate.strategy, @@ -6200,7 +6203,7 @@ fn strictest_sibling_accuracy( siblings: &[Rc], ) -> Option { fn approximate(intent: &AggIntent) -> Option<&AccuracyTarget> { - accuracy_target(intent).filter(|accuracy| !matches!(accuracy, AccuracyTarget::Exact)) + accuracy_target(intent).filter(|accuracy| !accuracy.is_exact()) } let Some(NonASAPOp::Aggregate { reduction, @@ -6249,12 +6252,9 @@ fn strictest_sibling_accuracy( if (eps, delta) == own { None } else if delta == DEFAULT_DELTA { - Some(AccuracyTarget::Epsilon(eps)) + Some(AccuracyTarget::epsilon(eps)) } else { - Some(AccuracyTarget::EpsilonDelta { - epsilon: eps, - delta, - }) + Some(AccuracyTarget::epsilon_delta(eps, delta)) } } @@ -6579,6 +6579,7 @@ 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; @@ -6587,7 +6588,7 @@ mod tests { /// selected root is returned as assembled. #[test] fn assemble_selected_query_finalizes_only_an_exact_state_root() { - let assemble = |query: &str, accuracy: AccuracyTarget| { + let assemble = |query: &str, accuracy: RequestedAccuracy| { let space = search_workload(vec![("q", lower_promql(query, accuracy))]).unwrap(); let selection = space.global_selection(&DefaultCostModel); let root = &space.roots[0].1; @@ -6596,7 +6597,7 @@ mod tests { selection.assemble_selected_query(root).unwrap().unwrap(), ) }; - let (state, query) = assemble("sum_over_time(m[1m])", AccuracyTarget::Exact); + let (state, query) = assemble("sum_over_time(m[1m])", RequestedAccuracy::exact()); assert!(matches!( state.asap(), Some(ASAPOp::SummaryAgg { @@ -6612,7 +6613,7 @@ mod tests { let (estimate, query) = assemble( "quantile_over_time(0.99, m[1m])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Rank), ); assert!(estimate.contains_asap()); assert!(Rc::ptr_eq(&estimate, &query)); @@ -6654,7 +6655,7 @@ mod tests { OperatorNode::new_shared(asap_types::ir::Operator::NonASAP(NonASAPOp::Aggregate { reduction: ReductionTy::by(vec![2]), measures: vec![AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }], output_names: vec![], filters: vec![filter], @@ -6665,7 +6666,7 @@ mod tests { }; let intent = AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; let family = FieldDataType::Sketch( SketchKind::new( @@ -6706,7 +6707,7 @@ mod tests { fn search_rejects_a_root_with_an_asap_operator() { let root = lower_promql( "quantile_over_time(0.99, m[1m])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ); let space = search_workload(vec![("q", root)]).unwrap(); let summary = space @@ -6747,8 +6748,11 @@ mod tests { #[test] fn rate_candidate_inventories_have_no_timing_only_duplicates() { for (query, accuracy) in [ - ("sum by(job)(rate(m[1m]))", AccuracyTarget::Exact), - ("topk by(job)(2, rate(m[1m]))", AccuracyTarget::Epsilon(0.1)), + ("sum by(job)(rate(m[1m]))", RequestedAccuracy::exact()), + ( + "topk by(job)(2, rate(m[1m]))", + RequestedAccuracy::epsilon(0.1), + ), ] { let root = lower_promql(query, accuracy); let inventory = search_workload(vec![(0usize, root)]) @@ -6770,7 +6774,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]))", AccuracyTarget::Exact); + let root = lower_promql("sum by(job)(rate(m[1m]))", RequestedAccuracy::exact()); let inventory = search_workload(vec![(0usize, root)]) .unwrap() .enumerate_candidate_dags(4096) @@ -6800,7 +6804,7 @@ mod tests { "sum by(job)(m)", "sum_over_time(m[1m])", ] { - let root = lower_promql(query, AccuracyTarget::Exact); + let root = lower_promql(query, RequestedAccuracy::exact()); let space = search_workload(vec![(0usize, root.clone())]).unwrap(); let inventory = space.enumerate_candidate_dags(4096).unwrap(); assert!(!inventory.candidates.is_empty()); @@ -6948,7 +6952,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])", AccuracyTarget::Exact); + let root = lower_promql("avg_over_time(a[5m])", RequestedAccuracy::exact()); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); let operator = candidates @@ -6975,10 +6979,7 @@ mod tests { fn approximate_temporal_topk_admits_exact_maintained_values() { let root = lower_promql( "topk by(job)(1,count_over_time(a[5m]))", - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + RequestedAccuracy::epsilon_delta(0.01, 0.01), ); let planning_inputs = CandidatePlanningInputs::with_default_accuracy(&crate::cost_model::DefaultCostModel); @@ -6996,7 +6997,7 @@ mod tests { "topk(5, sum_over_time(a[5m]))", "topk by(job)(5, count_over_time(a[5m]))", ] { - let root = lower_promql(query, AccuracyTarget::Exact); + let root = lower_promql(query, RequestedAccuracy::exact()); let planning_inputs = CandidatePlanningInputs::with_default_accuracy( &crate::cost_model::DefaultCostModel, ); @@ -7048,10 +7049,9 @@ mod tests { }) } } - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let requested = + RequestedAccuracy::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Relative); + let target = requested.target_in(ErrorMetric::Relative); let inputs = CandidatePlanningInputs { evidence: &Domain, ..CandidatePlanningInputs::with_default_accuracy(&DefaultCostModel) @@ -7060,7 +7060,7 @@ mod tests { "avg_over_time(a[5m]) / quantile_over_time(0.5,a[5m])", "quantile_over_time(0.5,a[5m]) / avg_over_time(a[5m])", ] { - let root = lower_promql(query, target.clone()); + let root = lower_promql(query, requested.clone()); let node = realize_binary(&root, inputs, Some(&target)) .unwrap() .expect("bounded ratio candidate"); @@ -7071,13 +7071,11 @@ mod tests { // Missing domain proof permits an uncertified direct quantile ratio only. #[test] fn quantile_ratio_without_input_proof_has_no_root_guarantee() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let requested = RequestedAccuracy::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])", - target.clone(), + requested.clone(), ); let planning_inputs = CandidatePlanningInputs::with_default_accuracy(&crate::cost_model::DefaultCostModel); @@ -7088,7 +7086,7 @@ mod tests { let other = lower_promql( "avg_over_time(a[5m]) / quantile_over_time(0.5,a[5m])", - target.clone(), + requested.clone(), ); assert!(realize_binary(&other, planning_inputs, Some(&target)) .unwrap() @@ -7165,12 +7163,12 @@ mod tests { AggIntent::Quantile { col: None, q, - accuracy: AccuracyTarget::Epsilon(e), + accuracy: AccuracyTarget::epsilon(e), } } fn eps(e: f64) -> AccuracyTarget { - AccuracyTarget::Epsilon(e) + AccuracyTarget::epsilon(e) } // ── realizations_for_intent / sizing ─────────────────────────────── @@ -7244,34 +7242,34 @@ mod tests { A::Quantile { col: None, q: 0.5, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, Pass, ), ( A::Cardinality { cols: vec![], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, Pass, ), ( A::Cardinality { cols: vec![0, 1], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, Pass, ), ( A::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, Acc(E::Count), ), ( A::TopK { k: 10, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, Pass, ), @@ -7380,7 +7378,7 @@ mod tests { let exact = AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; assert_eq!(preferred(&exact), Realization::PassThrough); @@ -7444,10 +7442,7 @@ mod tests { #[test] fn epsilon_delta_sizes_cms_depth() { let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.001, - delta: 0.001, - }, + accuracy: AccuracyTarget::epsilon_delta(0.001, 0.001), }; assert_eq!( preferred(&intent), @@ -7815,7 +7810,7 @@ mod tests { fn fitting_cardinality() -> AggIntent { AggIntent::Cardinality { cols: vec![], - accuracy: AccuracyTarget::Epsilon(0.05), + accuracy: AccuracyTarget::epsilon(0.05), } } @@ -7846,10 +7841,7 @@ mod tests { vec![2], AggIntent::Cardinality { cols: vec![], - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.05, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.05, 0.01), }, metric_scan(&["job"]), ); @@ -7880,7 +7872,7 @@ mod tests { let intent = AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; let q = agg(vec![2], intent, metric_scan(&["job"])); let target = TargetSubDAG::new(&q); @@ -8146,10 +8138,7 @@ mod tests { #[test] fn single_bindable_aggregate_keeps_unprovable_hydra_candidates() { let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let root = agg(vec![2], intent, metric_scan(&["job"])); let space = search_workload(vec![("q", root)]).unwrap(); @@ -8240,14 +8229,7 @@ mod tests { let root = agg(vec![2], fitting_cardinality(), metric_scan(&["job"])); let targeted = search_workload_with_targets( - vec![( - "q", - root, - Some(AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }), - )], + vec![("q", root, Some(AccuracyTarget::epsilon_delta(0.01, 0.01)))], &default_strategies(), &DefaultAccuracyModel, ) @@ -8274,7 +8256,7 @@ mod tests { vec![( "q", agg(vec![2], default_cardinality(), metric_scan(&["job"])), - Some(AccuracyTarget::Exact), + Some(AccuracyTarget::exact()), )], &default_strategies(), &DefaultAccuracyModel, @@ -8600,10 +8582,7 @@ mod tests { fn first_grouping(estimated_count: usize) -> GroupingStrategy { let model = EstimatedSubpopulations(estimated_count); let intent = AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }; let root = agg(vec![2, 3], intent, metric_scan(&["tenant_id", "endpoint"])); let strategies = default_strategies_with(&model); @@ -9259,7 +9238,7 @@ mod tests { Some(false) } } - let root = lower_promql("sum_over_time(a[1m])", AccuracyTarget::Exact); + let root = lower_promql("sum_over_time(a[1m])", RequestedAccuracy::exact()); let space = search_workload(vec![("q", root)]).unwrap(); let selected = space.global_selection(&Unsupported); assert!(selected @@ -9272,7 +9251,10 @@ 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]))", AccuracyTarget::Exact); + let root = lower_promql( + "sum by(job)(sum_over_time(a[1m]))", + RequestedAccuracy::exact(), + ); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); assert!(candidates @@ -9342,7 +9324,10 @@ mod tests { )) } } - let root = lower_promql("sum by(job)(sum_over_time(a[1m]))", AccuracyTarget::Exact); + let root = lower_promql( + "sum by(job)(sum_over_time(a[1m]))", + RequestedAccuracy::exact(), + ); let space = search_workload(vec![("q", root)]).unwrap(); let selection = space.global_selection(&ExplicitCosts); let selected = selection @@ -9898,7 +9883,7 @@ mod tests { fn explicit_empty_by_aggregate_realizes_summary_agg_with_reduce_reduction() { let intent = AggIntent::Cardinality { cols: vec![], - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; let q = agg(vec![], intent, metric_scan(&["job"])); let root = realize(&q).unwrap(); @@ -9978,7 +9963,7 @@ mod tests { for (cols, want) in cases { let intent = AggIntent::Cardinality { cols: cols.clone(), - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; let root = realize(&agg(vec![0], intent, metric_scan(&["job"]))).unwrap(); let bound = find_summary_input(&root) @@ -10011,7 +9996,7 @@ mod tests { AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, ] { let q = agg(vec![2], intent.clone(), metric_scan(&["job"])); @@ -10097,7 +10082,7 @@ mod tests { vec![2], AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["job"]), ); @@ -10110,7 +10095,7 @@ mod tests { let inner = agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["job"]), ); @@ -10118,7 +10103,7 @@ mod tests { vec![], AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); @@ -10128,8 +10113,8 @@ mod tests { assert!(proposals.iter().any(|candidate| matches!( &candidate.replacement, Replacement::SubDAG(node) if node.guarantee.as_ref().is_some_and(|g| - g.bound.evaluate().is_none() - && g.failure_probability.evaluate().is_none()) + g.spec.error.evaluate().is_none() + && g.spec.failure_probability.evaluate().is_none()) ))); } @@ -10160,10 +10145,7 @@ mod tests { let inner = agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }, metric_scan(&["job"]), ); @@ -10171,10 +10153,7 @@ mod tests { vec![], AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01), }, inner, ); @@ -10190,8 +10169,8 @@ mod tests { &candidate.replacement, Replacement::SubDAG(node) if node.guarantee.as_ref().is_some_and(|g| - g.metric == ErrorMetric::TopKMembership - && g.failure_probability.evaluate() == Some(0.005)) + g.spec.error.evaluate().is_some() + && g.spec.failure_probability.evaluate() == Some(0.005)) ))); } @@ -10200,7 +10179,7 @@ mod tests { let inner = agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["service"]), ); @@ -10208,7 +10187,7 @@ mod tests { vec![], AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); @@ -10275,7 +10254,7 @@ mod tests { vec![], AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); @@ -10350,7 +10329,7 @@ mod tests { vec![2], AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); @@ -10394,7 +10373,7 @@ mod tests { let inner = agg( vec![2, 3], AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["service", "region"]), ); @@ -10402,7 +10381,7 @@ mod tests { vec![], AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); @@ -10445,7 +10424,7 @@ mod tests { let inner = agg( vec![2, 3, 4], AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["service", "method", "region"]), ); @@ -10455,7 +10434,7 @@ mod tests { vec![2], AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); @@ -10553,18 +10532,19 @@ mod tests { local: Option<&AccuracyGuarantee>, stats: &PropagationStats, ) -> Result { - let rank = |g: &AccuracyGuarantee| g.is_exact() || g.metric == ErrorMetric::Rank; + let rank = |g: &AccuracyGuarantee| g.is_exact() || g.spec.metric == ErrorMetric::Rank; if let (CompositionOperator::ApproximateAggregate, true, Some(local)) = (op, inputs.iter().all(rank), local) { - let relabel = |g: &AccuracyGuarantee| AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - ..g.clone() + let relabel = |g: &AccuracyGuarantee| { + let mut g = g.clone(); + g.spec.metric = ErrorMetric::ABSOLUTE; + g }; let inputs: Vec<_> = inputs.iter().map(relabel).collect(); let mut out = DefaultAccuracyModel.propagate(op, &inputs, Some(&relabel(local)), stats)?; - out.metric = local.metric; + out.spec.metric = local.spec.metric; return Ok(out); } DefaultAccuracyModel.propagate(op, inputs, local, stats) @@ -10579,7 +10559,7 @@ mod tests { AggIntent::Quantile { col: None, q, - accuracy: AccuracyTarget::Epsilon(eps), + accuracy: AccuracyTarget::epsilon(eps), } } @@ -10629,15 +10609,24 @@ mod tests { .as_ref() .is_some_and(AccuracyGuarantee::is_exact)); - // Cross-metric: a quantile over a cardinality estimate. + // A quantile over a distinct-count estimate: a rank error has no rule + // over a relative input, while DDSketch's relative error composes + // with it as with any relative value error (#690: a distinct count's + // error is a relative one). let inner = agg(vec![2], default_cardinality(), metric_scan(&["job"])); let outer = agg(vec![], default_quantile(0.99), inner); let proposals = ASAPStrategies::default_cost_model().propose(&TargetSubDAG::new(&outer)); - assert!(proposals.candidates.is_empty()); + assert!(!proposals.rejected.is_empty()); assert!(proposals.rejected.iter().all(|r| matches!( &r.error, - AccuracyError::UnsupportedComposition { input_metrics, .. } - if input_metrics == &vec![ErrorMetric::Cardinality] + AccuracyError::UnsupportedComposition { input_metrics, local_metric, .. } + if input_metrics == &vec![ErrorMetric::Relative] + && *local_metric == Some(ErrorMetric::Rank) + ))); + assert!(proposals.candidates.iter().all(|candidate| matches!( + &candidate.replacement, + Replacement::SubDAG(node) + if node.guarantee.as_ref().is_some_and(|g| g.spec.metric == ErrorMetric::Relative) ))); } @@ -10652,9 +10641,9 @@ mod tests { .guarantee .as_ref() .expect("a evaluation carries a guarantee"); - assert_eq!(guarantee.metric, ErrorMetric::Rank); + assert_eq!(guarantee.spec.metric, ErrorMetric::Rank); assert_eq!( - guarantee.bound.evaluate(), + guarantee.spec.error.evaluate(), Some(crate::accuracy::estimators::kll::kll_rank_error_99(269)) ); assert_eq!(guarantee.approximate_layer_count(), 1); @@ -10700,7 +10689,7 @@ mod tests { let outer = agg( vec![], AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, inner, ); @@ -10733,7 +10722,10 @@ mod tests { node.operator, Operator::ASAP(ASAPOp::SummaryEstimate { .. }) ) && node.guarantee.as_ref().is_some_and(|guarantee| { - DefaultAccuracyModel.satisfies(guarantee, &AccuracyTarget::Epsilon(0.1)) + DefaultAccuracyModel.satisfies( + guarantee, + &crate::accuracy::native_target(&AccuracyTarget::epsilon(0.1), guarantee), + ) }) })); } @@ -10759,7 +10751,10 @@ mod tests { // ASAP node; a logical rewrite (old `Replacement::Rewrite`) does not. Replacement::SubDAG(node) if node.contains_asap() => { node.guarantee.as_ref().is_some_and(|g| { - DefaultAccuracyModel.satisfies(g, &AccuracyTarget::Epsilon(0.1)) + DefaultAccuracyModel.satisfies( + g, + &crate::accuracy::native_target(&AccuracyTarget::epsilon(0.1), g), + ) }) } Replacement::SubDAG(_) => false, @@ -10791,7 +10786,11 @@ mod tests { // candidate misses it and is moved to `rejected`; nothing is left // for the cost model to rank. let space = search_workload_with_targets( - vec![("q", Rc::clone(&q), Some(AccuracyTarget::Epsilon(0.001)))], + vec![( + "q", + Rc::clone(&q), + Some(AccuracyTarget::epsilon(0.001).with_metric(ErrorMetric::Rank)), + )], &default_strategies(), &DefaultAccuracyModel, ) @@ -10804,7 +10803,7 @@ mod tests { .all(|c| matches!(&c.replacement, Replacement::SubDAG(n) if !n.contains_asap()))); assert!(group.rejected.iter().all(|r| matches!( r.error, - AccuracyError::TargetNotSatisfied { target: AccuracyTarget::Epsilon(e), .. } if e == 0.001 + AccuracyError::TargetNotSatisfied { ref target, .. } if target.error == 0.001 ))); assert!(group.rejected.len() >= 2); let selection = space.global_selection(&DefaultCostModel); @@ -10812,7 +10811,11 @@ mod tests { // A root target the node's own sizing meets keeps every candidate. let space = search_workload_with_targets( - vec![("q", Rc::clone(&q), Some(AccuracyTarget::Epsilon(0.01)))], + vec![( + "q", + Rc::clone(&q), + Some(AccuracyTarget::epsilon(0.01).with_metric(ErrorMetric::Rank)), + )], &default_strategies(), &DefaultAccuracyModel, ) @@ -10825,7 +10828,7 @@ mod tests { // An `Exact` root target admits only exact candidates. let space = search_workload_with_targets( - vec![("q", Rc::clone(&q), Some(AccuracyTarget::Exact))], + vec![("q", Rc::clone(&q), Some(AccuracyTarget::exact()))], &default_strategies(), &DefaultAccuracyModel, ) @@ -10846,7 +10849,7 @@ mod tests { let inner = agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["job"]), ); @@ -10854,12 +10857,12 @@ mod tests { vec![], AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, inner, ); let space = search_workload_with_targets( - vec![("q", Rc::clone(&q), Some(AccuracyTarget::Epsilon(0.01)))], + vec![("q", Rc::clone(&q), Some(AccuracyTarget::epsilon(0.01)))], &default_strategies(), &DefaultAccuracyModel, ) @@ -10908,7 +10911,7 @@ mod tests { })) .unwrap(); let space = search_workload_with_targets( - vec![("q", root.clone(), Some(AccuracyTarget::Exact))], + vec![("q", root.clone(), Some(AccuracyTarget::exact()))], &default_strategies(), &DefaultAccuracyModel, ) @@ -10958,7 +10961,7 @@ mod tests { vec![], AggIntent::TopK { k: 2, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, agg(vec![2], AggIntent::Sum { col: None }, raw.clone()), ); @@ -10996,7 +10999,7 @@ mod tests { AggIntent::Quantile { q: 0.9, col: None, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, metric_scan(&["job"]), ); diff --git a/crates/asap-aware-mapping/src/rewrite.rs b/crates/asap-aware-mapping/src/rewrite.rs index 999529364..4c16cac8b 100644 --- a/crates/asap-aware-mapping/src/rewrite.rs +++ b/crates/asap-aware-mapping/src/rewrite.rs @@ -199,7 +199,7 @@ pub(crate) fn temporal_average_components(root: &Rc) -> Option) -> Option> { OperatorNode::new_shared(asap_types::ir::Operator::NonASAP(NonASAPOp::Aggregate { reduction: reduction.clone(), measures: vec![AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }], output_names: Vec::new(), filters: vec![], @@ -437,6 +437,7 @@ 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; @@ -471,7 +472,10 @@ 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])", AccuracyTarget::Exact); + let root = lower_promql( + "avg_over_time(a{job=\"api\"}[5m])", + RequestedAccuracy::exact(), + ); assert!(SemanticEquivalentRewriteStrategy .replacements(&TargetSubDAG::new(&root)) .is_empty()); @@ -537,7 +541,7 @@ mod tests { for intent in [ AggIntent::Sum { col: None }, AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, AggIntent::Min { col: None }, ] { @@ -632,7 +636,7 @@ mod tests { )); assert!(matches!(rhs.non_asap(), Some(NonASAPOp::Aggregate { measures, .. }) - if matches!(measures.as_slice(), [AggIntent::Count { accuracy: AccuracyTarget::Exact }]) + if matches!(measures.as_slice(), [AggIntent::Count { accuracy }] if accuracy.is_exact()) )); let original_schema = original.schema.clone(); @@ -708,9 +712,7 @@ mod tests { let expected = matches!(measures.as_slice(), [AggIntent::Sum { .. }]) || matches!( measures.as_slice(), - [AggIntent::Count { - accuracy: AccuracyTarget::Exact - }] + [AggIntent::Count { accuracy }] if accuracy.is_exact() ); if !expected { continue; @@ -726,9 +728,7 @@ mod tests { found_sum |= matches!(measures.as_slice(), [AggIntent::Sum { .. }]); found_count |= matches!( measures.as_slice(), - [AggIntent::Count { - accuracy: AccuracyTarget::Exact - }] + [AggIntent::Count { accuracy }] if accuracy.is_exact() ); } assert!(found_sum && found_count); @@ -845,7 +845,7 @@ mod tests { #[test] fn semantic_rewrite_composes_every_supported_aggregate_pair() { let exact_count = AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; for (outer, inner) in [ (AggIntent::Sum { col: None }, AggIntent::Sum { col: None }), @@ -905,7 +905,7 @@ mod tests { let root = nested_aggregate( AggIntent::Sum { col: None }, AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, ); let space = crate::replacement::search_workload(vec![("sum-count", root)]).unwrap(); diff --git a/crates/asap-aware-mapping/src/rollup.rs b/crates/asap-aware-mapping/src/rollup.rs index 69a5bfb09..3b679d84c 100644 --- a/crates/asap-aware-mapping/src/rollup.rs +++ b/crates/asap-aware-mapping/src/rollup.rs @@ -106,8 +106,6 @@ use asap_types::ir::{NonASAPOp, OperatorNode}; use asap_types::pre_asap::agg_intent::AggIntent; use asap_types::pre_asap::schema::{ColumnId, Schema}; -use asap_types::types::AccuracyTarget; - use crate::replacement::{Replacement, ReplacementStrategy, ReplacementSubDAG, TargetSubDAG}; /// The `(by, intent, child)` shape this strategy operates on: a single @@ -171,10 +169,10 @@ fn rollup_combinator(intent: &AggIntent, finer_measure_col: ColumnId) -> Option< }), // Not self-combining — see the module docs. Both merge by addition // over the finer side's own output column instead. - AggIntent::Count { - accuracy: AccuracyTarget::Exact, - } - | AggIntent::Increase => Some(AggIntent::Sum { + AggIntent::Count { accuracy } if accuracy.is_exact() => Some(AggIntent::Sum { + col: Some(finer_measure_col), + }), + AggIntent::Increase => Some(AggIntent::Sum { col: Some(finer_measure_col), }), _ => None, @@ -629,14 +627,14 @@ mod tests { let fine = agg( vec![2, 3], AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, &scan, ); let coarse = agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, &scan, ); @@ -664,7 +662,7 @@ mod tests { fn approximate_count_does_not_roll_up_via_sum() { let scan = metric_scan(); let intent = AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; let fine = agg(vec![2, 3], intent.clone(), &scan); let coarse = agg(vec![2], intent, &scan); @@ -720,7 +718,7 @@ mod tests { #[test] fn workload_search_does_not_roll_up_approximate_count() { let intent = AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; let fine = agg(vec![2, 3], intent.clone(), &metric_scan()); let coarse = agg(vec![2], intent, &metric_scan()); @@ -873,7 +871,7 @@ mod tests { measures: vec![ AggIntent::Sum { col: Some(1) }, AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, ], output_names: vec![], 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 180550b5a..c821f675b 100644 --- a/crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs +++ b/crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs @@ -257,6 +257,7 @@ 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] { @@ -297,7 +298,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]))", - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ); let candidate = optimize_physical_asap("L1", &[root.clone().into()]).unwrap(); let physical = operator_root(&candidate, 0); @@ -310,7 +311,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))", AccuracyTarget::Exact); + let root = lower_promql("topk(5, sum by (job) (m))", RequestedAccuracy::exact()); let candidate = optimize_physical_asap("L1", &[root.into()]).unwrap(); let physical = operator_root(&candidate, 0); assert_eq!(names(&physical), ["topk_5"]); @@ -326,7 +327,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])))", - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ); let candidate = optimize_physical_asap("L1", &[root.clone().into()]).unwrap(); let physical = operator_root(&candidate, 0); @@ -371,10 +372,10 @@ mod tests { use asap_types::pre_asap::expr_ir::ScalarValue; let topk = AggIntent::TopK { k: 3, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; let count = AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; let by_job = || Reduction::by(vec![2]); let cases = [ @@ -406,7 +407,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 = AccuracyTarget::Exact; + let exact = RequestedAccuracy::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. @@ -479,10 +480,7 @@ mod tests { fn everything_runs_at_query_time() { let root = lower_promql( "topk by (job) (10, sum_over_time(m[1m]))", - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }, + RequestedAccuracy::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 0ac9092f1..aa8f56c8b 100644 --- a/crates/asap-aware-mapping/src/stage3_plan_selection.rs +++ b/crates/asap-aware-mapping/src/stage3_plan_selection.rs @@ -194,6 +194,13 @@ pub enum SelectionError { }, #[error("no valid candidate: {0:?}")] NoValidCandidate(Vec), + /// 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 @@ -343,10 +350,12 @@ fn accuracy_violation( } return Some(match guarantee { Some(g) => format!( - "q{query}: the output guarantees bound {:?}, failure probability {:?}, which \ - misses target {target:?}, and the deployment has no evidence that it meets it", - g.bound.evaluate(), - g.failure_probability.evaluate(), + "q{query}: the output guarantees a {:?} bound {:?}, failure probability {:?}, \ + which misses target {target:?}, and the deployment has no evidence that it \ + meets it", + g.spec.metric, + g.spec.error.evaluate(), + g.spec.failure_probability.evaluate(), ), None => format!( "q{query}: the output has no guarantee, and the deployment has no evidence \ @@ -679,7 +688,8 @@ fn domain_bound( .accuracy .propagate(&CompositionOperator::ExactDivision, &[gx, gy], None, &stats) .ok()? - .bound + .spec + .error .evaluate(); } let valid = @@ -690,11 +700,11 @@ fn domain_bound( let magnitude = |d: &OperandDomain| d.lower.abs().max(d.upper.abs()); let (mx, my) = (magnitude(&dx), magnitude(&dy)); let error = |g: &AccuracyGuarantee, m: f64| { - let bound = g.bound.evaluate()?; - match g.metric { + let bound = g.spec.error.evaluate()?; + match g.spec.metric { _ if g.is_exact() => Some(0.0), - ErrorMetric::AbsoluteValue => Some(bound), - ErrorMetric::RelativeValue | ErrorMetric::Cardinality => Some(bound * m), + ErrorMetric::ABSOLUTE => Some(bound), + ErrorMetric::Relative => Some(bound * m), _ => None, } }; @@ -1327,6 +1337,7 @@ mod tests { BoundExpr, ErrorMetric, GroupingStrategy, ProbabilityExpr, SketchKind, }; use asap_types::pre_asap::expr_ir::ArithmeticOpKind; + use asap_types::types::RequestedAccuracy; use asap_types::workload::{Evidence, Rate}; /// `n` queries repeating every 10 s, as Example 1's panels do. @@ -1360,10 +1371,7 @@ mod tests { fn candidates() -> Vec { let root = lower_promql( "topk by (job) (10, sum_over_time(m[1m]))", - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }, + RequestedAccuracy::epsilon_delta(0.01, 0.001), ); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], @@ -1398,15 +1406,9 @@ 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 = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let planned = RequestedAccuracy::epsilon_delta(0.01, 0.001); let candidates = all_candidates("quantile_over_time(0.99, m[1m])", &planned); - let strict = AccuracyTarget::EpsilonDelta { - epsilon: 1e-6, - delta: 1e-9, - }; + let strict = AccuracyTarget::epsilon_delta(1e-6, 1e-9); let summaries: Vec<_> = candidates .iter() .filter(|c| has_summary_estimate(c)) @@ -1452,13 +1454,10 @@ mod tests { /// non-negative is invalid. #[test] fn count_min_over_signed_weights_is_rejected_as_invalid() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); let selection = select_plan( &candidates(), - &[Some(target)], + &[Some(target.target_in(ErrorMetric::ABSOLUTE_STREAM_L1))], &every_10s(1), &data(), PlanningModels::builtin(), @@ -1476,10 +1475,7 @@ mod tests { #[test] fn count_min_over_samples_needs_a_declared_counter() { use asap_types::workload::MetricType; - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::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() @@ -1493,7 +1489,7 @@ mod tests { let data = with_types(&types); let selection = select_plan( &candidates(), - &[Some(target.clone())], + &[Some(target.target_in(ErrorMetric::ABSOLUTE_STREAM_L1))], &every_10s(1), &data, PlanningModels::builtin(), @@ -1525,9 +1521,9 @@ mod tests { #[test] fn the_sign_gate_opens_only_at_one() { use crate::accuracy::composition::samples_sign_statistic; - let gated = AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Product { + let gated = AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Product { factors: vec![ BoundExpr::Constant { value: 0.01 }, BoundExpr::Unknown { @@ -1535,28 +1531,32 @@ mod tests { }, ], }, - failure_probability: ProbabilityExpr::Constant { value: 0.001 }, - provenance: vec![], - }; + ProbabilityExpr::Constant { value: 0.001 }, + vec![], + ); for value in [0.0, 0.5, 2.0, -1.0] { assert_eq!( - bind_statistics(&gated, &|_| Some(value)).bound.evaluate(), + bind_statistics(&gated, &|_| Some(value)) + .spec + .error + .evaluate(), None, "{value}" ); } assert_eq!( - bind_statistics(&gated, &|_| Some(1.0)).bound.evaluate(), + bind_statistics(&gated, &|_| Some(1.0)) + .spec + .error + .evaluate(), Some(0.01) ); - let norm = AccuracyGuarantee { - bound: BoundExpr::Unknown { - statistic: INPUT_ROW_COUNT.into(), - }, - ..gated + let mut norm = gated; + norm.spec.error = BoundExpr::Unknown { + statistic: INPUT_ROW_COUNT.into(), }; assert_eq!( - bind_statistics(&norm, &|_| Some(0.0)).bound.evaluate(), + bind_statistics(&norm, &|_| Some(0.0)).spec.error.evaluate(), Some(0.0) ); } @@ -1568,9 +1568,9 @@ mod tests { #[test] fn sign_of_counter_samples_is_bound_from_the_declared_type() { use crate::accuracy::composition::samples_sign_statistic; - let gated = AccuracyGuarantee { - metric: ErrorMetric::RelativeValue, - bound: BoundExpr::Product { + let gated = AccuracyGuarantee::new( + ErrorMetric::Relative, + BoundExpr::Product { factors: vec![ BoundExpr::Constant { value: 0.01 }, BoundExpr::Unknown { @@ -1578,10 +1578,10 @@ mod tests { }, ], }, - failure_probability: ProbabilityExpr::Constant { value: 0.001 }, - provenance: vec![], - }; - let node = lower_promql("sum(m)", AccuracyTarget::Exact); + ProbabilityExpr::Constant { value: 0.001 }, + vec![], + ); + let node = lower_promql("sum(m)", RequestedAccuracy::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(), @@ -1590,7 +1590,8 @@ mod tests { let bound = |data: &DataWorkload| { bound_root_guarantee(&root, data, &PlanningModels::builtin()) .unwrap() - .bound + .spec + .error .evaluate() }; assert_eq!(bound(&with(&[("m", MetricType::Counter)])), Some(0.01)); @@ -1610,10 +1611,7 @@ mod tests { #[test] fn the_selected_plan_carries_confirmed_weights() { use asap_types::workload::MetricType; - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::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 { @@ -1728,13 +1726,10 @@ mod tests { #[test] fn per_node_costs_cover_the_dag_and_sum_to_the_total() { let candidates = candidates(); - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); let selection = select_plan( &candidates, - &[Some(target)], + &[Some(target.target_in(ErrorMetric::ABSOLUTE_STREAM_L1))], &every_10s(1), &data(), PlanningModels::builtin().with_evidence(&EveryOutputMeasuredExact), @@ -1763,7 +1758,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]))", AccuracyTarget::Exact); + let outer = lower_promql("sum(sum_over_time(m[1m]))", RequestedAccuracy::exact()); let inner = outer.children()[0].clone(); let mut candidate = optimize_physical_asap( "L", @@ -1811,7 +1806,7 @@ mod tests { /// Every combination of Pass 1 candidates of one query, as Stage 2 /// candidates `P1`, `P2`, ... - fn all_candidates(query: &str, target: &AccuracyTarget) -> Vec { + fn all_candidates(query: &str, target: &RequestedAccuracy) -> Vec { let root = lower_promql(query, target.clone()); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], @@ -1865,16 +1860,69 @@ mod tests { } } + /// Deployment evidence of a small error in a fixed metric for every + /// output that is not exact. + struct EveryOutputMeasuredIn(ErrorMetric); + + impl AccuracyEvidenceProvider for EveryOutputMeasuredIn { + fn output_guarantee(&self, _output: &QueryRoot) -> Option { + Some(AccuracyGuarantee::new( + self.0, + BoundExpr::Constant { value: 1e-6 }, + ProbabilityExpr::Constant { value: 1e-6 }, + vec![], + )) + } + } + + /// A bound stated in another metric misses the target however small it + /// is: a relative bound does not bound a top-k frequency's L1-scaled + /// error, so only the exact candidate stays valid; the same evidence in + /// the target's metric admits the sketches. + #[test] + fn a_bound_in_another_metric_misses_the_target() { + let counter = DataWorkload { + metric_types: [("m".to_string(), asap_types::workload::MetricType::Counter)].into(), + ..data() + }; + let target = + RequestedAccuracy::epsilon_delta(0.5, 0.5).target_in(ErrorMetric::ABSOLUTE_STREAM_L1); + let select = |metric| { + select_plan( + &candidates(), + &[Some(target.clone())], + &every_10s(1), + &counter, + PlanningModels::builtin().with_evidence(&EveryOutputMeasuredIn(metric)), + ) + .unwrap() + }; + let relative = select(ErrorMetric::Relative); + assert_eq!(relative.selected, "P1"); + let mut invalid: Vec<_> = relative.rejected.iter().filter(|r| !r.valid).collect(); + invalid.sort_by(|a, b| a.id.cmp(&b.id)); + assert_eq!( + invalid.iter().map(|r| r.id.as_str()).collect::>(), + ["P2", "P3"] + ); + for rejected in invalid { + assert!( + rejected.reason.contains("no evidence that it meets"), + "{}", + rejected.reason + ); + } + let l1 = select(ErrorMetric::ABSOLUTE_STREAM_L1); + assert!(l1.rejected.iter().all(|r| r.valid), "{:?}", l1.rejected); + } + /// A difference of two quantile estimates has no output guarantee, though /// each estimate is certified: its summary candidates stay listed as /// invalid with a reason. The deployment's empirical accuracy for the /// output admits them. #[test] fn combined_estimates_need_evidence_about_the_output() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::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, @@ -1888,7 +1936,7 @@ mod tests { let builtin = select_plan( &candidates, - &[Some(target.clone())], + &[Some(target.target_in(ErrorMetric::ABSOLUTE))], &every_10s(1), &data(), PlanningModels::builtin(), @@ -1907,7 +1955,7 @@ mod tests { let measured = select_plan( &candidates, - &[Some(target)], + &[Some(target.target_in(ErrorMetric::ABSOLUTE))], &every_10s(1), &data(), PlanningModels::builtin().with_evidence(&EveryOutputMeasuredExact), @@ -1943,10 +1991,7 @@ mod tests { /// query runs is not charged.) #[test] fn exact_quantile_state_costs_more_than_a_sketch_over_large_groups() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let target = RequestedAccuracy::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), @@ -1954,7 +1999,7 @@ mod tests { let candidates = keeping_state(all_candidates(query, &target)); let selection = select_plan( &candidates, - &[Some(target.clone())], + &[Some(target.target_in(ErrorMetric::Rank))], &every_10s(1), &data(), PlanningModels::builtin(), @@ -1977,7 +2022,7 @@ mod tests { #[test] fn exact_l2_state_is_priced_like_exact_quantile_state() { let aggregate_cost = |query: &str| { - let candidates = all_candidates(query, &AccuracyTarget::Exact); + let candidates = all_candidates(query, &RequestedAccuracy::exact()); let selection = select_plan( &candidates, &[None], @@ -2134,10 +2179,7 @@ mod tests { /// sketch is. #[test] fn exact_distinct_count_beats_a_sketch_over_small_groups() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let target = RequestedAccuracy::epsilon_delta(0.01, 0.01); let one_series = DataWorkload { input_cardinality: Evidence { value: Some(1), @@ -2149,7 +2191,7 @@ mod tests { let candidates = keeping_state(all_candidates("distinct_over_time(x[5m])", &target)); let selection = select_plan( &candidates, - &[Some(target.clone())], + &[Some(target.target_in(ErrorMetric::Relative))], &every_10s(1), &data, PlanningModels::builtin(), @@ -2192,14 +2234,11 @@ 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 = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); let candidates = all_candidates("quantile(0.99, rate(x[5m]))", &target); let selection = select_plan( &candidates, - &[Some(target)], + &[Some(target.target_in(ErrorMetric::Rank))], &every_10s(1), &data(), PlanningModels::builtin(), @@ -2224,13 +2263,13 @@ mod tests { /// operands only is exact. #[test] fn scalar_root_guarantees_compose_through_arithmetic() { - let node = lower_promql("sum(m)", AccuracyTarget::Exact); - let approximate = AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Constant { value: 0.01 }, - failure_probability: ProbabilityExpr::Constant { value: 0.001 }, - provenance: vec![], - }; + let node = lower_promql("sum(m)", RequestedAccuracy::exact()); + let approximate = AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Constant { value: 0.01 }, + ProbabilityExpr::Constant { value: 0.001 }, + vec![], + ); let with = |guarantee: Option| { ScalarExpr::PromqlScalarFromVector(Rc::new((*node).clone().with_guarantee(guarantee))) }; @@ -2247,9 +2286,9 @@ mod tests { Some(approximate.clone()) ); let scaled = root_guarantee(&doubled(with(Some(approximate.clone())))).unwrap(); - assert_eq!(scaled.metric, ErrorMetric::AbsoluteValue); - assert!((scaled.bound.evaluate().unwrap() - 0.02).abs() < 1e-12); - assert_eq!(scaled.failure_probability.evaluate(), Some(0.001)); + assert_eq!(scaled.spec.metric, ErrorMetric::ABSOLUTE); + assert!((scaled.spec.error.evaluate().unwrap() - 0.02).abs() < 1e-12); + assert_eq!(scaled.spec.failure_probability.evaluate(), Some(0.001)); let difference = QueryRoot::Scalar(ScalarExpr::Arithmetic { op: ArithmeticOpKind::Sub, left: Box::new(with(Some(approximate.clone()))), @@ -2257,11 +2296,9 @@ mod tests { semantics: ExprSemantics::Promql, }); let sum = root_guarantee(&difference).unwrap(); - assert!((sum.bound.evaluate().unwrap() - 0.02).abs() < 1e-12); - let rank = AccuracyGuarantee { - metric: ErrorMetric::Rank, - ..approximate - }; + assert!((sum.spec.error.evaluate().unwrap() - 0.02).abs() < 1e-12); + let mut rank = approximate; + rank.spec.metric = ErrorMetric::Rank; assert_eq!(root_guarantee(&doubled(with(Some(rank)))), None); assert!( root_guarantee(&doubled(with(Some(AccuracyGuarantee::exact("exact"))))) @@ -2272,7 +2309,7 @@ mod tests { /// Every candidate of the Stage 1 to Stage 3 run of one query. fn run( query: &str, - target: &AccuracyTarget, + target: &RequestedAccuracy, data: &DataWorkload, models: PlanningModels<'_>, ) -> (Vec, Selection) { @@ -2283,8 +2320,8 @@ mod tests { /// against `selected`. fn run_selecting( query: &str, - planned: &AccuracyTarget, - selected: &AccuracyTarget, + planned: &RequestedAccuracy, + selected: &RequestedAccuracy, data: &DataWorkload, models: PlanningModels<'_>, ) -> (Vec, Selection) { @@ -2333,7 +2370,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 = AccuracyTarget::Epsilon(0.01); + let target = RequestedAccuracy::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]))", @@ -2346,8 +2383,8 @@ mod tests { assert!(!ddsketch.is_empty(), "{query}"); for candidate in ddsketch { let g = output_guarantee(candidate).expect(query); - assert_eq!(g.metric, ErrorMetric::RelativeValue, "{query}"); - assert!(g.bound.evaluate().unwrap() <= 0.01, "{query}"); + assert_eq!(g.spec.metric, ErrorMetric::Relative, "{query}"); + assert!(g.spec.error.evaluate().unwrap() <= 0.01, "{query}"); assert!(selection.costs.contains_key(&candidate.id), "{query}"); } for candidate in candidates @@ -2365,7 +2402,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 = AccuracyTarget::Epsilon(0.01); + let target = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); let q = "quantile_over_time(0.99, m[1m])"; for (query, kept) in [ (format!("-{q}"), true), @@ -2386,8 +2423,8 @@ mod tests { let g = output_guarantee(candidate); if kept { let g = g.expect(&query); - assert_eq!(g.metric, ErrorMetric::RelativeValue, "{query}"); - assert!(g.bound.evaluate().unwrap() <= 0.01, "{query}"); + assert_eq!(g.spec.metric, ErrorMetric::Relative, "{query}"); + assert!(g.spec.error.evaluate().unwrap() <= 0.01, "{query}"); assert!(selection.costs.contains_key(&candidate.id), "{query}"); } else { assert_eq!(g, None, "{query}"); @@ -2404,7 +2441,7 @@ mod tests { /// the query, and the fold has no guarantee. #[test] fn a_fold_over_subquery_estimates_counts_its_steps() { - let target = AccuracyTarget::Epsilon(0.01); + let target = RequestedAccuracy::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])"), @@ -2419,9 +2456,9 @@ mod tests { assert!(!ddsketch.is_empty()); for candidate in ddsketch { let g = output_guarantee(candidate).expect("a guarantee"); - assert_eq!(g.metric, ErrorMetric::RelativeValue); - assert!(g.bound.evaluate().unwrap() <= 0.01); - let ProbabilityExpr::Scaled { count, .. } = &g.failure_probability else { + assert_eq!(g.spec.metric, ErrorMetric::Relative); + assert!(g.spec.error.evaluate().unwrap() <= 0.01); + let ProbabilityExpr::Scaled { count, .. } = &g.spec.failure_probability else { panic!("{g:?}"); }; assert_eq!(count, &BoundExpr::Constant { value: 10.0 }); @@ -2445,7 +2482,7 @@ mod tests { /// are labels, not the estimates (#669). #[test] fn counting_estimates_by_a_label_stays_exact() { - let target = AccuracyTarget::Epsilon(0.01); + let target = RequestedAccuracy::epsilon(0.01); let (candidates, _) = run( "count by (le) (quantile_over_time(0.99, m[1m]))", &target, @@ -2478,7 +2515,7 @@ mod tests { /// symbolic and plan selection treats it as unmet. #[test] fn average_over_relative_estimates_needs_the_sign() { - let target = AccuracyTarget::Epsilon(0.01); + let target = RequestedAccuracy::epsilon(0.01); let (candidates, selection) = run( "avg by (job) (quantile_over_time(0.99, m[1m]))", &target, @@ -2492,7 +2529,7 @@ mod tests { assert!(!ddsketch.is_empty()); for candidate in ddsketch { let g = output_guarantee(candidate).expect("a symbolic bound"); - assert!(g.bound.has_unknown(), "{g:?}"); + assert!(g.spec.error.has_unknown(), "{g:?}"); assert!(!selection.costs.contains_key(&candidate.id)); } } @@ -2502,10 +2539,8 @@ 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 = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = + RequestedAccuracy::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 @@ -2551,8 +2586,8 @@ mod tests { /// twice that, is checked against a looser one. #[test] fn ddsketch_ratio_meets_its_target_only_with_operand_domains() { - let planned = AccuracyTarget::Epsilon(0.01); - let target = AccuracyTarget::Epsilon(0.05); + let planned = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = RequestedAccuracy::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()); @@ -2570,17 +2605,17 @@ mod tests { /// `query` carrying `guarantee`, as a `scalar(…)` operand. fn scalar_of(query: &str, guarantee: AccuracyGuarantee) -> ScalarExpr { - let node = lower_promql(query, AccuracyTarget::Exact); + let node = lower_promql(query, RequestedAccuracy::exact()); ScalarExpr::PromqlScalarFromVector(Rc::new((*node).clone().with_guarantee(Some(guarantee)))) } fn guarantee(metric: ErrorMetric, bound: f64, delta: f64) -> AccuracyGuarantee { - AccuracyGuarantee { + AccuracyGuarantee::new( metric, - bound: BoundExpr::Constant { value: bound }, - failure_probability: ProbabilityExpr::Constant { value: delta }, - provenance: vec![], - } + BoundExpr::Constant { value: bound }, + ProbabilityExpr::Constant { value: delta }, + vec![], + ) } fn scalar_root(op: ArithmeticOpKind, left: ScalarExpr, right: ScalarExpr) -> QueryRoot { @@ -2638,22 +2673,22 @@ mod tests { ArithmeticOpKind::Sub, raw( "count by (job) (a)", - guarantee(ErrorMetric::Frequency, 0.01, 0.001), + guarantee(ErrorMetric::ABSOLUTE_STREAM_L1, 0.01, 0.001), ), raw( "count by (job) (b)", - guarantee(ErrorMetric::Frequency, 0.02, 0.001), + guarantee(ErrorMetric::ABSOLUTE_STREAM_L1, 0.02, 0.001), ), ); let models = PlanningModels::builtin(); let both = bound_root_guarantee(&root, &norms(&[("a", 100.0), ("b", 1_000.0)]), &models).unwrap(); assert!( - (both.bound.evaluate().unwrap() - 21.0).abs() < 1e-9, + (both.spec.error.evaluate().unwrap() - 21.0).abs() < 1e-9, "{both:?}" ); let one = bound_root_guarantee(&root, &norms(&[("a", 100.0)]), &models).unwrap(); - assert!(one.bound.has_unknown()); + assert!(one.spec.error.has_unknown()); } /// A stream norm is bound only for a summary of the source's raw value @@ -2664,7 +2699,7 @@ mod tests { fn stage3_binds_a_stream_norm_only_for_the_raw_value_column() { use asap_types::post_asap::SummaryInputExpr; use asap_types::pre_asap::ColumnRef; - let frequency = guarantee(ErrorMetric::Frequency, 0.01, 0.001); + let frequency = guarantee(ErrorMetric::ABSOLUTE_STREAM_L1, 0.01, 0.001); let doubled = |summary: Rc| { scalar_root( ArithmeticOpKind::Mul, @@ -2683,7 +2718,10 @@ mod tests { column(), )); let g = bound_root_guarantee(&raw, &data, &models).unwrap(); - assert!((g.bound.evaluate().unwrap() - 2.0).abs() < 1e-12, "{g:?}"); + assert!( + (g.spec.error.evaluate().unwrap() - 2.0).abs() < 1e-12, + "{g:?}" + ); for unbound in [ doubled(Rc::new(crate::test_support::cms_summary( "count by (job) (m)", @@ -2694,7 +2732,7 @@ mod tests { )), ] { let g = bound_root_guarantee(&unbound, &data, &models).unwrap(); - assert!(g.bound.has_unknown(), "{g:?}"); + assert!(g.spec.error.has_unknown(), "{g:?}"); } } @@ -2706,8 +2744,8 @@ mod tests { #[test] fn stage3_binds_products_and_quotients_from_operand_domains() { let (x, y) = ( - scalar_of("sum(a)", guarantee(ErrorMetric::AbsoluteValue, 0.01, 0.001)), - scalar_of("sum(b)", guarantee(ErrorMetric::AbsoluteValue, 0.02, 0.001)), + scalar_of("sum(a)", guarantee(ErrorMetric::ABSOLUTE, 0.01, 0.001)), + scalar_of("sum(b)", guarantee(ErrorMetric::ABSOLUTE, 0.02, 0.001)), ); let product = scalar_root(ArithmeticOpKind::Mul, x.clone(), y.clone()); let quotient = scalar_root(ArithmeticOpKind::Div, x, y); @@ -2718,20 +2756,20 @@ mod tests { ] { let g = bound_root_guarantee(root, &data(), &bounded).unwrap(); assert!( - (g.bound.evaluate().unwrap() - expected).abs() < 1e-9, + (g.spec.error.evaluate().unwrap() - expected).abs() < 1e-9, "{g:?}" ); - assert!((g.failure_probability.evaluate().unwrap() - 0.002).abs() < 1e-12); + assert!((g.spec.failure_probability.evaluate().unwrap() - 0.002).abs() < 1e-12); let unbound = bound_root_guarantee(root, &data(), &PlanningModels::builtin()).unwrap(); - assert!(unbound.bound.has_unknown(), "{unbound:?}"); + assert!(unbound.spec.error.has_unknown(), "{unbound:?}"); } let ratio = scalar_root( ArithmeticOpKind::Div, - scalar_of("sum(a)", guarantee(ErrorMetric::RelativeValue, 0.01, 0.0)), - scalar_of("sum(b)", guarantee(ErrorMetric::RelativeValue, 0.01, 0.0)), + scalar_of("sum(a)", guarantee(ErrorMetric::Relative, 0.01, 0.0)), + scalar_of("sum(b)", guarantee(ErrorMetric::Relative, 0.01, 0.0)), ); let g = bound_root_guarantee(&ratio, &data(), &PlanningModels::builtin()).unwrap(); - assert!(g.bound.has_unknown(), "{g:?}"); + assert!(g.spec.error.has_unknown(), "{g:?}"); } /// A ratio divided by an exact vector, `(q99 / q50) / max_over_time(m)`, @@ -2740,8 +2778,8 @@ mod tests { /// inherited it, so the candidate is selectable. #[test] fn a_scaled_ratio_binds_at_its_division() { - let planned = AccuracyTarget::Epsilon(0.01); - let target = AccuracyTarget::Epsilon(0.05); + let planned = RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Relative); + let target = RequestedAccuracy::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( @@ -2768,10 +2806,7 @@ mod tests { /// have expired) is not used; timeless declared evidence is. #[test] fn stage3_binds_only_fresh_input_cardinality() { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }; + let target = RequestedAccuracy::epsilon_delta(0.01, 0.001); let query = "max by (job) (quantile_over_time(0.99, m[1m]))"; let expiring = DataWorkload { input_cardinality: Evidence { @@ -2801,10 +2836,7 @@ mod tests { }; let root = lower_promql( "quantile_over_time(0.99, m[1m])", - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + RequestedAccuracy::epsilon_delta(0.01, 0.01), ); let inventory = crate::per_computation_candidates::generate_per_computation_candidates( vec![(0, QueryRoot::Operator(root))], @@ -2933,7 +2965,7 @@ mod tests { "quantile_over_time(0.99, m[1m])", "quantile by (job) (0.99, m)", ] { - let target = AccuracyTarget::Exact; + let target = RequestedAccuracy::exact(); let root = lower_promql(query, target); let dag = optimize_physical_asap("L", &[QueryRoot::Operator(root)]) .unwrap() @@ -2970,7 +3002,7 @@ mod tests { #[test] fn kept_exact_state_is_charged_by_the_values_it_keeps() { let kept = |query: &str| { - let root = lower_promql(query, AccuracyTarget::Exact); + let root = lower_promql(query, RequestedAccuracy::exact()); let mut dag = optimize_physical_asap("L", &[QueryRoot::Operator(root)]) .unwrap() .dag; @@ -3005,7 +3037,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]))", AccuracyTarget::Exact); + let root = lower_promql("sum by (job) (rate(m[1m]))", RequestedAccuracy::exact()); let one = optimize_physical_asap("L", &[QueryRoot::Operator(root.clone())]) .unwrap() .dag; @@ -3040,7 +3072,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]))", AccuracyTarget::Exact); + let root = lower_promql("sum by (job) (rate(m[1m]))", RequestedAccuracy::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 597f5530d..613c6ddc3 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, }; -pub(crate) fn lower_promql(query: &str, accuracy: AccuracyTarget) -> Rc { +pub(crate) fn lower_promql(query: &str, accuracy: RequestedAccuracy) -> Rc { let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -216,10 +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 = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let target = RequestedAccuracy::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-aware-mapping/tests/per_computation_candidates.rs b/crates/asap-aware-mapping/tests/per_computation_candidates.rs index 912ee6ed0..ba5543d16 100644 --- a/crates/asap-aware-mapping/tests/per_computation_candidates.rs +++ b/crates/asap-aware-mapping/tests/per_computation_candidates.rs @@ -20,10 +20,7 @@ use asap_types::{ use std::rc::Rc; fn approximate() -> AccuracyTarget { - AccuracyTarget::EpsilonDelta { - epsilon: 0.05, - delta: 0.01, - } + AccuracyTarget::epsilon_delta(0.05, 0.01) } fn candidates_for(intent: AggIntent) -> Vec { computation_candidates_for_intent(&intent) @@ -136,7 +133,7 @@ fn exact_quantile_stays_exact_and_approximate_keeps_both_families() { let exact = computation_candidates_for_intent(&AggIntent::Quantile { col: Some(0), q: 0.99, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }) .unwrap(); assert_eq!(exact.candidates, vec![ComputationCandidate::Exact]); @@ -304,9 +301,9 @@ fn assigned_physical_annotation_asap_input_and_invalid_accuracy_are_rejected() { generate_per_computation_candidates(vec![(0, QueryRoot::Operator(summary))]), Err(PerComputationCandidateError::ContainsAsap) )); - let epsilon_delta = |epsilon, delta| AccuracyTarget::EpsilonDelta { epsilon, delta }; + let epsilon_delta = |epsilon, delta| AccuracyTarget::epsilon_delta(epsilon, delta); for target in [ - AccuracyTarget::Epsilon(f64::NAN), + AccuracyTarget::epsilon(f64::NAN), epsilon_delta(0.0, 0.01), epsilon_delta(-0.1, 0.01), epsilon_delta(1.0, 0.01), @@ -382,10 +379,7 @@ fn summary_candidates_are_unsized() { accuracy: approximate(), }); let tight = candidates_for(AggIntent::Count { - accuracy: AccuracyTarget::EpsilonDelta { - epsilon: 0.0001, - delta: 0.0001, - }, + accuracy: AccuracyTarget::epsilon_delta(0.0001, 0.0001), }); assert_eq!(loose, tight); } diff --git a/crates/asap-physical-operators/src/operators/aggregate/temporal.rs b/crates/asap-physical-operators/src/operators/aggregate/temporal.rs index a289c5284..9836534e7 100644 --- a/crates/asap-physical-operators/src/operators/aggregate/temporal.rs +++ b/crates/asap-physical-operators/src/operators/aggregate/temporal.rs @@ -352,7 +352,7 @@ mod tests { (AggIntent::Increase, Value::Float64(4.)), ( AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, Value::Int64(3), ), diff --git a/crates/asap-physical-operators/src/physical_planner/candidates.rs b/crates/asap-physical-operators/src/physical_planner/candidates.rs index 7aad45bee..b3a3ec170 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::AccuracyTarget::Exact, + planner_types::types::RequestedAccuracy::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::AccuracyTarget::Exact)], + &[Some(planner_types::types::RequestedAccuracy::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 d04894d4e..ada16b2f0 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::AccuracyTarget, + accuracy: planner_types::types::RequestedAccuracy, ) -> 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::AccuracyTarget, + accuracy: planner_types::types::RequestedAccuracy, ) -> 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::AccuracyTarget, + accuracy: planner_types::types::RequestedAccuracy, ) -> 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 1f5d5ab2d..5ab8f9280 100644 --- a/crates/asap-physical-operators/tests/current_series_heap.rs +++ b/crates/asap-physical-operators/tests/current_series_heap.rs @@ -12,6 +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::{post_asap::*, pre_asap::DataType}; use std::{collections::BTreeMap, sync::Arc}; @@ -296,7 +297,7 @@ fn identity_encoding_is_lossless_and_rejects_noncanonical_inputs() { #[test] fn planner_current_series_candidate_compiles_with_dynamic_identity() { use asap_physical_operators::physical_planner::{compile, promql_rows::with_series_identity}; - use planner_types::{types::AccuracyTarget, workload::*}; + use planner_types::workload::*; use std::rc::Rc; let workload = PlanningWorkload { query_workload: QueryWorkload { @@ -304,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(AccuracyTarget::Exact), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::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 9673455d7..124e7f4aa 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::AccuracyTarget, workload::*}; +use planner_types::{post_asap::*, types::RequestedAccuracy, workload::*}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; fn lower(query: &str) -> Rc { - lower_with(query, AccuracyTarget::Exact) + lower_with(query, RequestedAccuracy::exact()) } -fn lower_with(query: &str, accuracy: AccuracyTarget) -> Rc { +fn lower_with(query: &str, accuracy: RequestedAccuracy) -> Rc { let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -52,7 +52,7 @@ fn lower_with(query: &str, accuracy: AccuracyTarget) -> Rc PhysicalASAPDAG { let expression = lower(query); let root = promql_rows::with_series_identity(&expression).unwrap_or(expression); - compile_physical_asap_dag(&selected_dag(root, AccuracyTarget::Exact)).unwrap() + compile_physical_asap_dag(&selected_dag(root, RequestedAccuracy::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 = AccuracyTarget::Epsilon(0.02); + let accuracy = RequestedAccuracy::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/physical_semantics.rs b/crates/asap-physical-operators/tests/physical_semantics.rs index 5b8e00c17..a934b99ce 100644 --- a/crates/asap-physical-operators/tests/physical_semantics.rs +++ b/crates/asap-physical-operators/tests/physical_semantics.rs @@ -959,11 +959,11 @@ fn measure_filters_keep_groups_without_matching_rows() { reduction: Plan::Reduce(GroupKeys::by(vec![0])), measures: vec![ AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, AggIntent::Sum { col: Some(1) }, AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, ], output_names: vec!["c".into(), "s".into(), "all".into()], 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 dbd265d2c..d9e1876e6 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::AccuracyTarget, + types::RequestedAccuracy, 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: &AccuracyTarget) -> Rc { +fn lower(query: &str, accuracy: &RequestedAccuracy) -> 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 = AccuracyTarget::Epsilon(0.1); + let accuracy = RequestedAccuracy::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 752d4272f..0391ccdcd 100644 --- a/crates/asap-physical-operators/tests/precompute_candidates.rs +++ b/crates/asap-physical-operators/tests/precompute_candidates.rs @@ -15,7 +15,8 @@ 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::*, pre_asap::DataType, types::AccuracyTarget, workload::*}; +use planner_types::types::RequestedAccuracy; +use planner_types::{post_asap::*, pre_asap::DataType, workload::*}; use std::{collections::BTreeMap, sync::Arc}; /// `sum by(job)(rate(m[1m]))` with its rows carrying the series identity. @@ -31,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(), AccuracyTarget::Exact)).unwrap() + compile_physical_asap_dag(&selected_dag( + grouped_rate_root(), + RequestedAccuracy::exact(), + )) + .unwrap() } fn run(plan: &CompiledPhysicalDAG, inputs: BTreeMap, scope: Scope) -> Vec { let sources = inputs @@ -414,7 +419,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(), AccuracyTarget::Exact) { + for root in logical_candidates(grouped_rate_root(), RequestedAccuracy::exact()) { let dag = compile_physical_asap_dag(&root).unwrap(); let Some(state) = dag.nodes.iter().find(|node| { matches!( @@ -632,7 +637,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(AccuracyTarget::Exact), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::exact()), ..Default::default() }, predictability: Predictability::Unknown, @@ -742,6 +747,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])))"), - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ); } diff --git a/crates/asap-physical-operators/tests/promql_values.rs b/crates/asap-physical-operators/tests/promql_values.rs index 35b9f2c0b..7339bec76 100644 --- a/crates/asap-physical-operators/tests/promql_values.rs +++ b/crates/asap-physical-operators/tests/promql_values.rs @@ -99,7 +99,7 @@ fn grouping_preserves_unenumerated_labels_and_empty_label_semantics() { run( compile_aggregate( &AggIntent::Count { - accuracy: planner_types::types::AccuracyTarget::Exact, + accuracy: planner_types::types::AccuracyTarget::exact(), }, &GroupKeys::by(vec![ColumnRef::Named("job".into())]), ) diff --git a/crates/asap-physical-operators/tests/weighted_topk_binding.rs b/crates/asap-physical-operators/tests/weighted_topk_binding.rs index 9016ba502..e0fc7c808 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::AccuracyTarget}; +use planner_types::{post_asap::*, pre_asap::DataType, types::RequestedAccuracy}; 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, AccuracyTarget::Epsilon(0.1)) + logical_candidates(root, RequestedAccuracy::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])))", - AccuracyTarget::Epsilon(0.1), + RequestedAccuracy::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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> Result, asap_frontend_promql::PromqlError> { let workload = PlanningWorkload { query_workload: QueryWorkload { @@ -257,8 +257,11 @@ 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]))", AccuracyTarget::Epsilon(0.1)).unwrap(); + let logical = lower_promql( + "topk by(job)(2, rate(m[1m]))", + RequestedAccuracy::epsilon(0.1), + ) + .unwrap(); let root = with_series_identity(&logical).unwrap(); for algorithm in [SketchAlgorithm::CountSketchWithHeap] { let candidate = &heap_candidate(Rc::clone(&root), &algorithm); @@ -559,7 +562,11 @@ fn maintained_rate_heap_compiles_fixed_window_precompute() { }; let root = Rc::new( with_series_identity( - &lower_promql("topk by(job)(2, rate(m[1m]))", AccuracyTarget::Epsilon(0.1)).unwrap(), + &lower_promql( + "topk by(job)(2, rate(m[1m]))", + RequestedAccuracy::epsilon(0.1), + ) + .unwrap(), ) .unwrap(), ); diff --git a/crates/devtools/examples/canonical_examples.rs b/crates/devtools/examples/canonical_examples.rs index fd4b703a1..29a0d5d31 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact, 1_000) { + match lower_promql_with_data_ingestion_interval(q, RequestedAccuracy::exact(), 1_000) { Ok(qe) => println!("{qe:#?}"), Err(e) => println!("ERR: {e}"), } @@ -98,7 +98,13 @@ async fn main() { &packets }; println!("=== {label} === sql> {q}"); - match lower_sql_dialect(q, catalog, SqlDialect::DataFusionSQL, AccuracyTarget::Exact).await + match lower_sql_dialect( + q, + catalog, + SqlDialect::DataFusionSQL, + RequestedAccuracy::exact(), + ) + .await { Ok(qe) => println!("{qe:#?}"), Err(e) => println!("ERR: {e}"), diff --git a/crates/devtools/examples/topk_ir.rs b/crates/devtools/examples/topk_ir.rs index 21dcefb4b..d9ec9ccdf 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), AccuracyTarget::Exact).await { + match lower_sql(query, &catalog(), RequestedAccuracy::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, AccuracyTarget::Exact, 1_000) { + match lower_promql_with_data_ingestion_interval(query, RequestedAccuracy::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 0eebe1381..4b6b3aafb 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await { diff --git a/crates/devtools/src/bin/show_logical_dag.rs b/crates/devtools/src/bin/show_logical_dag.rs index a37ceb966..4a3a7ff97 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact).await { + match lower_sql(q.trim(), &catalog, RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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 06c5446e3..e158aa2db 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* `AccuracyTarget`, runs Pass 1 of logical ASAP-aware +// *approximate* `RequestedAccuracy`, 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 `AccuracyTarget` every query in +// `--epsilon ` (default 0.01) sets the `RequestedAccuracy` 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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 = AccuracyTarget::Epsilon(epsilon); + let accuracy = RequestedAccuracy::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 fb72cb70a..323df79ab 100644 --- a/crates/devtools/src/bin/stage_pipeline.rs +++ b/crates/devtools/src/bin/stage_pipeline.rs @@ -45,6 +45,9 @@ // `--promql` and `--sql` may repeat and mix, in workload order; `--name` // 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. // `--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| @@ -66,7 +69,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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataArrival, DataWorkload, DurationMs, LatencyRequirement, PlanningWorkload, Query, QueryLanguage, QueryRecurrence, QueryRequirements, QueryWorkload, @@ -76,7 +79,7 @@ use serde_json::{json, Value}; const USAGE: &str = "usage: stage_pipeline (--example planner-layering-{1,3a,3b} | ((--promql | --sql) \ -[--name ]... [--table ...] [--epsilon --delta ] [--interval-ms ])) \ +[--name ]... [--table ...] [--epsilon --delta --metric ] [--interval-ms ])) \ [--max-combinations ] [--max-displayed ] --out "; /// The default `--max-combinations`: the most Stage 1 candidates (Pass 2's @@ -155,6 +158,7 @@ fn run(args: Vec) -> Result<(), Failure> { let mut queries: Vec<(QueryLanguage, String, Option)> = Vec::new(); let mut tables = Vec::new(); let (mut epsilon, mut delta, mut interval_ms) = (None, None, 15_000u64); + let mut metric: Option = None; let mut max_displayed = 64usize; let mut max_combinations = MAX_COMBINATIONS; let mut out = None; @@ -180,6 +184,13 @@ fn run(args: Vec) -> Result<(), Failure> { "--table" => tables.push(value()?), "--epsilon" => epsilon = Some(number(value()?)?), "--delta" => delta = Some(number(value()?)?), + "--metric" => { + let name = value()?; + metric = Some( + serde_json::from_value(Value::String(name.clone())) + .map_err(|_| format!("unknown metric {name}"))?, + ) + } "--interval-ms" => interval_ms = value()?.parse().map_err(|e| format!("{e}"))?, "--max-displayed" => max_displayed = value()?.parse().map_err(|e| format!("{e}"))?, "--max-combinations" => { @@ -212,11 +223,16 @@ fn run(args: Vec) -> Result<(), Failure> { } (None, false) => { let accuracy = match (epsilon, delta) { - (None, None) => AccuracyTarget::Exact, - (Some(epsilon), None) => AccuracyTarget::Epsilon(epsilon), - (Some(epsilon), Some(delta)) => AccuracyTarget::EpsilonDelta { epsilon, delta }, + (None, None) => RequestedAccuracy::exact(), + (Some(epsilon), None) => RequestedAccuracy::epsilon(epsilon), + (Some(epsilon), Some(delta)) => RequestedAccuracy::epsilon_delta(epsilon, delta), (None, Some(_)) => return Err("--delta needs --epsilon".into()), }; + let accuracy = match metric { + Some(_) if epsilon.is_none() => return Err("--metric needs --epsilon".into()), + Some(metric) => accuracy.with_metric(metric), + None => accuracy, + }; let catalog = sql_catalog(&tables)?; let mut names = HashSet::new(); let (mut described, mut roots, mut recurrence) = (Vec::new(), Vec::new(), Vec::new()); @@ -403,12 +419,18 @@ fn workload_queries(workload: &PlanningWorkload) -> Vec { .entries() .enumerate() .map(|(index, entry)| { - let accuracy = match entry.requirements.accuracy.target() { - AccuracyTarget::Exact => json!("exact"), - AccuracyTarget::Epsilon(epsilon) => json!({ "epsilon": epsilon }), - AccuracyTarget::EpsilonDelta { epsilon, delta } => { - json!({ "epsilon": epsilon, "delta": delta }) + 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 mut requirements = json!({ "accuracy": accuracy }); if let LatencyRequirement::ExplicitMaxMs(ms) = entry.requirements.response_latency { @@ -486,7 +508,7 @@ fn sql_catalog(tables: &[String]) -> Result { fn query_batch( language: QueryLanguage, queries: &[String], - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, interval_ms: u64, ) -> PlanningWorkload { PlanningWorkload { @@ -549,7 +571,7 @@ mod tests { let workload = query_batch( QueryLanguage::PromQL, &["sum(m)".into()], - AccuracyTarget::Exact, + RequestedAccuracy::exact(), 15_000, ); let run = diff --git a/crates/devtools/src/bin/variant_coverage.rs b/crates/devtools/src/bin/variant_coverage.rs index 83d797005..7a2a848d6 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::SqlDialect; use std::collections::BTreeSet; use std::rc::Rc; @@ -296,7 +296,11 @@ 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, AccuracyTarget::Exact, interval_ms) { + match lower_promql_with_data_ingestion_interval( + q, + RequestedAccuracy::exact(), + interval_ms, + ) { Ok(qe) => { walk(&qe, &mut variants); lowered += 1; @@ -328,7 +332,7 @@ async fn main() { &q, &catalog, SqlDialect::DataFusionSQL, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await { @@ -361,7 +365,7 @@ async fn main() { &q, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await { diff --git a/crates/devtools/src/lib.rs b/crates/devtools/src/lib.rs index 3a44b6cee..d83df3372 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::AccuracyTarget, + accuracy: asap_types::types::RequestedAccuracy, 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 b9f38a639..6789bd995 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataArrival, DataDistribution, DataWorkload, DurationMs, Evidence, EvidenceSource, LatencyRequirement, MetricType, PlanningWorkload, Predictability, @@ -71,15 +71,12 @@ pub fn example1() -> PlanningWorkload { repeating_queries: Some(vec![ panel( EXAMPLE1_Q1, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), LatencyRequirement::Unspecified, ), panel( EXAMPLE1_Q2, - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }, + RequestedAccuracy::epsilon_delta(0.01, 0.001), LatencyRequirement::ExplicitMaxMs(100.0), ), ]), @@ -124,10 +121,9 @@ pub enum PatternARecurrence { fn pattern_a_requirements() -> QueryRequirements { QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::EpsilonDelta { - epsilon: 0.005, - delta: 0.01, - }), + accuracy: AccuracyRequirement::Explicit( + RequestedAccuracy::epsilon_delta(0.005, 0.01).with_metric(ErrorMetric::Rank), + ), response_latency: LatencyRequirement::Unspecified, } } @@ -205,10 +201,9 @@ pub fn pattern_b() -> PlanningWorkload { PATTERN_B_INTERVAL_MS as u32, )), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }), + accuracy: AccuracyRequirement::Explicit( + RequestedAccuracy::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank), + ), response_latency: LatencyRequirement::ExplicitMaxMs(200.0), }, predictability: Predictability::Predictable { known_at: None }, diff --git a/crates/devtools/src/planning_stages.rs b/crates/devtools/src/planning_stages.rs index 0468037c2..17fc08ef0 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::{DataWorkload, PlanningWorkload, QueryRecurrence}; #[derive(Debug)] @@ -114,7 +114,7 @@ pub fn run_planning_stages( let targets: Vec<_> = workload .query_workload .entries() - .map(|entry| entry.requirements.accuracy.target()) + .map(|entry| entry.requirements.accuracy.requested()) .collect(); let recurrence: Vec<_> = workload .query_workload @@ -130,7 +130,7 @@ pub fn run_planning_stages( /// evaluated as `recurrence[i]` says. pub fn run_planning_stages_over( roots: Vec, - targets: &[AccuracyTarget], + targets: &[RequestedAccuracy], recurrence: &[QueryRecurrence], data: &DataWorkload, max_combinations: Option, diff --git a/crates/devtools/tests/cross_language.rs b/crates/devtools/tests/cross_language.rs index 597362e99..3b0d9e76a 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), AccuracyTarget::Exact) + lower_sql(q, &catalog(), RequestedAccuracy::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, AccuracyTarget::Exact, 1_000) + lower_promql_with_data_ingestion_interval(q, RequestedAccuracy::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 fce25c9c2..27184d8bb 100644 --- a/crates/devtools/tests/stage_pipeline.rs +++ b/crates/devtools/tests/stage_pipeline.rs @@ -501,3 +501,26 @@ 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. +#[test] +fn a_quantile_target_names_its_metric() { + 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()); + assert!( + unnamed["stage3_plan_selection"]["error"] + .as_str() + .is_some_and(|error| error.contains("must name its metric")), + "{}", + unnamed["stage3_plan_selection"] + ); +} diff --git a/crates/frontend-common/src/resolve.rs b/crates/frontend-common/src/resolve.rs index e528aa9c5..24b354cf1 100644 --- a/crates/frontend-common/src/resolve.rs +++ b/crates/frontend-common/src/resolve.rs @@ -932,18 +932,18 @@ mod tests { }; let intent = AggIntent::Cardinality { cols: vec![qualified("b"), qualified("a")], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; assert_eq!( resolve_agg_intent(&intent, &schema).unwrap(), AggIntent::Cardinality { cols: vec![1, 0], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), } ); let missing = AggIntent::Cardinality { cols: vec![qualified("a"), ColumnRef::Named("missing".into())], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }; assert!(resolve_agg_intent(&missing, &schema).is_err()); } diff --git a/crates/frontend-metricsql/src/lib.rs b/crates/frontend-metricsql/src/lib.rs index 10d8b0ea1..4bf6e2b00 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; use asap_types::workload::{DurationMs, PlanningWorkload, QueryLanguage, WorkloadError}; use metricsql_parser::ast::{AggregateModifier, DurationExpr, Expr, MetricExpr, RollupExpr}; use metricsql_parser::functions::{AggregateFunction, BuiltinFunction, RollupFunction}; @@ -68,7 +68,7 @@ pub fn lower_metricsql_query_workload( .map(|entry| { lower_metricsql_query( &entry.query.0, - entry.requirements.accuracy.target(), + entry.requirements.accuracy.requested(), Duration::from_millis(interval_ms), ) }) @@ -77,7 +77,7 @@ pub fn lower_metricsql_query_workload( pub fn lower_metricsql( query: &str, - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, 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: AccuracyTarget, + accuracy: RequestedAccuracy, ingestion_interval: Duration, ) -> Result { let ast = parse_metricsql(query)?; @@ -110,7 +110,7 @@ pub fn lower_metricsql_query( } struct Lowerer { - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, ingestion_interval: Duration, } @@ -221,7 +221,7 @@ impl Lowerer { RollupFunction::MaxOverTime => AggIntent::Max { col: None }, RollupFunction::SumOverTime => AggIntent::Sum { col: None }, RollupFunction::CountOverTime => AggIntent::Count { - accuracy: self.accuracy.clone(), + accuracy: self.accuracy.target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }, RollupFunction::StddevOverTime => AggIntent::StdDev { col: None, @@ -245,7 +245,7 @@ impl Lowerer { RollupFunction::QuantileOverTime => AggIntent::Quantile { col: None, q: number_arg(&function.args, 0)?, - accuracy: self.accuracy.clone(), + accuracy: self.accuracy.for_quantile(), }, _ => { return Err(unsupported(format!( @@ -295,7 +295,7 @@ impl Lowerer { AggregateFunction::Max => AggIntent::Max { col: None }, // `count(v)` counts series, not distinct sample values. AggregateFunction::Count => AggIntent::Count { - accuracy: self.accuracy.clone(), + accuracy: self.accuracy.target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }, AggregateFunction::StdDev => AggIntent::StdDev { col: None, @@ -309,7 +309,7 @@ impl Lowerer { AggregateFunction::Quantile => AggIntent::Quantile { col: None, q: number_arg(&expr.args, 0)?, - accuracy: self.accuracy.clone(), + accuracy: self.accuracy.for_quantile(), }, _ => return Err(unsupported(format!("aggregate `{}`", expr.name()))), }; diff --git a/crates/frontend-metricsql/tests/lowering.rs b/crates/frontend-metricsql/tests/lowering.rs index 549edfadd..3785e747b 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; /// 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, AccuracyTarget::Epsilon(0.01), INTERVAL).unwrap() + lower_metricsql(query, RequestedAccuracy::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", - AccuracyTarget::Exact, + RequestedAccuracy::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, AccuracyTarget::Exact, INTERVAL); + let result = lower_metricsql(query, RequestedAccuracy::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, AccuracyTarget::Exact, INTERVAL).unwrap_err(); + let error = lower_metricsql(query, RequestedAccuracy::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 ea24bf1e4..52572b069 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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)", AccuracyTarget::Exact).unwrap(); + let node = lower_metricsql("count(up)", RequestedAccuracy::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, AccuracyTarget::Exact).unwrap_err(); + let error = lower_metricsql(query, RequestedAccuracy::exact()).unwrap_err(); assert!( matches!(&error, MetricsqlError::UnsupportedFeature(message) if message.contains("evaluation step")), "{query}: {error:?}" ); } - assert!(lower_metricsql("rate(m[5m])", AccuracyTarget::Exact).is_ok()); + assert!(lower_metricsql("rate(m[5m])", RequestedAccuracy::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)", AccuracyTarget::Exact).unwrap(); + let node = lower_metricsql("sum(up)", RequestedAccuracy::exact()).unwrap(); assert_eq!( time_range_under_aggregate(&node), (INTERVAL, TimeRangeKind::Instant) ); - let node = lower_metricsql("rate(m[5m])", AccuracyTarget::Exact).unwrap(); + let node = lower_metricsql("rate(m[5m])", RequestedAccuracy::exact()).unwrap(); assert_eq!( time_range_under_aggregate(&node), (Duration::from_secs(300), TimeRangeKind::Range) diff --git a/crates/frontend-promql/src/lib.rs b/crates/frontend-promql/src/lib.rs index e7d99fe4c..b252ef952 100644 --- a/crates/frontend-promql/src/lib.rs +++ b/crates/frontend-promql/src/lib.rs @@ -101,7 +101,7 @@ fn lower_promql_query_workload_inner( .map(|entry| { let root = promql::PromqlLowerer::lower_query_with_ingestion_interval( &entry.query.0, - &entry.requirements.accuracy.target(), + &entry.requirements.accuracy.requested(), std::time::Duration::from_millis(interval_ms), )?; Ok(root) diff --git a/crates/frontend-promql/src/promql.rs b/crates/frontend-promql/src/promql.rs index 0e8b3df1c..0e35e9e74 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; /// 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: &AccuracyTarget, + accuracy: &RequestedAccuracy, 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(AccuracyTarget::Exact) }; + static ACCURACY: std::cell::RefCell = + const { std::cell::RefCell::new(RequestedAccuracy::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(AccuracyTarget); +struct AccuracyGuard(RequestedAccuracy); impl AccuracyGuard { - fn install(accuracy: AccuracyTarget) -> Self { + fn install(accuracy: RequestedAccuracy) -> Self { let prev = ACCURACY.with(|a| a.replace(accuracy)); AccuracyGuard(prev) } @@ -233,12 +233,13 @@ impl AccuracyGuard { impl Drop for AccuracyGuard { fn drop(&mut self) { - ACCURACY.with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, AccuracyTarget::Exact)); + ACCURACY + .with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, RequestedAccuracy::exact())); } } /// The ambient accuracy target installed by the current [`PromqlLowerer::lower`] call. -fn current_accuracy() -> AccuracyTarget { +fn current_accuracy() -> RequestedAccuracy { ACCURACY.with(|a| a.borrow().clone()) } @@ -784,7 +785,7 @@ fn build_over_sub_dag(outer: Outer, keys: Vec, child: Unresolved) -> keys, AggIntent::TopK { k: k as usize, - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }, child, )); @@ -851,7 +852,7 @@ fn walk_histogram(call: &Call) -> Result { let func = AggIntent::Quantile { col: None, q: phi, - accuracy: current_accuracy(), + accuracy: current_accuracy().for_quantile(), }; return Ok(outer_aggregate(vec![], func, walk(arg_expr)?)); } @@ -922,7 +923,7 @@ fn walk_histogram_quantiles(call: &Call) -> Result { let intent = AggIntent::Quantile { col: None, q: phi, - accuracy: current_accuracy(), + accuracy: current_accuracy().for_quantile(), }; Unresolved::Aggregate { reduction: reduction_for(&[], intent.is_per_series()), @@ -1690,7 +1691,7 @@ fn build(inner: Inner, keys: Vec, outer: Outer) -> Result reduction: Reduction::Reduce(keys.into()), measures: vec![AggIntent::TopK { k: k as usize, - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }], output_names: vec![], filters: vec![], @@ -1732,7 +1733,7 @@ fn build(inner: Inner, keys: Vec, outer: Outer) -> Result reduction: Reduction::Reduce(keys.into()), measures: vec![AggIntent::TopK { k: k as usize, - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }], output_names: vec![], filters: vec![], @@ -1883,7 +1884,7 @@ fn ingestion_lookback(child: Unresolved) -> Unresolved { /// Count vector elements regardless of their sample values. fn count() -> AggIntent { AggIntent::Count { - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE_STREAM_L1), } } @@ -1891,20 +1892,20 @@ fn inner_intent(f: &InnerFunc) -> AggIntent { match f { InnerFunc::FrequencyL2 => AggIntent::FrequencyL2 { col: None, - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE), }, InnerFunc::FrequencyEntropy => AggIntent::FrequencyEntropy { col: None, - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE), }, InnerFunc::Cardinality => AggIntent::Cardinality { cols: vec![], - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::Relative), }, InnerFunc::Quantile(q) => AggIntent::Quantile { col: None, q: *q, - accuracy: current_accuracy(), + accuracy: current_accuracy().for_quantile(), }, InnerFunc::Avg => AggIntent::Avg { col: None }, InnerFunc::Min => AggIntent::Min { col: None }, @@ -1919,7 +1920,7 @@ fn inner_intent(f: &InnerFunc) -> AggIntent { population: true, }, InnerFunc::Count => AggIntent::Count { - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }, InnerFunc::Rate => AggIntent::Rate, InnerFunc::IRate => AggIntent::IRate, @@ -1961,7 +1962,7 @@ fn outer_intent(o: &OuterIntent) -> AggIntent { OuterIntent::Quantile(q) => AggIntent::Quantile { col: None, q: *q, - accuracy: current_accuracy(), + accuracy: current_accuracy().for_quantile(), }, OuterIntent::Group => AggIntent::Group, } diff --git a/crates/frontend-promql/tests/count_planning.rs b/crates/frontend-promql/tests/count_planning.rs index 40a07b8bd..1472bdb92 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use std::rc::Rc; use support::{logical_candidates, lower_promql, post_asap_dag, selected_dag}; @@ -26,10 +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 = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let target = RequestedAccuracy::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( @@ -46,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, AccuracyTarget::Exact).unwrap(); - let selected = selected_dag(root, AccuracyTarget::Exact); + let root = lower_promql(query, RequestedAccuracy::exact()).unwrap(); + let selected = selected_dag(root, RequestedAccuracy::exact()); assert!( OperatorNode::reachable(&selected).iter().any(|node| { matches!( @@ -67,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 = AccuracyTarget::Epsilon(0.02); + let accuracy = RequestedAccuracy::epsilon(0.02); let root = lower_promql(query, accuracy.clone()).unwrap(); let candidates = logical_candidates(root, accuracy); let mut algorithms = Vec::new(); @@ -141,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, AccuracyTarget::Exact).unwrap(); + let root = lower_promql(query, RequestedAccuracy::exact()).unwrap(); let NonASAPOp::Aggregate { reduction, measures, @@ -230,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 = AccuracyTarget::Epsilon(0.02); + let accuracy = RequestedAccuracy::epsilon(0.02); let root = lower_promql("count_over_time(up[5m])", accuracy.clone()).unwrap(); let candidates = logical_candidates(root, accuracy); let dag = candidates @@ -284,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 = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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 aba373804..d1942c984 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact).unwrap()) + quantile_kind(&lower_promql(q, RequestedAccuracy::exact()).unwrap()) } fn with_meta(q: &str, catalog: HistogramCatalog) -> &'static str { - quantile_kind(&lower_promql_with_histograms(q, AccuracyTarget::Exact, catalog).unwrap()) + quantile_kind(&lower_promql_with_histograms(q, RequestedAccuracy::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)", - AccuracyTarget::Exact + RequestedAccuracy::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, AccuracyTarget::Exact).is_err()); + assert!(lower_promql(q, RequestedAccuracy::exact()).is_err()); assert_eq!( with_meta( q, @@ -98,8 +98,10 @@ 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, AccuracyTarget::Exact, catalog.clone()).is_err()); - assert!(lower_promql_with_histograms("foo_bucket", AccuracyTarget::Exact, catalog).is_err()); + assert!(lower_promql_with_histograms(q, RequestedAccuracy::exact(), catalog.clone()).is_err()); + assert!( + lower_promql_with_histograms("foo_bucket", RequestedAccuracy::exact(), catalog).is_err() + ); } #[test] @@ -116,7 +118,7 @@ fn undeclared_metric_falls_back_to_the_heuristic() { ); assert!(lower_promql_with_histograms( "histogram_quantile(0.9, native_thing)", - AccuracyTarget::Exact, + RequestedAccuracy::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 b4130bf86..f04f886ec 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; // 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)", AccuracyTarget::Exact).unwrap(); + let root = support::lower_promql("sum(a)", RequestedAccuracy::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 5e78e261d..66e354631 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact) + lower_promql(q, RequestedAccuracy::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, AccuracyTarget::Exact) { + match lower_promql(q, RequestedAccuracy::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 9cea886fc..73256382b 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Epsilon(0.01)) { + match lower_promql(query, RequestedAccuracy::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, AccuracyTarget::Epsilon(0.01)).unwrap(); + let root = lower_promql(query, RequestedAccuracy::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 cd371be69..6d6e781e6 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact) { + match lower_promql(q, RequestedAccuracy::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 8a3529ff4..ae84468aa 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact), + &support::workload(q, RequestedAccuracy::exact()), 0, ) { Ok(_) => t.lowered += 1, @@ -75,7 +75,7 @@ struct BindTally { errored: usize, } -fn bind_tally(corpus: &str, accuracy: AccuracyTarget) -> BindTally { +fn bind_tally(corpus: &str, accuracy: RequestedAccuracy) -> 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: AccuracyTarget) -> BindTally { #[test] fn binding_is_total_over_the_entire_corpus() { - let accuracy = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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 416b6680d..a8597e316 100644 --- a/crates/frontend-promql/tests/promql_binding_regressions.rs +++ b/crates/frontend-promql/tests/promql_binding_regressions.rs @@ -1,6 +1,5 @@ -use asap_types::types::AccuracyTarget; - mod support; +use asap_types::types::RequestedAccuracy; use support::lower_promql; /// Prometheus treats these quantile parameters as valid queries returning special values. @@ -13,7 +12,7 @@ fn quantile_parameters_retain_prometheus_special_value_semantics() { format!("histogram_quantile({parameter}, smoke_bucket)"), ] { assert!( - lower_promql(&query, AccuracyTarget::Exact).is_ok(), + lower_promql(&query, RequestedAccuracy::exact()).is_ok(), "{query}" ); } @@ -24,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})"), AccuracyTarget::Exact).unwrap(); - let irate = lower_promql(&format!("irate({input})"), AccuracyTarget::Exact).unwrap(); + let rate = lower_promql(&format!("rate({input})"), RequestedAccuracy::exact()).unwrap(); + let irate = lower_promql(&format!("irate({input})"), RequestedAccuracy::exact()).unwrap(); assert_ne!(rate, irate, "rate and irate must not collapse: {input}"); } } @@ -35,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)", AccuracyTarget::Exact).unwrap(); + let tree = lower_promql("count(smoke_gauge)", RequestedAccuracy::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 026468734..2edf7546a 100644 --- a/crates/frontend-promql/tests/promql_conformance.rs +++ b/crates/frontend-promql/tests/promql_conformance.rs @@ -44,20 +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, AccuracyTarget::Exact) + lower_promql(q, RequestedAccuracy::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, AccuracyTarget::Exact) { + match lower_promql(q, RequestedAccuracy::exact()) { Err(e) => e, Ok(dag) => panic!("expected {q:?} to be rejected, but it lowered to: {dag:?}"), } @@ -692,25 +693,22 @@ 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)", AccuracyTarget::Exact).unwrap(); + let exact = lower_promql("count by (job) (up)", RequestedAccuracy::exact()).unwrap(); assert!( has(&exact, |i| matches!( i, - AggIntent::Count { - accuracy: AccuracyTarget::Exact - } + AggIntent::Count { accuracy } if accuracy.is_exact() )), - "Count must stay Exact under AccuracyTarget::Exact, got {:?}", + "Count must stay exact under an exact target, got {:?}", intents(&exact) ); - let approx = lower_promql("count by (job) (up)", AccuracyTarget::Epsilon(0.01)).unwrap(); + let approx = lower_promql("count by (job) (up)", RequestedAccuracy::epsilon(0.01)).unwrap(); assert!( has(&approx, |i| matches!( i, - AggIntent::Count { - accuracy: AccuracyTarget::Epsilon(e) - } if (*e - 0.01).abs() < 1e-9 + AggIntent::Count { accuracy } if (accuracy.error - 0.01).abs() < 1e-9 + && accuracy.metric == 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 ac3c5a06f..fa5efc803 100644 --- a/crates/frontend-promql/tests/promql_equivalence.rs +++ b/crates/frontend-promql/tests/promql_equivalence.rs @@ -21,11 +21,12 @@ use std::rc::Rc; mod support; use asap_types::ir::OperatorNode; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use support::lower_promql; fn lo(q: &str) -> Rc { - lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("{q:?} should lower: {e}")) + lower_promql(q, RequestedAccuracy::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 6ccddeec5..8138f04d2 100644 --- a/crates/frontend-promql/tests/promql_lowering.rs +++ b/crates/frontend-promql/tests/promql_lowering.rs @@ -9,7 +9,7 @@ use asap_types::ir::{ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, BinaryOpKind, CompareOpKind, Reduction, ScalarValue, Source, }; -use asap_types::types::AccuracyTarget; +use asap_types::types::{AccuracyTarget, ErrorMetric}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, @@ -17,10 +17,12 @@ 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, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } #[test] @@ -32,14 +34,14 @@ fn frequency_extensions_lower_to_explicit_frequency_statistics() { "entropy_over_time(cpu_usage[5m])", AggIntent::FrequencyEntropy { col: None, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, ), ( "l2_over_time(cpu_usage[5m])", AggIntent::FrequencyL2 { col: None, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, ), ] { @@ -58,13 +60,13 @@ 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 [AccuracyTarget::Exact, AccuracyTarget::Epsilon(0.02)] { + for accuracy in [RequestedAccuracy::exact(), RequestedAccuracy::epsilon(0.02)] { let dag = lower_promql(query, accuracy.clone()).unwrap(); let mut intents = Vec::new(); collect_intents(&dag, &mut intents); assert!( intents.iter().any(|intent| matches!( - intent, AggIntent::Cardinality { accuracy: actual, .. } if actual == &accuracy + intent, AggIntent::Cardinality { accuracy: actual, .. } if *actual == accuracy.target_in(ErrorMetric::Relative) )), "{query}: {dag:?}" ); @@ -382,7 +384,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])))", - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .unwrap_err(); assert!(error.to_string().contains("le"), "{error}"); @@ -1029,7 +1031,7 @@ fn nan_or_overflowing_topk_k_is_rejected() { "limitk(Inf, http_requests)", ] { assert!( - lower_promql(q, AccuracyTarget::Exact).is_err(), + lower_promql(q, RequestedAccuracy::exact()).is_err(), "{q} should be rejected" ); } @@ -1044,7 +1046,7 @@ fn out_of_range_quantile_phi_is_accepted() { "histogram_quantile(2.0, rate(b_bucket[5m]))", ] { assert!( - lower_promql(query, AccuracyTarget::Exact).is_ok(), + lower_promql(query, RequestedAccuracy::exact()).is_ok(), "{query}" ); } @@ -1055,7 +1057,11 @@ fn function_wrapped_range_vector_is_rejected_not_stripped() { // `rate(abs(m[5m]))` must NOT silently lower as `rate(m[5m])` — the wrapper // is rejected (here, at parse or in extract_matrix), never stripped. assert!( - lower_promql("rate(abs(http_requests_total[5m]))", AccuracyTarget::Exact).is_err(), + lower_promql( + "rate(abs(http_requests_total[5m]))", + RequestedAccuracy::exact() + ) + .is_err(), "function-wrapped range vector should be rejected" ); } @@ -1065,7 +1071,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, AccuracyTarget::Exact).unwrap_err(); + let err = lower_promql(&q, RequestedAccuracy::exact()).unwrap_err(); assert!(format!("{err}").contains("nesting"), "got {err}"); } @@ -1084,7 +1090,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, AccuracyTarget::Exact) { + match lower_promql(q, RequestedAccuracy::exact()) { Err(LoweringError::UnsupportedFeature(m)) if m.contains("`fill`") => {} other => panic!("expected fill rejection for {q:?}, got {other:?}"), } @@ -1097,7 +1103,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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let NonASAPOp::Aggregate { measures, .. } = qe.expect_non_asap() else { @@ -1105,7 +1111,7 @@ fn accuracy_target_flows_into_quantile_intent() { }; assert!(matches!( &measures[0], - AggIntent::Quantile { accuracy: AccuracyTarget::Epsilon(e), .. } if (*e - 0.01).abs() < 1e-12 + AggIntent::Quantile { accuracy, .. } if (accuracy.error - 0.01).abs() < 1e-12 )); } @@ -1174,7 +1180,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(AccuracyTarget::Epsilon(0.02)), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon(0.02)), ..Default::default() }, predictability: Predictability::Unknown, @@ -1383,7 +1389,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)"#, - AccuracyTarget::Exact + RequestedAccuracy::exact() ) .is_err()); } @@ -1655,7 +1661,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, AccuracyTarget::Exact).is_err(), + lower_promql(q, RequestedAccuracy::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 09fa3d9b6..1b5410655 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; fn root(query: &str) -> QueryRoot { asap_frontend_promql::lower_promql_query_workload( - &support::workload(query, AccuracyTarget::Exact), + &support::workload(query, RequestedAccuracy::exact()), 0, ) .unwrap() diff --git a/crates/frontend-promql/tests/support.rs b/crates/frontend-promql/tests/support.rs index d1424a541..c3ee039f4 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, TimeSelection, }; -pub fn workload(query: &str, accuracy: AccuracyTarget) -> PlanningWorkload { +pub fn workload(query: &str, accuracy: RequestedAccuracy) -> PlanningWorkload { PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, @@ -41,7 +41,7 @@ pub fn workload(query: &str, accuracy: AccuracyTarget) -> PlanningWorkload { #[allow(dead_code)] pub fn lower_promql( query: &str, - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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: AccuracyTarget, + accuracy: RequestedAccuracy, 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, AccuracyTarget::Exact), + &workload(query, RequestedAccuracy::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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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: AccuracyTarget) -> Rc { +pub fn selected_dag(root: Rc, accuracy: RequestedAccuracy) -> 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: AccuracyTarget) -> Rc, - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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 7a250d1e4..e78717f9b 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; +use asap_types::types::{AccuracyTarget, RequestedAccuracy}; 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: AccuracyTarget) -> Rc { +fn candidate(query: &str, accuracy: RequestedAccuracy) -> 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 = AccuracyTarget::Epsilon(0.02); + let accuracy = RequestedAccuracy::epsilon(0.02); let roots: Vec<_> = [ ("distinct_over_time(m[5m])", accuracy.clone()), ("count_over_time(m[5m])", accuracy.clone()), @@ -83,9 +83,12 @@ fn four_evaluations_size_their_value_frequency_states_and_keep_honest_guarantees // source's distinct bound, which Stage 1 does not read. 0 => { let guarantee = guarantee.expect("a distinct-count guarantee"); - assert_eq!(guarantee.metric, ErrorMetric::Cardinality); - assert_eq!(guarantee.bound.evaluate().map(|e| e <= 0.02), Some(true)); - assert_eq!(guarantee.failure_probability.evaluate(), None); + assert_eq!(guarantee.spec.metric, ErrorMetric::Relative); + assert_eq!( + guarantee.spec.error.evaluate().map(|e| e <= 0.02), + Some(true) + ); + assert_eq!(guarantee.spec.failure_probability.evaluate(), None); assert!(DefaultAccuracyModel .local_guarantee(family, query) .is_none()); @@ -99,9 +102,12 @@ fn four_evaluations_size_their_value_frequency_states_and_keep_honest_guarantees } 2 => { let guarantee = guarantee.expect("an L2 guarantee"); - assert_eq!(guarantee.metric, ErrorMetric::RelativeValue); - assert!(DefaultAccuracyModel.satisfies(guarantee, &AccuracyTarget::Epsilon(0.02))); - assert!(guarantee.failure_probability.evaluate().unwrap() <= 0.01); + assert_eq!(guarantee.spec.metric, ErrorMetric::Relative); + assert!(DefaultAccuracyModel.satisfies( + guarantee, + &AccuracyTarget::epsilon(0.02).with_metric(ErrorMetric::Relative) + )); + assert!(guarantee.spec.failure_probability.evaluate().unwrap() <= 0.01); } // Entropy has no certified bound, so composition certifies nothing. _ => { @@ -122,12 +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 [ - AccuracyTarget::Exact, - AccuracyTarget::Epsilon(0.02), - AccuracyTarget::EpsilonDelta { - epsilon: 0.02, - delta: 0.01, - }, + RequestedAccuracy::exact(), + RequestedAccuracy::epsilon(0.02), + RequestedAccuracy::epsilon_delta(0.02, 0.01), ] { let root = lower_promql(query, target.clone()).unwrap(); let run = run_stage1_to_stage3(root, target.clone()); @@ -141,7 +144,7 @@ fn uncalibrated_frequency_evaluations_do_not_bypass_accuracy_targets() { univmon_estimate(node).is_some() && node.guarantee.is_none() }) .collect(); - if target == AccuracyTarget::Exact { + if target.is_exact() { assert!(unknown.is_empty(), "{query}"); } else { assert!(!unknown.is_empty(), "{query}"); diff --git a/crates/frontend-sql/src/lib.rs b/crates/frontend-sql/src/lib.rs index 1747b09c8..e65a25f0a 100644 --- a/crates/frontend-sql/src/lib.rs +++ b/crates/frontend-sql/src/lib.rs @@ -15,7 +15,7 @@ use std::rc::Rc; use asap_frontend_common::resolve_root; use asap_types::ir::OperatorNode; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::{QueryLanguage, QueryWorkload, SqlDialect}; pub use error::SqlError; @@ -30,7 +30,7 @@ pub use sql::{SqlCatalog, SqlLowerer}; pub async fn lower_sql( query: &str, catalog: &SqlCatalog, - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> Result, SqlError> { lower_sql_dialect(query, catalog, SqlDialect::DataFusionSQL, accuracy).await } @@ -47,7 +47,7 @@ pub async fn lower_sql_dialect( query: &str, catalog: &SqlCatalog, dialect: SqlDialect, - accuracy: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> Result, SqlError> { let unresolved = SqlLowerer::with_dialect(catalog, dialect) .lower(query, &accuracy) @@ -96,7 +96,7 @@ pub async fn lower_sql_batch( let mut results = Vec::with_capacity(entries.len()); for entry in entries { - let accuracy = entry.requirements.accuracy.target(); + let accuracy = entry.requirements.accuracy.requested(); results.push(lower_sql_dialect(&entry.query.0, catalog, dialect.clone(), accuracy).await); } results diff --git a/crates/frontend-sql/src/sql/mod.rs b/crates/frontend-sql/src/sql/mod.rs index 3f873d071..d11a1afb3 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; 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(AccuracyTarget::Exact) }; + static ACCURACY: std::cell::RefCell = + const { std::cell::RefCell::new(RequestedAccuracy::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(AccuracyTarget); +struct AccuracyGuard(RequestedAccuracy); impl AccuracyGuard { - fn install(accuracy: AccuracyTarget) -> Self { + fn install(accuracy: RequestedAccuracy) -> Self { let prev = ACCURACY.with(|a| a.replace(accuracy)); AccuracyGuard(prev) } @@ -106,11 +106,12 @@ impl AccuracyGuard { impl Drop for AccuracyGuard { fn drop(&mut self) { - ACCURACY.with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, AccuracyTarget::Exact)); + ACCURACY + .with(|a| *a.borrow_mut() = std::mem::replace(&mut self.0, RequestedAccuracy::exact())); } } -fn current_accuracy() -> AccuracyTarget { +fn current_accuracy() -> RequestedAccuracy { ACCURACY.with(|a| a.borrow().clone()) } @@ -175,7 +176,7 @@ impl<'a> SqlLowerer<'a> { pub async fn lower( &self, sql: &str, - accuracy: &AccuracyTarget, + accuracy: &RequestedAccuracy, ) -> Result { let ctx = self.build_context()?; let state = ctx.state(); @@ -1783,11 +1784,11 @@ fn lower_agg_intent(expr: &Expr) -> Result, LoweringError> } args => AggIntent::Cardinality { cols: args.iter().map(distinct_col).collect::>()?, - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::Relative), }, }, AggSemantic::Count => AggIntent::Count { - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::ABSOLUTE_STREAM_L1), }, AggSemantic::Sum => AggIntent::Sum { col: col(&agg_fn.params.args)?, @@ -1828,11 +1829,11 @@ fn lower_agg_intent(expr: &Expr) -> Result, LoweringError> _ => extract_percentile_q(&agg_fn.params.args)?, }, }, - accuracy: current_accuracy(), + accuracy: current_accuracy().for_quantile(), }, AggSemantic::Cardinality => AggIntent::Cardinality { cols: vec![reducer_col(&name, &agg_fn.params.args)?], - accuracy: current_accuracy(), + accuracy: current_accuracy().target_in(ErrorMetric::Relative), }, }) } diff --git a/crates/frontend-sql/tests/bgp_analytics/bgp_analytics.rs b/crates/frontend-sql/tests/bgp_analytics/bgp_analytics.rs index cc24329cd..0432570b2 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::SqlDialect; use datafusion::error::DataFusionError; @@ -100,7 +100,7 @@ async fn lower(q: &str) -> Result, LoweringError> { q, &catalog(), SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact, + RequestedAccuracy::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 d7c750d7d..175a004fb 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, - AccuracyTarget::Exact, + RequestedAccuracy::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 cce394a08..db7f32294 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), AccuracyTarget::Exact) + lower_sql(q, &catalog(), RequestedAccuracy::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, AccuracyTarget::Exact).await { + match lower_sql(&q, &cat, RequestedAccuracy::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 83cff5ec9..e3a8d22a2 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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, AccuracyTarget::Exact).await { + match lower_sql(query, &cat, RequestedAccuracy::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/maintained_population.rs b/crates/frontend-sql/tests/maintained_population.rs index a1563223f..28043b810 100644 --- a/crates/frontend-sql/tests/maintained_population.rs +++ b/crates/frontend-sql/tests/maintained_population.rs @@ -1,6 +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::{ ir::{ apply_materialization_timings, cse::share_common_sub_dags, @@ -9,7 +10,6 @@ use asap_types::{ }, post_asap::maintained_population::{MaintainedPopulation, PopulationInput}, pre_asap::{DataType, Field, Schema}, - types::AccuracyTarget, }; use std::rc::Rc; @@ -21,7 +21,9 @@ async fn aggregate(q: &str) -> Rc { Field::plain("job", DataType::Utf8, false), ]), ); - lower_sql(q, &catalog, AccuracyTarget::Exact).await.unwrap() + lower_sql(q, &catalog, RequestedAccuracy::exact()) + .await + .unwrap() } /// The `MaintainPopulation` node a candidate's evaluation reads, and its spec. diff --git a/crates/frontend-sql/tests/netflow/netflow.rs b/crates/frontend-sql/tests/netflow/netflow.rs index da680d664..eed469f78 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), AccuracyTarget::Exact) + let qe = lower_sql(query, &catalog(), RequestedAccuracy::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 6f1b8dd47..8cc827f01 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), AccuracyTarget::Exact) + lower_sql(sql, &catalog(), RequestedAccuracy::exact()) .await .unwrap() } @@ -114,7 +114,7 @@ async fn corr_rejects_unrepresented_forms() { "SELECT corr(x) FROM a", ] { assert!( - lower_sql(sql, &catalog(), AccuracyTarget::Exact) + lower_sql(sql, &catalog(), RequestedAccuracy::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::AccuracyTarget::Exact)], + &[Some(asap_types::types::RequestedAccuracy::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 9444f835b..b2a10abf7 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), AccuracyTarget::Exact) + lower_sql(sql, &catalog(), RequestedAccuracy::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(), AccuracyTarget::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(), RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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, AccuracyTarget::Exact) + let qe = lower_sql(sql, &catalog, RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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, &AccuracyTarget::Exact) + .lower(sql, &RequestedAccuracy::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(), - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .await .expect("approx_median should lower"); @@ -1467,8 +1467,8 @@ async fn median_threads_the_accuracy_target() { assert!( matches!( measures.as_slice(), - [AggIntent::Quantile { accuracy: AccuracyTarget::Epsilon(e), .. }] - if (*e - 0.01).abs() < 1e-12 + [AggIntent::Quantile { accuracy, .. }] + if (accuracy.error - 0.01).abs() < 1e-12 ), "median must carry the workload's accuracy target, got {measures:?}" ); @@ -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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, AccuracyTarget::Exact) + let qe = lower_sql(sql, &catalog, RequestedAccuracy::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, AccuracyTarget::Exact) + let qe = lower_sql(sql, &catalog, RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap() @@ -2480,7 +2480,7 @@ async fn original_o11y_map_queries_lower_with_typed_results() { sql, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact + RequestedAccuracy::exact() ) .await .is_err()); @@ -2594,7 +2594,7 @@ async fn arg_selector_result_schema_tracks_selected_argument() { sql, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap(); @@ -2628,7 +2628,7 @@ async fn clickhouse_list_element_uses_canonical_typed_access() { sql, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap(); @@ -2644,7 +2644,7 @@ async fn clickhouse_list_element_uses_canonical_typed_access() { sql, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact + RequestedAccuracy::exact() ) .await .is_err(), @@ -2687,7 +2687,7 @@ async fn clickhouse_tuple_element_preserves_declared_field_metadata() { sql, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap(); @@ -2705,7 +2705,7 @@ async fn clickhouse_tuple_element_preserves_declared_field_metadata() { &sql, &catalog, SqlDialect::ClickhouseSQL, - AccuracyTarget::Exact + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap(); @@ -2755,7 +2755,7 @@ async fn composite_distinct_counts_tuples() { let single = lower_sql( "SELECT COUNT(DISTINCT a) FROM t", &cat, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, AccuracyTarget::Exact).await; + let result = lower_sql(sql, &catalog, RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), AccuracyTarget::Exact) + lower_sql(sql, &catalog(), RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap_err() diff --git a/crates/frontend-sql/tests/temporal_types.rs b/crates/frontend-sql/tests/temporal_types.rs index 7e12c6f33..82d1529c4 100644 --- a/crates/frontend-sql/tests/temporal_types.rs +++ b/crates/frontend-sql/tests/temporal_types.rs @@ -1,8 +1,6 @@ use asap_frontend_sql::{lower_sql, SqlCatalog}; -use asap_types::{ - pre_asap::schema::{DataType, Field, Schema}, - types::AccuracyTarget, -}; +use asap_types::pre_asap::schema::{DataType, Field, Schema}; +use asap_types::types::RequestedAccuracy; fn catalog() -> SqlCatalog { SqlCatalog::new().with_table( "t", @@ -18,7 +16,7 @@ async fn temporal_subtraction_rejects_unrepresentable_duration() { let error = lower_sql( "SELECT d - d AS elapsed FROM t", &catalog, - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await .unwrap_err(); @@ -35,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(), AccuracyTarget::Exact) + let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Date); @@ -48,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(), AccuracyTarget::Exact) + let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Interval); @@ -64,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(), AccuracyTarget::Exact).await; + let result = lower_sql(query, &catalog(), RequestedAccuracy::exact()).await; let error = result.expect_err(&format!("accepted unsupported duration: {query}")); assert!( error.to_string().contains("unsupported duration type"), @@ -81,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(), AccuracyTarget::Exact) + let node = lower_sql(query, &catalog(), RequestedAccuracy::exact()) .await .unwrap(); assert_eq!(node.schema.fields[0].dtype, DataType::Interval, "{query}"); @@ -95,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(), AccuracyTarget::Exact) + let node = lower_sql(query, &catalog(), RequestedAccuracy::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 435730358..892df98dd 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::AccuracyTarget; + use asap_types::types::RequestedAccuracy; 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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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::AccuracyTarget; + use asap_types::types::RequestedAccuracy; 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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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: AccuracyTarget) -> Rc { + pub fn selected_dag(root: Rc, accuracy: RequestedAccuracy) -> 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: AccuracyTarget, + accuracy: RequestedAccuracy, ) -> 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 13d2875b9..39821d80a 100644 --- a/crates/integration-tests/tests/aggregate.rs +++ b/crates/integration-tests/tests/aggregate.rs @@ -16,9 +16,11 @@ 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, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } fn node(op: NonASAPOp) -> Rc { @@ -85,7 +87,7 @@ fn q07_count_is_row_count() { agg( vec![], AggIntent::Count { - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() }, scan("http_requests_total", &[]), ), @@ -186,7 +188,7 @@ fn q10_quantile_cross_series() { AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() }, scan("http_requests_total", &[]), ), diff --git a/crates/integration-tests/tests/binary_op.rs b/crates/integration-tests/tests/binary_op.rs index 284501080..36f5e38fd 100644 --- a/crates/integration-tests/tests/binary_op.rs +++ b/crates/integration-tests/tests/binary_op.rs @@ -16,10 +16,11 @@ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, BinaryOpKind, CompareOpKind, GroupSide, Reduction, Source, VectorGrouping, VectorMatch, VectorMatchKind, }; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; fn lower(q: &str) -> Rc { - lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } fn node(op: NonASAPOp) -> Rc { diff --git a/crates/integration-tests/tests/exact_composition.rs b/crates/integration-tests/tests/exact_composition.rs index d4c449293..3bc45e981 100644 --- a/crates/integration-tests/tests/exact_composition.rs +++ b/crates/integration-tests/tests/exact_composition.rs @@ -37,6 +37,7 @@ 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 ──────────────────────────────────────────────────────────── @@ -135,7 +136,7 @@ fn custom_accuracy_rule_survives_root_target_and_materialization() { } let root = agg(vec![0], AggIntent::Max { col: None }, fine_quantile()); let space = asap_aware_mapping::replacement::search_workload_with_targets( - vec![("q", root, Some(AccuracyTarget::Exact))], + vec![("q", root, Some(AccuracyTarget::exact()))], &default_strategies_with(&StatsModel), &Model, ) @@ -160,7 +161,7 @@ fn custom_accuracy_rule_survives_root_target_and_materialization() { fn root_target_rejects_unproven_composition() { let root = agg(vec![0], AggIntent::Max { col: None }, fine_quantile()); let space = asap_aware_mapping::replacement::search_workload_with_targets( - vec![("q", root, Some(AccuracyTarget::Exact))], + vec![("q", root, Some(AccuracyTarget::exact()))], &default_strategies_with(&StatsModel), &asap_aware_mapping::DefaultAccuracyModel, ) @@ -334,7 +335,7 @@ fn every_exact_accumulator_is_finalized_before_an_outer_sketch() { agg( vec![2], AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, metric_scan(&["zone"]), ), @@ -808,7 +809,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]))", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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 07646b214..c2118b2cd 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use futures::StreamExt; fn catalog() -> SqlCatalog { @@ -135,15 +135,15 @@ fn execute(root: &Rc) -> Vec { out } -async fn lower(sql: &str, accuracy: AccuracyTarget) -> Rc { +async fn lower(sql: &str, accuracy: RequestedAccuracy) -> 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, AccuracyTarget::Exact).await, - AccuracyTarget::Exact, + lower(sql, RequestedAccuracy::exact()).await, + RequestedAccuracy::exact(), ) } @@ -204,7 +204,7 @@ fn with_summary_filter(root: &Rc, filter: &Predicate) -> Rc (Predicate, Schema) { - OperatorNode::reachable(&lower(sql, AccuracyTarget::Exact).await) + OperatorNode::reachable(&lower(sql, RequestedAccuracy::exact()).await) .into_iter() .find_map(|n| match n.non_asap() { Some(NonASAPOp::Aggregate { filters, child, .. }) => { @@ -222,10 +222,7 @@ async fn hand_filtered( family: impl Fn(&FieldDataType) -> bool, filter: &(Predicate, Schema), ) -> Vec> { - let target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let target = RequestedAccuracy::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) @@ -293,10 +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 = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let target = RequestedAccuracy::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/frontend_timestamps.rs b/crates/integration-tests/tests/frontend_timestamps.rs index 300f0cee7..412bca345 100644 --- a/crates/integration-tests/tests/frontend_timestamps.rs +++ b/crates/integration-tests/tests/frontend_timestamps.rs @@ -4,14 +4,15 @@ 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; /// 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()", AccuracyTarget::Exact).expect("lower PromQL time()"); + let promql = + lower_promql_root("time()", RequestedAccuracy::exact()).expect("lower PromQL time()"); assert!( matches!( promql, @@ -34,7 +35,7 @@ async fn promql_eval_time_and_sql_current_timestamp_remain_distinct() { let sql = lower_sql( "SELECT CURRENT_TIMESTAMP FROM metrics", &catalog, - AccuracyTarget::Exact, + RequestedAccuracy::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 5adb98d4c..4188666c6 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; mod physical_common; @@ -34,7 +34,24 @@ 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 { - let accuracy = AccuracyTarget::Epsilon(0.01); + selected_with(query, series_identity, RequestedAccuracy::epsilon(0.01)) +} + +/// [`selected`] for a quantile query, whose target names its metric (a +/// rank error). +fn selected_quantile(query: &str, series_identity: bool) -> Rc { + selected_with( + query, + series_identity, + RequestedAccuracy::epsilon(0.01).with_metric(ErrorMetric::Rank), + ) +} + +fn selected_with( + query: &str, + series_identity: bool, + accuracy: RequestedAccuracy, +) -> Rc { let mut root = lower_promql(query, accuracy.clone()).unwrap(); if series_identity { root = promql_rows::with_series_identity(&root).unwrap(); @@ -45,7 +62,7 @@ fn selected(query: &str, series_identity: bool) -> Rc { /// 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 = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::epsilon(0.01); let root = lower_promql(query, accuracy.clone()).unwrap(); logical_candidates(root, accuracy) .into_iter() @@ -244,7 +261,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)", AccuracyTarget::Epsilon(0.01)).unwrap(), + &lower_promql("topk by(job)(1, m)", RequestedAccuracy::epsilon(0.01)).unwrap(), ) .unwrap(); let root = MaintainedPopulationStrategy::new(std::slice::from_ref(&root)) @@ -355,7 +372,7 @@ fn arithmetic_over_different_selectors_matches_prometheus() { let (values, _) = execute(&dag, &job_samples(&overlapping), INGESTION_WINDOW, 300_000); assert_eq!(values, [10.0]); } - let root = selected( + let root = selected_quantile( "quantile(0.9, sum_over_time(m[1m]) + sum_over_time(n[1m]))", true, ); @@ -368,7 +385,7 @@ fn arithmetic_over_different_selectors_matches_prometheus() { #[test] #[ignore = "the two identical selectors stay separate until Pass 2 merges identical sub-DAGs (#580)"] fn arithmetic_over_one_selector_executes() { - let root = selected( + let root = selected_quantile( "quantile(0.9, sum_over_time(m[1m]) + sum_over_time(m[1m]))", true, ); diff --git a/crates/integration-tests/tests/nested.rs b/crates/integration-tests/tests/nested.rs index 26ac23a24..820034bd5 100644 --- a/crates/integration-tests/tests/nested.rs +++ b/crates/integration-tests/tests/nested.rs @@ -20,10 +20,11 @@ use asap_types::pre_asap::{ AggIntent, ArithmeticOpKind, AtModifier, BinaryOpKind, CompareOpKind, GroupKeys, PromQLVectorSetOpKind, Reduction, ScalarValue, Source, TimeShift, VectorMatch, VectorMatchKind, }; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; fn lower(q: &str) -> Rc { - lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } fn node(op: NonASAPOp) -> Rc { diff --git a/crates/integration-tests/tests/operator_design_examples.rs b/crates/integration-tests/tests/operator_design_examples.rs index c8ab2cb47..87284ff34 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), AccuracyTarget::Exact) + let root = lower_sql(query, &catalog(), RequestedAccuracy::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(), - AccuracyTarget::Exact, + RequestedAccuracy::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(), AccuracyTarget::Exact).await.unwrap(); + let root = lower_sql(query, &catalog(), RequestedAccuracy::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(AccuracyTarget::Exact), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::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 22b68d2e2..e5d643fd3 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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: AccuracyTarget) -> Rc { +async fn plan(sql: &str, accuracy: RequestedAccuracy) -> 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", - AccuracyTarget::Exact, + RequestedAccuracy::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", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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 f1a82490c..8ffbbaa3d 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; async fn targets(sql: &str) -> Vec { let catalog = SqlCatalog::new().with_table( @@ -21,10 +21,7 @@ async fn targets(sql: &str) -> Vec { Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let accuracy = RequestedAccuracy::epsilon_delta(0.01, 0.01); let root = lower_sql(sql, &catalog, accuracy) .await .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")); @@ -99,7 +96,7 @@ async fn a_filter_or_group_key_over_an_estimate_has_no_guarantee() { Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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", @@ -166,7 +163,7 @@ async fn a_sql_fold_over_estimates_leaves_its_row_count_unbound() { Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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(); @@ -177,7 +174,7 @@ async fn a_sql_fold_over_estimates_leaves_its_row_count_unbound() { assert!(!candidates.is_empty()); for candidate in candidates { let g = candidate.guarantee.clone().expect("a relative max"); - let delta = format!("{:?}", g.failure_probability); + let delta = format!("{:?}", g.spec.failure_probability); assert!(delta.contains("group_row_count"), "{delta}"); assert!(!delta.contains("input_row_count"), "{delta}"); } @@ -194,10 +191,7 @@ async fn heap_updates(key: &str, key_type: DataType, sql: &str) -> Vec<(String, Field::plain("bytes", DataType::Float64, false), ]), ); - let accuracy = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let accuracy = RequestedAccuracy::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_example1.rs b/crates/integration-tests/tests/planner_layering_example1.rs index abb9c331d..f2c62a2b0 100644 --- a/crates/integration-tests/tests/planner_layering_example1.rs +++ b/crates/integration-tests/tests/planner_layering_example1.rs @@ -599,7 +599,7 @@ fn materialized_case() -> Run { time_range.materialization = Materialization::MatAtQuery(Medium::Memory); let selection = select_plan( std::slice::from_ref(&p.candidate), - &[Some(AccuracyTarget::Exact), Some(AccuracyTarget::Exact)], + &[Some(AccuracyTarget::exact()), Some(AccuracyTarget::exact())], &[every_10s(), every_10s()], workload.data_workload.as_ref().unwrap(), PlanningModels::builtin(), diff --git a/crates/integration-tests/tests/planner_layering_example2.rs b/crates/integration-tests/tests/planner_layering_example2.rs index d7a913060..b0c87134d 100644 --- a/crates/integration-tests/tests/planner_layering_example2.rs +++ b/crates/integration-tests/tests/planner_layering_example2.rs @@ -23,7 +23,7 @@ use asap_types::post_asap::{ Field, ProbabilityExpr, Schema, SketchAlgorithm, SketchParams, SketchStatistic, }; use asap_types::pre_asap::DataType; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::{ AccuracyRequirement, DataArrival, DataDistribution, DataWorkload, DurationMs, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, @@ -56,10 +56,9 @@ fn panel(query: &str, (epsilon, delta): (f64, f64)) -> RepeatingEntry { query: Query(query.into()), demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::EpsilonDelta { - epsilon, - delta, - }), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon_delta( + epsilon, delta, + )), response_latency: LatencyRequirement::Unspecified, }, predictability: Predictability::Predictable { known_at: None }, @@ -143,7 +142,7 @@ async fn lower_sql(workload: &PlanningWorkload) -> Vec { &entry.query.0, &catalog(), SqlDialect::DataFusionSQL, - entry.requirements.accuracy.target(), + entry.requirements.accuracy.requested(), ) .await .unwrap_or_else(|e| panic!("{}: {e}", entry.query.0)); @@ -645,7 +644,7 @@ fn stage1_logical_asap_optimization_leaves_the_hll_failure_probability_symbolic( }; let guarantee = q1.guarantee.as_ref().expect("Q1 has a guarantee"); assert_eq!( - guarantee.failure_probability, + guarantee.spec.failure_probability, ProbabilityExpr::Unknown { statistic: "classic_hll_failure_probability[p=12](max_distinct[flows_src_ip])" .into() @@ -804,7 +803,8 @@ fn stage3_plan_selection_selected_kmv_plan_executes_end_to_end() { use std::sync::Arc; let workload = standin_workload(); - let run = run_stages(&workload, lower_promql(&workload)); + let roots = lower_promql(&workload); + let run = run_stages(&workload, roots.clone()); let kmv: Vec<_> = run .logical .iter() @@ -820,7 +820,8 @@ fn stage3_plan_selection_selected_kmv_plan_executes_end_to_end() { let entries: Vec<_> = workload.query_workload.entries().collect(); let targets: Vec<_> = entries .iter() - .map(|e| Some(e.requirements.accuracy.target())) + .zip(&roots) + .map(|(e, root)| Some(e.requirements.accuracy.requested().for_root(root).unwrap())) .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 0f2b36804..da1fdaea9 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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: AccuracyTarget) -> Vec> { +fn candidates(query: &str, accuracy: RequestedAccuracy) -> Vec> { use asap_aware_mapping::stage3_plan_selection::with_confirmed_weights; use asap_types::ir::QueryRoot; use asap_types::workload::{DataWorkload, MetricType}; @@ -389,8 +389,8 @@ 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 = AccuracyTarget::Exact; - let sketch = AccuracyTarget::Epsilon(0.02); + let exact = RequestedAccuracy::exact(); + let sketch = RequestedAccuracy::epsilon(0.02); let queries = [ ("sum_over_time(m[5m])", &exact), ("count_over_time(m[5m])", &exact), @@ -472,7 +472,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]))", - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .pop() .unwrap(); diff --git a/crates/integration-tests/tests/promql_numeric_regressions.rs b/crates/integration-tests/tests/promql_numeric_regressions.rs index 15dbe1567..7b810ea91 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use std::rc::Rc; /// The first logical candidate (in enumeration order) with an ASAP operator, /// else the exact one. -fn plan(query: &str, accuracy: AccuracyTarget) -> Rc { +fn plan(query: &str, accuracy: RequestedAccuracy) -> 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)", AccuracyTarget::Exact); + let node = plan("count(up)", RequestedAccuracy::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, AccuracyTarget::Exact); + let node = plan(query, RequestedAccuracy::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, AccuracyTarget::Exact); + let node = plan(query, RequestedAccuracy::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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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 = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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 = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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 = AccuracyTarget::Epsilon(0.01); + let accuracy = RequestedAccuracy::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 cb49b6aba..f14358b94 100644 --- a/crates/integration-tests/tests/promql_to_post_asap.rs +++ b/crates/integration-tests/tests/promql_to_post_asap.rs @@ -33,6 +33,8 @@ use asap_types::post_asap::{ 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 @@ -67,7 +69,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])", - AccuracyTarget::Epsilon(0.02), + RequestedAccuracy::epsilon(0.02), ) .unwrap(); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); @@ -86,7 +88,7 @@ fn distinct_over_time_offers_hll_cardinality_evaluation() { } fn lower_search_and_materialize(query: &str) -> Rc { - let pre = lower_promql(query, AccuracyTarget::Exact).expect("lowering failed"); + let pre = lower_promql(query, RequestedAccuracy::exact()).expect("lowering failed"); let space = search_workload(vec![("query", pre)]).unwrap(); let selection = space.global_selection(&DefaultCostModel); selection @@ -184,7 +186,7 @@ fn dtype<'a>(schema: &'a Schema, name: &str) -> &'a FieldDataType { } fn lower_and_realize(query: &str) -> Rc { - let pre = lower_promql(query, AccuracyTarget::Exact).expect("lowering failed"); + let pre = lower_promql(query, RequestedAccuracy::exact()).expect("lowering failed"); realize(&pre).expect("binding failed") } @@ -266,7 +268,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])))", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -330,7 +332,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])))", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -351,10 +353,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])))", - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + RequestedAccuracy::epsilon_delta(0.01, 0.01), ) .unwrap(); let candidates = ASAPStrategies::default_cost_model().replacements(&TargetSubDAG::new(&root)); @@ -384,7 +383,7 @@ fn weighted_topk_rejects_invalid_population_evidence() { } let root = lower_promql( "topk by(job)(2, sum by(service, job)(rate(m[1m])))", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -404,14 +403,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, - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, - ) - .unwrap(); + let root = lower_promql(query, RequestedAccuracy::epsilon_delta(0.01, 0.01)).unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( &DefaultCostModel, &DefaultAccuracyModel, @@ -504,10 +496,10 @@ fn rate_and_increase_topk_use_summary_scores_and_grouped_limits() { }) ); let guarantee = plan.guarantee.as_ref().unwrap(); - assert!(guarantee.failure_probability.evaluate().unwrap() <= 0.01); + assert!(guarantee.spec.failure_probability.evaluate().unwrap() <= 0.01); assert!(guarantee.provenance.iter().any(|source| matches!(source, asap_types::post_asap::GuaranteeSource::ChildGuarantee { guarantee, .. } - if guarantee.metric == asap_types::post_asap::ErrorMetric::Frequency))); + if guarantee.spec.metric == asap_types::post_asap::ErrorMetric::ABSOLUTE_STREAM_L1))); } } @@ -554,7 +546,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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .expect("lowering failed"); let root = realize(&pre).expect("binding failed"); @@ -574,13 +566,11 @@ 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 target = AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }; + let requested = RequestedAccuracy::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])", - target.clone(), + requested, ) .expect("lowering failed"); @@ -607,7 +597,7 @@ fn ddsketch_quantile_ratio_meets_the_shared_relative_error_target() { panic!("expected a summary candidate") }; let guarantee = node.guarantee.as_ref().expect("ratio guarantee"); - assert_eq!(guarantee.failure_probability.evaluate(), Some(0.0)); + assert_eq!(guarantee.spec.failure_probability.evaluate(), Some(0.0)); assert!( DefaultAccuracyModel.satisfies(guarantee, &target), "ratio guarantee should satisfy the requested target: {guarantee:?}" @@ -647,14 +637,8 @@ 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, - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, - ) - .expect("lower temporal Top-K"); + let pre = lower_promql(source, RequestedAccuracy::epsilon_delta(0.01, 0.01)) + .expect("lower temporal Top-K"); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( &DefaultCostModel, &DefaultAccuracyModel, @@ -816,14 +800,7 @@ fn planner_heap_topk_reference_execution_matches_ground_truth() { vec![("worker", 100.0), ("cron", 30.0)], ), ] { - let pre = lower_promql( - query, - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, - ) - .unwrap(); + let pre = lower_promql(query, RequestedAccuracy::epsilon_delta(0.01, 0.01)).unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( &DefaultCostModel, &DefaultAccuracyModel, @@ -864,7 +841,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]))", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .expect("lowering failed"); let root = realize(&pre_asap).expect("binding failed"); @@ -991,12 +968,15 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { } /// An exact workload binds zero sketches: `sum by (job) (m)` at -/// `AccuracyTarget::Exact` still gets its mergeable exact accumulator, and +/// `AccuracyTarget::exact()` still gets its mergeable exact accumulator, and /// `avg(m)` (non-mergeable) passes through as a whole logical sub-DAG. #[test] fn promql_exact_workload_binds_accumulators_not_sketches() { - let pre_asap = lower_promql("sum by (job) (http_requests_total)", AccuracyTarget::Exact) - .expect("lowering failed"); + let pre_asap = lower_promql( + "sum by (job) (http_requests_total)", + RequestedAccuracy::exact(), + ) + .expect("lowering failed"); let root = realize(&pre_asap).expect("binding failed"); let Some(ASAPOp::SummaryAgg { family, reduction, .. @@ -1019,8 +999,8 @@ fn promql_exact_workload_binds_accumulators_not_sketches() { "group keys pass through verbatim" ); - let pre_asap = - lower_promql("avg(http_requests_total)", AccuracyTarget::Exact).expect("lowering failed"); + let pre_asap = lower_promql("avg(http_requests_total)", RequestedAccuracy::exact()) + .expect("lowering failed"); let root = realize(&pre_asap).expect("binding failed"); assert!( !root.contains_asap(), @@ -1032,7 +1012,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]))", - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .expect("lowering failed"); let original_schema = original.schema.clone(); @@ -1065,9 +1045,7 @@ fn promql_sum_of_count_over_time_is_composed_by_default_search() { assert_eq!(by.keys(), &[2]); assert!(matches!( measures.as_slice(), - [asap_types::pre_asap::AggIntent::Count { - accuracy: AccuracyTarget::Exact - }] + [asap_types::pre_asap::AggIntent::Count { accuracy }] if accuracy.is_exact() )); assert!(matches!( child.non_asap(), @@ -1082,7 +1060,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]))", - AccuracyTarget::Epsilon(0.05), + RequestedAccuracy::epsilon(0.05), ) .unwrap(); let space = search_workload(vec![("query", pre)]).unwrap(); @@ -1145,7 +1123,7 @@ fn physical_node_owns_phase_independently_of_binary_payload() { ExecutionTiming::QueryTime, ), ] { - let input = lower_promql(query, AccuracyTarget::Epsilon(0.05)).unwrap(); + let input = lower_promql(query, RequestedAccuracy::epsilon(0.05)).unwrap(); let search = search_workload(vec![("q", input)]).unwrap(); let choice = search.global_selection(&DefaultCostModel); let plan = choice @@ -1179,14 +1157,14 @@ 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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let root = realize(&pre).unwrap(); assert!(matches!(root.non_asap(), Some(NonASAPOp::BinaryOp { .. }))); assert!(root.guarantee.is_none()); let space = search_workload_with_targets( - vec![("unproven", pre, Some(AccuracyTarget::Epsilon(0.01)))], + vec![("unproven", pre, Some(AccuracyTarget::epsilon(0.01)))], &asap_aware_mapping::default_strategies(), &DefaultAccuracyModel, ) @@ -1253,7 +1231,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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -1294,7 +1272,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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -1316,7 +1294,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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); let strategy = ASAPStrategies::new_with_planning_inputs_and_evidence( @@ -1349,7 +1327,14 @@ fn ddsketch_ratio_bound_holds_for_signed_pinned_sketch_evaluations() { *alpha }; assert_eq!(alpha(lhs), alpha(rhs)); - let bound = node.guarantee.as_ref().unwrap().bound.evaluate().unwrap(); + let bound = node + .guarantee + .as_ref() + .unwrap() + .spec + .error + .evaluate() + .unwrap(); for values in [ vec![sign; 30], (1..=100).map(|i| sign * i as f64).collect(), @@ -1390,7 +1375,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])", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .unwrap(); for count in [0, (1u64 << 53) + 1] { @@ -1410,16 +1395,13 @@ fn ddsketch_ratio_requires_a_supported_population_size() { #[test] fn without_aggregation_candidates_export_valid_dags() { for accuracy in [ - AccuracyTarget::Exact, - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - }, + RequestedAccuracy::exact(), + RequestedAccuracy::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(); let space = search_workload_with_targets( - vec![(0, root, Some(accuracy.clone()))], + vec![(0, root, Some(accuracy.target_in(ErrorMetric::ABSOLUTE)))], &asap_aware_mapping::default_strategies(), &DefaultAccuracyModel, ) diff --git a/crates/integration-tests/tests/scan.rs b/crates/integration-tests/tests/scan.rs index e43e27a6c..e4abbf69e 100644 --- a/crates/integration-tests/tests/scan.rs +++ b/crates/integration-tests/tests/scan.rs @@ -15,10 +15,11 @@ use asap_types::ir::{ ExprSemantics, NonASAPOp, OperatorNode, Predicate, ScalarExpr, TimeRangeKind, }; use asap_types::pre_asap::{CompareOpKind, ScalarValue, Source}; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; fn lower(q: &str) -> Rc { - lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } fn node(op: NonASAPOp) -> Rc { diff --git a/crates/integration-tests/tests/schema.rs b/crates/integration-tests/tests/schema.rs index c616c0d5f..bc2b5cf05 100644 --- a/crates/integration-tests/tests/schema.rs +++ b/crates/integration-tests/tests/schema.rs @@ -10,10 +10,11 @@ //! are label-preserving and keep the schema open. use asap_integration_tests::fixtures::lower_promql; -use asap_types::types::AccuracyTarget; +use asap_types::types::RequestedAccuracy; fn lower(q: &str) -> std::rc::Rc { - lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } // bare scan is open — the metric's full label set is unknown at plan time diff --git a/crates/integration-tests/tests/sql_to_physical.rs b/crates/integration-tests/tests/sql_to_physical.rs index 035765d28..644c47aea 100644 --- a/crates/integration-tests/tests/sql_to_physical.rs +++ b/crates/integration-tests/tests/sql_to_physical.rs @@ -10,10 +10,10 @@ use asap_physical_operators::{ values::{Batch, Value}, }; use asap_types::ir::physical_export::PhysicalASAPOperatorPayload; +use asap_types::types::RequestedAccuracy; use asap_types::{ post_asap::FieldDataType, pre_asap::{DataType, Field, Schema}, - types::AccuracyTarget, }; use futures::StreamExt; use physical_common::compile_physical_asap_dag; @@ -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, AccuracyTarget::Exact) + let logical = lower_sql(query, &catalog, RequestedAccuracy::exact()) .await .unwrap(); - let selected = selected_dag(logical, AccuracyTarget::Exact); + let selected = selected_dag(logical, RequestedAccuracy::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 3e4fcbe5d..d29df4d91 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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: AccuracyTarget) -> Rc { +async fn lower(sql: &str, accuracy: RequestedAccuracy) -> 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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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, - AccuracyTarget::Exact, + RequestedAccuracy::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", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::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", - AccuracyTarget::Exact, + RequestedAccuracy::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", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .await; let agg = inner_aggregate(&pre_asap); @@ -740,7 +740,7 @@ async fn sql_quantile_binds_kll_sketch_over_named_column() { async fn sql_count_distinct_with_epsilon_binds_hll_rse_over_named_column() { let pre_asap = lower( "SELECT COUNT(DISTINCT service) FROM metrics", - AccuracyTarget::Epsilon(0.01), + RequestedAccuracy::epsilon(0.01), ) .await; let agg = inner_aggregate(&pre_asap); @@ -787,7 +787,7 @@ async fn sql_count_distinct_with_epsilon_binds_hll_rse_over_named_column() { } /// An exact workload binds zero sketches: `SUM(bytes) GROUP BY service` at -/// `AccuracyTarget::Exact` still gets its mergeable exact accumulator, and +/// `AccuracyTarget::exact()` still gets its mergeable exact accumulator, and /// `AVG(bytes)` (non-mergeable) stays a whole logical sub-DAG untouched. SQL /// counterpart of `promql_to_post_asap.rs`'s /// `promql_exact_workload_binds_accumulators_not_sketches`. @@ -795,7 +795,7 @@ async fn sql_count_distinct_with_epsilon_binds_hll_rse_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", - AccuracyTarget::Exact, + RequestedAccuracy::exact(), ) .await; let agg = inner_aggregate(&pre_asap); @@ -821,7 +821,7 @@ async fn sql_exact_workload_binds_accumulators_not_sketches() { "group keys pass through verbatim" ); - let pre_asap = lower("SELECT AVG(bytes) FROM metrics", AccuracyTarget::Exact).await; + let pre_asap = lower("SELECT AVG(bytes) FROM metrics", RequestedAccuracy::exact()).await; let agg = inner_aggregate(&pre_asap); let root = realize(agg).expect("binding failed"); assert!( @@ -838,7 +838,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, AccuracyTarget::Exact, + &catalog(), SqlDialect::ClickhouseSQL, RequestedAccuracy::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 b56c0851f..1f0303cb1 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, PlanningWorkload, Query, QueryLanguage, QueryRequirements, QueryWorkload, @@ -30,11 +30,8 @@ use futures::StreamExt; mod physical_common; -fn accuracy() -> AccuracyTarget { - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - } +fn accuracy() -> RequestedAccuracy { + RequestedAccuracy::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 0c7569284..0e93ab058 100644 --- a/crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs +++ b/crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs @@ -11,13 +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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; -fn approximate() -> AccuracyTarget { - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - } +fn approximate() -> RequestedAccuracy { + RequestedAccuracy::epsilon_delta(0.01, 0.001) } fn names(node: &OperatorNode) -> Vec { @@ -56,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: AccuracyTarget, + accuracy: RequestedAccuracy, ranked: &str, root_names: &[&str], ) -> usize { @@ -134,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 [AccuracyTarget::Exact, approximate()] { + for accuracy in [RequestedAccuracy::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 23afaedaf..c81c9a70b 100644 --- a/crates/integration-tests/tests/summary_coverage_e2e.rs +++ b/crates/integration-tests/tests/summary_coverage_e2e.rs @@ -17,13 +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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; -fn accuracy() -> AccuracyTarget { - AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.01, - } +fn accuracy() -> RequestedAccuracy { + RequestedAccuracy::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 95212ca99..478ddf5d8 100644 --- a/crates/integration-tests/tests/time_range.rs +++ b/crates/integration-tests/tests/time_range.rs @@ -15,9 +15,11 @@ 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, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) + lower_promql(q, RequestedAccuracy::exact()) + .unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) } fn node(op: NonASAPOp) -> Rc { @@ -76,7 +78,7 @@ fn q15_count_over_time() { range_agg( 300, AggIntent::Count { - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() }, "http_requests_total" ), @@ -161,7 +163,7 @@ fn q17_quantile_over_time() { AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() }, "http_requests_total", ), diff --git a/crates/integration-tests/tests/tumbling_pane_merge.rs b/crates/integration-tests/tests/tumbling_pane_merge.rs index f64285050..9dac6ae81 100644 --- a/crates/integration-tests/tests/tumbling_pane_merge.rs +++ b/crates/integration-tests/tests/tumbling_pane_merge.rs @@ -22,14 +22,14 @@ use asap_types::ir::{ NonASAPOp, Operator, OperatorNode, TimingMemo, }; use asap_types::post_asap::Schema; -use asap_types::types::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; 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: AccuracyTarget) -> Rc { +fn single_build(query: &str, accuracy: RequestedAccuracy) -> Rc { let root = lower_promql(query, accuracy.clone()).unwrap(); let root = promql_rows::with_series_identity(&root).unwrap(); selected_dag(root, accuracy) @@ -306,7 +306,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: AccuracyTarget) -> Vec { +fn assert_pane_merge_matches_single_build(query: &str, accuracy: RequestedAccuracy) -> Vec { let single = single_build(query, accuracy); let (panes, pane_builds) = tumbling_panes(&single); let expected = execute(&single); @@ -332,7 +332,7 @@ fn assert_pane_merge_matches_single_build(query: &str, accuracy: AccuracyTarget) #[test] fn exact_sum_over_tumbling_panes_matches_single_build() { let sums = - assert_pane_merge_matches_single_build("sum_over_time(m[5m])", AccuracyTarget::Exact); + assert_pane_merge_matches_single_build("sum_over_time(m[5m])", RequestedAccuracy::exact()); assert_eq!(sums, vec![210., 2210.]); } @@ -342,7 +342,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])", - AccuracyTarget::Epsilon(0.05), + RequestedAccuracy::epsilon(0.05).with_metric(ErrorMetric::Relative), ); let mut p99 = p99; p99.sort_by(f64::total_cmp); @@ -360,6 +360,6 @@ fn quantile_over_tumbling_panes_matches_single_build() { #[test] fn exact_increase_over_tumbling_panes_matches_single_build() { let increases = - assert_pane_merge_matches_single_build("increase(m[5m])", AccuracyTarget::Exact); + assert_pane_merge_matches_single_build("increase(m[5m])", RequestedAccuracy::exact()); assert_eq!(increases.len(), 2); } diff --git a/crates/planner/src/lib.rs b/crates/planner/src/lib.rs index 9b95c5d31..428dac90f 100644 --- a/crates/planner/src/lib.rs +++ b/crates/planner/src/lib.rs @@ -206,7 +206,7 @@ async fn lower(input: &UserInput<'_>) -> Result, &entry.query.0, catalog, dialect.clone(), - entry.requirements.accuracy.target(), + entry.requirements.accuracy.requested(), ) .await .map_err(|source| PlanError::Lowering { diff --git a/crates/planner/tests/e2e_plan.rs b/crates/planner/tests/e2e_plan.rs index f580415d8..dd39f9922 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataArrival, DataWorkload, DurationMs, Evidence, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, @@ -21,15 +21,27 @@ const NOW_MS: u64 = 1_700_000_000_000; fn approximate() -> QueryRequirements { QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::Epsilon(0.01)), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon(0.01)), response_latency: LatencyRequirement::Unspecified, } } +/// [`approximate`], stated as a rank error for a quantile query, whose +/// target names its metric. +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), + ); + } + requirements +} + fn batch(sql: &str) -> BatchEntry { BatchEntry { query: Query(sql.into()), - requirements: approximate(), + requirements: approximate_for(sql), predictability: Predictability::AdHoc, invocations: 1, execute_at: None, @@ -115,7 +127,7 @@ async fn facade_plans_match_run_stage1_to_stage3() { let mut roots = Vec::new(); let mut targets = Vec::new(); for (index, entry) in workload.query_workload.entries().enumerate() { - let accuracy = entry.requirements.accuracy.target(); + let accuracy = entry.requirements.accuracy.requested(); let expr = lower_sql_dialect( &entry.query.0, &catalog, @@ -160,7 +172,7 @@ async fn lowers_repeating_sql_entries_too() { "SELECT approx_percentile_cont(l_extendedprice, 0.99) FROM lineitem".into(), ), demand: RepeatedDemand::FixedInterval(RepetitionInterval(60_000)), - requirements: approximate(), + requirements: approximate_for("approx_percentile_cont"), predictability: Predictability::Unknown, time_selection: TimeSelection::default(), }]), diff --git a/crates/planner/tests/plan_stages.rs b/crates/planner/tests/plan_stages.rs index 6cf65abc7..8f409b8c3 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::AccuracyTarget; +use asap_types::types::RequestedAccuracy; 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 = AccuracyTarget::Epsilon(epsilon); + let target = RequestedAccuracy::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 75c0b670e..696529eed 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::AccuracyTarget; +use asap_types::types::{ErrorMetric, RequestedAccuracy}; use asap_types::workload::{ AccuracyRequirement, DataArrival, DataWorkload, DurationMs, Evidence, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage, QueryRequirements, QueryWorkload, Rate, @@ -26,9 +26,20 @@ use asap_types::workload::{ const NOW_MS: u64 = 1_700_000_000_000; -fn requirements(epsilon: f64) -> QueryRequirements { +/// `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); + if query.contains("quantile") { + requested.with_metric(ErrorMetric::Rank) + } else { + requested + } +} + +fn requirements(query: &str, epsilon: f64) -> QueryRequirements { QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::Epsilon(epsilon)), + accuracy: AccuracyRequirement::Explicit(requested(query, epsilon)), response_latency: LatencyRequirement::Unspecified, } } @@ -38,7 +49,7 @@ fn repeating(query: &str, epsilon: f64) -> RepeatingEntry { RepeatingEntry { query: Query(query.into()), demand: RepeatedDemand::FixedInterval(RepetitionInterval(600_000)), - requirements: requirements(epsilon), + requirements: requirements(query, epsilon), predictability: Predictability::Unknown, time_selection: TimeSelection::default(), } @@ -90,7 +101,7 @@ fn promql_stages(queries: &[(&str, f64)]) -> Stage1ToStage3Run { let roots = lower_promql_query_workload(&workload, NOW_MS).expect("lowers"); let targets: Vec<_> = queries .iter() - .map(|(_, epsilon)| Some(AccuracyTarget::Epsilon(*epsilon))) + .map(|(query, epsilon)| Some(requested(query, *epsilon))) .collect(); run_stage1_to_stage3( roots.into_iter().enumerate().collect(), @@ -299,7 +310,7 @@ async fn quantiles_share_one_producer_sized_for_the_strictest_consumer() { assert_eq!(kll_k(plan), kll_k_for(0.001)); let guarantee = plan.root.guarantee.as_ref().expect("certified"); assert!( - guarantee.bound.evaluate().unwrap() <= epsilon, + guarantee.spec.error.evaluate().unwrap() <= epsilon, "{guarantee:?}" ); } diff --git a/crates/types/src/ir/asap.rs b/crates/types/src/ir/asap.rs index 75f8d1018..e28d29d8f 100644 --- a/crates/types/src/ir/asap.rs +++ b/crates/types/src/ir/asap.rs @@ -310,7 +310,7 @@ impl ASAPOp { ExactKind::Min => Some(AggIntent::Min { col: column }), ExactKind::Max => Some(AggIntent::Max { col: column }), ExactKind::Count => Some(AggIntent::Count { - accuracy: crate::types::AccuracyTarget::Exact, + accuracy: crate::types::AccuracyTarget::exact(), }), ExactKind::Rate => Some(AggIntent::Rate), ExactKind::IRate => Some(AggIntent::IRate), @@ -401,7 +401,7 @@ impl ASAPOp { ) } }; - let accuracy = crate::types::AccuracyTarget::Exact; + let accuracy = crate::types::AccuracyTarget::exact(); let measure = match evaluation { PopulationStatistic::Quantile { q } => AggIntent::Quantile { q: *q, diff --git a/crates/types/src/ir/canonicalize.rs b/crates/types/src/ir/canonicalize.rs index f19c79dc8..a5d64511d 100644 --- a/crates/types/src/ir/canonicalize.rs +++ b/crates/types/src/ir/canonicalize.rs @@ -250,7 +250,7 @@ fn try_promote_additive_top_ranking( // Count ranks unit updates; a direct Sum ranks weighted updates. let accuracy = match ranked_agg { AggIntent::Count { accuracy } => accuracy.clone(), - AggIntent::Sum { .. } => AccuracyTarget::Exact, + AggIntent::Sum { .. } => AccuracyTarget::exact(), _ => unreachable!("additive ranking gate admitted a non-additive measure"), }; @@ -418,7 +418,7 @@ mod tests { fn count() -> AggIntent { AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), } } diff --git a/crates/types/src/ir/cse.rs b/crates/types/src/ir/cse.rs index e5568ee0a..50af4f4c7 100644 --- a/crates/types/src/ir/cse.rs +++ b/crates/types/src/ir/cse.rs @@ -323,7 +323,7 @@ mod tests { measures: vec![AggIntent::Quantile { col, q, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }], output_names: vec![], filters: vec![], @@ -663,7 +663,7 @@ mod tests { node(NonASAPOp::Aggregate { reduction: Reduction::Reduce(GroupKeys::without(vec![0])), measures: vec![AggIntent::Count { - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }], output_names: vec![], filters: vec![], diff --git a/crates/types/src/post_asap/guarantee.rs b/crates/types/src/post_asap/guarantee.rs index 4246896b2..42c45e62b 100644 --- a/crates/types/src/post_asap/guarantee.rs +++ b/crates/types/src/post_asap/guarantee.rs @@ -21,14 +21,16 @@ //! guarantee). Its statement is: //! //! ```text -//! Pr[ err_metric(estimate, truth) > bound ] <= failure_probability +//! Pr[ err_metric(estimate, truth) > error ] <= failure_probability //! ``` //! -//! where `err_metric` is fixed by [`ErrorMetric`] and each metric has its -//! own normalization (documented per variant). Metrics are **not** -//! interchangeable: a cardinality error and a frequency error are different -//! quantities, and composing them needs an explicit rule, never an implicit -//! "add the epsilons". +//! for each output value separately ([`AccuracyScope::PerValue`]), where +//! `err_metric` is fixed by [`ErrorMetric`]: an absolute error (in value +//! units or in multiples of a stream norm), a relative error, or a rank +//! error. Metrics are **not** interchangeable: converting between them +//! needs an explicit rule with evidence, never an implicit "compare the +//! epsilons". [`AccuracyGuarantee`] shares its outer shape, [`AccuracySpec`], +//! with the requirement it is checked against, [`AccuracyTarget`]. //! //! ## Why expressions, not numbers //! @@ -51,38 +53,7 @@ use serde::{Deserialize, Serialize}; use crate::types::AccuracyTarget; - -/// Which error quantity a [`AccuracyGuarantee`] bounds. `#[non_exhaustive]`: -/// a deployment's own `AccuracyModel` may need a metric this crate does not -/// enumerate yet, and downstream matches must not assume the list is closed. -#[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ErrorMetric { - /// `|estimate − truth| ≤ bound`, in the value's own units. - AbsoluteValue, - /// `|estimate − truth| ≤ bound · |truth|` — a multiplicative guarantee, - /// only meaningful for values of known sign (DDSketch's α). - RelativeValue, - /// The returned value's *rank* in the input multiset is within - /// `bound · n` of the requested rank (KLL's ε). Says nothing about how - /// far the returned *value* is from the true quantile value. - Rank, - /// `|estimate − truth| ≤ bound · truth` for a distinct count (HLL/Theta/ - /// KMV's relative standard error). - Cardinality, - /// `|estimate − truth| ≤ bound · ‖f‖₁` for a point-frequency query - /// (CMS's ε, normalized by the stream's L1 norm). - Frequency, - /// `|estimate − truth| ≤ bound · ‖f‖₂` for a point-frequency query. - /// CountSketch uses this normalization; it is intentionally distinct - /// from CMS's [`ErrorMetric::Frequency`] (`L1`) guarantee. - L2Frequency, - /// The returned key set equals the true top-k set. The built-in model - /// produces this only from a supplied per-key interval margin certificate; - /// a frequency bound alone is insufficient. - TopKMembership, -} +pub use crate::types::{AccuracyScope, AccuracySpec, ErrorMetric, NormScale}; /// A symbolic, serializable error-bound expression. Non-negative real /// arithmetic only — there is no subtraction, so a bound can never be @@ -414,63 +385,83 @@ pub enum GuaranteeSource { } /// The machine-readable accuracy statement attached to a finalized -/// post-ASAP value — see the module docs for its semantics. +/// post-ASAP value — see the module docs for its semantics. Its +/// [`AccuracySpec`] is symbolic, for plan selection to bind; a guarantee a +/// sketch or a composition produces covers each output value separately +/// ([`AccuracyScope::PerValue`]). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct AccuracyGuarantee { - pub metric: ErrorMetric, - pub bound: BoundExpr, - pub failure_probability: ProbabilityExpr, + pub spec: AccuracySpec, + /// The explanation trail: why the bound is what it is. No planning + /// decision reads it. pub provenance: Vec, } impl AccuracyGuarantee { + /// A per-value guarantee: error at most `error` in `metric`, failing + /// with probability at most `failure_probability`. + pub fn new( + metric: ErrorMetric, + error: BoundExpr, + failure_probability: ProbabilityExpr, + provenance: Vec, + ) -> Self { + Self { + spec: AccuracySpec { + metric, + error, + failure_probability, + scope: AccuracyScope::PerValue, + }, + provenance, + } + } + /// This guarantee with every [`BoundExpr::Unknown`] (and /// [`ProbabilityExpr::Unknown`]) leaf whose statistic `value` supplies /// replaced by that value. Plan selection binds statistics this way; /// a statistic `value` does not supply stays unknown. pub fn bind(&self, value: &dyn Fn(&str) -> Option) -> Self { - Self { - bound: self.bound.bind(value), - failure_probability: self.failure_probability.bind(value), - ..self.clone() - } + let mut bound = self.clone(); + bound.spec.error = self.spec.error.bind(value); + bound.spec.failure_probability = self.spec.failure_probability.bind(value); + bound } /// Optimistic lower bound used only to reject an impossible target. /// Replacing unknown non-negative contributions by zero cannot certify /// this candidate; the original guarantee remains unresolved. pub fn optimistic_floor(&self) -> Self { - Self { - metric: self.metric, - bound: self.bound.optimistic_floor(), - failure_probability: self.failure_probability.optimistic_floor(), - provenance: self.provenance.clone(), - } + let mut floor = self.clone(); + floor.spec.error = self.spec.error.optimistic_floor(); + floor.spec.failure_probability = self.spec.failure_probability.optimistic_floor(); + floor } /// An unresolved bound or failure probability is not a certificate. pub fn has_unknown(&self) -> bool { - self.bound.has_unknown() || self.failure_probability.has_unknown() + self.spec.error.has_unknown() || self.spec.failure_probability.has_unknown() } /// The zero-error, zero-failure guarantee of a deterministic exact - /// computation. `metric` is [`ErrorMetric::AbsoluteValue`]: an exact - /// value is exact under every metric, and absolute error is the one - /// every same-metric rule accepts as a zero input. + /// computation. `metric` is [`ErrorMetric::ABSOLUTE`]: an exact value + /// is exact under every metric, and absolute error is the one every + /// same-metric rule accepts as a zero input. pub fn exact(reason: impl Into) -> Self { - Self { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Zero, - failure_probability: ProbabilityExpr::Zero, - provenance: vec![GuaranteeSource::Exact { + Self::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Zero, + ProbabilityExpr::Zero, + vec![GuaranteeSource::Exact { reason: reason.into(), }], - } + ) } /// `true` iff this guarantee promises zero error with certainty. pub fn is_exact(&self) -> bool { - self.bound.is_zero() && self.failure_probability.is_zero() + self.spec.error.is_zero() && self.spec.failure_probability.is_zero() } /// How many approximate sketch evaluations contributed to this value — @@ -566,9 +557,9 @@ mod tests { #[test] fn optimistic_floor_preserves_known_contributions_without_certifying() { - let guarantee = AccuracyGuarantee { - metric: ErrorMetric::Frequency, - bound: BoundExpr::Sum { + let guarantee = AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE_STREAM_L1, + BoundExpr::Sum { terms: vec![ BoundExpr::Constant { value: 0.1 }, BoundExpr::Unknown { @@ -576,7 +567,7 @@ mod tests { }, ], }, - failure_probability: ProbabilityExpr::UnionBound { + ProbabilityExpr::UnionBound { terms: vec![ ProbabilityExpr::Constant { value: 0.02 }, ProbabilityExpr::Unknown { @@ -584,13 +575,13 @@ mod tests { }, ], }, - provenance: vec![], - }; + vec![], + ); assert!(guarantee.has_unknown()); - assert_eq!(guarantee.bound.evaluate(), None); + assert_eq!(guarantee.spec.error.evaluate(), None); let floor = guarantee.optimistic_floor(); - assert_eq!(floor.bound.evaluate(), Some(0.1)); - assert_eq!(floor.failure_probability.evaluate(), Some(0.02)); + assert_eq!(floor.spec.error.evaluate(), Some(0.1)); + assert_eq!(floor.spec.failure_probability.evaluate(), Some(0.02)); assert!(!floor.has_unknown()); assert!(guarantee.has_unknown()); } @@ -630,9 +621,9 @@ mod tests { #[test] fn guarantee_round_trips_through_json() { - let g = AccuracyGuarantee { - metric: ErrorMetric::Frequency, - bound: BoundExpr::Sum { + let g = AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE_STREAM_L1, + BoundExpr::Sum { terms: vec![ BoundExpr::Constant { value: 0.01 }, BoundExpr::Scaled { @@ -641,17 +632,18 @@ mod tests { }, ], }, - failure_probability: ProbabilityExpr::UnionBound { + ProbabilityExpr::UnionBound { terms: vec![ProbabilityExpr::Constant { value: 0.01 }], }, - provenance: vec![GuaranteeSource::CompositionStep { + vec![GuaranteeSource::CompositionStep { operator: CompositionOperator::Lipschitz { constant: 2.0 }, rule: "lipschitz".into(), }], - }; + ); let json = serde_json::to_value(&g).unwrap(); - assert_eq!(json["metric"], "frequency"); - assert_eq!(json["bound"]["op"], "sum"); + assert_eq!(json["spec"]["metric"], "absolute_stream_l1"); + assert_eq!(json["spec"]["scope"], "per_value"); + assert_eq!(json["spec"]["error"]["op"], "sum"); let back: AccuracyGuarantee = serde_json::from_value(json).unwrap(); assert_eq!(back, g); } @@ -663,20 +655,20 @@ mod tests { let rows = || BoundExpr::Unknown { statistic: "input_row_count".into(), }; - let g = AccuracyGuarantee { - metric: ErrorMetric::AbsoluteValue, - bound: BoundExpr::Product { + let g = AccuracyGuarantee::new( + ErrorMetric::ABSOLUTE, + BoundExpr::Product { factors: vec![BoundExpr::Constant { value: 0.01 }, rows()], }, - failure_probability: ProbabilityExpr::Scaled { + ProbabilityExpr::Scaled { count: rows(), inner: Box::new(ProbabilityExpr::Constant { value: 1e-4 }), }, - provenance: vec![], - }; + vec![], + ); let bound = g.bind(&|statistic| (statistic == "input_row_count").then_some(10.0)); - assert_eq!(bound.bound.evaluate(), Some(0.1)); - assert!((bound.failure_probability.evaluate().unwrap() - 1e-3).abs() < 1e-12); + assert_eq!(bound.spec.error.evaluate(), Some(0.1)); + assert!((bound.spec.failure_probability.evaluate().unwrap() - 1e-3).abs() < 1e-12); let unbound = g.bind(&|_| None); assert_eq!(unbound, g); } diff --git a/crates/types/src/pre_asap/agg_intent.rs b/crates/types/src/pre_asap/agg_intent.rs index 1e926bd44..b9f1252e7 100644 --- a/crates/types/src/pre_asap/agg_intent.rs +++ b/crates/types/src/pre_asap/agg_intent.rs @@ -710,11 +710,7 @@ pub fn agg_accuracy(op: &AggIntent) -> f64 { } fn accuracy_target_to_f64(t: &AccuracyTarget) -> f64 { - match t { - AccuracyTarget::Exact => 0.0, - AccuracyTarget::Epsilon(eps) => *eps, - AccuracyTarget::EpsilonDelta { epsilon, .. } => *epsilon, - } + t.error } /// Default `Cardinality` intent over the sample value — HLL standard error at @@ -722,7 +718,7 @@ fn accuracy_target_to_f64(t: &AccuracyTarget) -> f64 { pub fn default_cardinality() -> AggIntent { AggIntent::Cardinality { cols: vec![], - accuracy: AccuracyTarget::Epsilon(1.04 / ((1u64 << 14) as f64).sqrt()), + accuracy: AccuracyTarget::epsilon(1.04 / ((1u64 << 14) as f64).sqrt()), } } @@ -731,7 +727,7 @@ pub fn default_quantile(q: f64) -> AggIntent { AggIntent::Quantile { col: None, q, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), } } @@ -765,7 +761,7 @@ mod tests { fn distinct_tuple_cardinality_contract() { let intent = AggIntent::Cardinality { cols: vec![2, 5], - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }; assert_eq!(intent.input_cols(), vec![2, 5]); assert!(!agg_is_exact(&intent)); @@ -782,7 +778,7 @@ mod tests { let v = c("value", DataType::Float64); assert_eq!( AggIntent::::Count { - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() } .output_column(&v) .name, @@ -798,7 +794,7 @@ mod tests { AggIntent::::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Epsilon(0.01) + accuracy: AccuracyTarget::epsilon(0.01) } .output_column(&v) .name, @@ -844,7 +840,7 @@ mod tests { use topk::Ranking::*; assert_eq!( topk::Ranking::from_aggregate(&AggIntent::Count { - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() }), Frequency ); @@ -876,7 +872,7 @@ mod tests { "empty = PromQL sample value" ); assert!(AggIntent::::Count { - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() } .input_cols() .is_empty()); @@ -888,24 +884,24 @@ mod tests { AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, AggIntent::Quantile { col: Some(3), q: 0.99, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, AggIntent::Cardinality { cols: vec![2], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, AggIntent::Cardinality { cols: vec![2, 3], - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), }, AggIntent::TopK { k: 10, - accuracy: AccuracyTarget::Epsilon(0.01), + accuracy: AccuracyTarget::epsilon(0.01), }, ] { let json = serde_json::to_string(&v).unwrap(); @@ -918,9 +914,9 @@ mod tests { #[test] fn cardinality_rejects_legacy_col_payloads() { for payload in [ - r#"{"kind":"cardinality","col":2,"accuracy":"Exact"}"#, - r#"{"kind":"cardinality","col":null,"accuracy":"Exact"}"#, - r#"{"kind":"cardinality","col":2,"cols":[1],"accuracy":"Exact"}"#, + r#"{"kind":"cardinality","col":2,"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#, + r#"{"kind":"cardinality","col":null,"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#, + r#"{"kind":"cardinality","col":2,"cols":[1],"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#, ] { assert!( serde_json::from_str::(payload).is_err(), @@ -933,31 +929,31 @@ 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":"Exact"}"#; + let legacy = r#"{"kind":"quantile","q":0.99,"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; assert_eq!( serde_json::from_str::(legacy).unwrap(), AggIntent::Quantile { col: None, q: 0.99, - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() } ); - let legacy = r#"{"kind":"top_k","k":5,"accuracy":"Exact"}"#; + let legacy = r#"{"kind":"top_k","k":5,"accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; assert_eq!( serde_json::from_str::(legacy).unwrap(), AggIntent::TopK { k: 5, - accuracy: AccuracyTarget::Exact, + accuracy: AccuracyTarget::exact(), } ); - let legacy = r#"{"kind":"cardinality","accuracy":"Exact"}"#; + let legacy = r#"{"kind":"cardinality","accuracy":{"metric":"absolute","error":0.0,"failure_probability":0.0,"scope":"whole_result"}}"#; assert_eq!( serde_json::from_str::(legacy).unwrap(), AggIntent::Cardinality { cols: vec![], - accuracy: AccuracyTarget::Exact + accuracy: AccuracyTarget::exact() } ); } diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index 7cd90bed1..4de62c478 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -1,13 +1,616 @@ +//! Accuracy requirements and the error metrics they are stated in (#690). +//! +//! 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. + use serde::{Deserialize, Serialize}; -/// Accuracy requirement that a query result must satisfy. +use crate::ir::{NonASAPOp, Operator, OperatorNode, QueryRoot, ScalarExpr}; +use crate::pre_asap::AggIntent; + +/// The stream norm an [`ErrorMetric::Absolute`] error is stated in +/// multiples of. A scaled error `ε` means `|estimate − truth| ≤ ε · ‖f‖`, +/// where `f` is the frequency vector of the one stream the estimate's +/// summaries read: `‖f‖₁` for Count-Min, `‖f‖₂` for Count Sketch. Two scaled +/// errors compare directly only over the same stream; restating one in value +/// units multiplies it by that stream's norm, a statistic plan selection +/// binds from the data workload (`stream_l1_norm[source]`, +/// `stream_l2_norm[source]`). Since `‖f‖₂ ≤ ‖f‖₁`, an L2-scaled error meets +/// an L1-scaled requirement of the same stream with the same ε. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NormScale { + StreamL1, + StreamL2, +} + +/// Which error quantity an accuracy statement bounds. `#[non_exhaustive]`: +/// a metric is added only when a primitive needs a notion these cannot +/// express (e.g. a false-positive rate), and downstream matches must not +/// assume the list is closed. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(into = "MetricName", from = "MetricName")] +pub enum ErrorMetric { + /// `|estimate − truth| ≤ ε`: in the value's own units when `scale` is + /// `None`, or in multiples of a stream norm ([`NormScale`]). + Absolute { scale: Option }, + /// `|estimate − truth| ≤ ε · |truth|`, e.g. DDSketch's α or a distinct + /// count's relative error. + Relative, + /// The returned value's rank in the input multiset is within `ε · n` of + /// the requested rank (a quantile's KLL ε). Says nothing about how far + /// the returned value is from the true quantile value. + Rank, +} + +impl ErrorMetric { + /// Absolute error in the value's own units. + pub const ABSOLUTE: Self = Self::Absolute { scale: None }; + /// Absolute error in multiples of the stream's L1 norm (Count-Min). + pub const ABSOLUTE_STREAM_L1: Self = Self::Absolute { + scale: Some(NormScale::StreamL1), + }; + /// Absolute error in multiples of the stream's L2 norm (Count Sketch). + pub const ABSOLUTE_STREAM_L2: Self = Self::Absolute { + scale: Some(NormScale::StreamL2), + }; +} + +/// The wire name of an [`ErrorMetric`]. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum MetricName { + Absolute, + AbsoluteStreamL1, + AbsoluteStreamL2, + Relative, + Rank, +} + +impl From for MetricName { + fn from(metric: ErrorMetric) -> Self { + match metric { + ErrorMetric::Absolute { scale: None } => Self::Absolute, + ErrorMetric::Absolute { + scale: Some(NormScale::StreamL1), + } => Self::AbsoluteStreamL1, + ErrorMetric::Absolute { + scale: Some(NormScale::StreamL2), + } => Self::AbsoluteStreamL2, + ErrorMetric::Relative => Self::Relative, + ErrorMetric::Rank => Self::Rank, + } + } +} + +impl From for ErrorMetric { + fn from(name: MetricName) -> Self { + match name { + MetricName::Absolute => Self::ABSOLUTE, + MetricName::AbsoluteStreamL1 => Self::ABSOLUTE_STREAM_L1, + MetricName::AbsoluteStreamL2 => Self::ABSOLUTE_STREAM_L2, + MetricName::Relative => Self::Relative, + MetricName::Rank => Self::Rank, + } + } +} + +/// Which output values one (ε, δ) statement covers *simultaneously*. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AccuracyScope { + /// Each output value (one series or row, at one evaluation) is within ε + /// of its true value with probability ≥ 1 − δ, separately. Values may + /// fail in different runs; nothing is said about all of them at once. + /// Sketches and composition produce guarantees of this scope. + PerValue, + /// Every value in one evaluation's returned result is within ε + /// simultaneously, with probability ≥ 1 − δ. The default of a target: a + /// target at a query root applies to the entire returned result. + WholeResult, +} + +/// The outer shape an accuracy target and an accuracy guarantee share: +/// `Pr[err_metric(estimate, truth) > error] ≤ failure_probability` over the +/// values `scope` names. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub enum AccuracyTarget { - /// Additive error bound ε: |estimate − true| ≤ ε · (domain size). +pub struct AccuracySpec { + pub metric: ErrorMetric, + /// ε, in `metric`'s units. + pub error: E, + /// δ. + pub failure_probability: P, + 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). +/// +/// 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. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(from = "RequestedAccuracyWire")] +pub struct RequestedAccuracy { + #[serde(skip_serializing_if = "Option::is_none")] + pub metric: Option, + pub error: f64, + #[serde(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), - /// Probabilistic (ε, δ) guarantee: error ≤ ε with probability ≥ 1 − δ. EpsilonDelta { epsilon: f64, delta: f64 }, - /// No approximation permitted; result must be exact. 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, +} + +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 { + /// No approximation permitted. + pub const fn exact() -> Self { + Self { + metric: None, + error: 0.0, + failure_probability: Some(0.0), + scope: AccuracyScope::WholeResult, + } + } + + /// Error at most `epsilon` in the query's default metric, with no δ + /// requirement. + pub const fn epsilon(epsilon: f64) -> Self { + Self { + metric: None, + error: epsilon, + failure_probability: None, + scope: AccuracyScope::WholeResult, + } + } + + /// Error at most `epsilon` in the query's default metric with + /// probability at least `1 − delta`. + pub const fn epsilon_delta(epsilon: f64, delta: f64) -> Self { + Self { + failure_probability: Some(delta), + ..Self::epsilon(epsilon) + } + } + + /// This request stated in `metric`. + pub fn with_metric(self, metric: ErrorMetric) -> Self { + Self { + metric: Some(metric), + ..self + } + } + + /// Whether this request 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 + /// every metric. + pub fn target_in(&self, metric: ErrorMetric) -> AccuracyTarget { + if self.is_exact() { + return AccuracyTarget::exact().with_scope(self.scope); + } + AccuracyTarget { + metric, + error: self.error, + failure_probability: self.failure_probability, + scope: self.scope, + } + } + + /// 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 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 { + 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 default metric of an intent's value, `None` for a quantile and for +/// intents without a listed default. +fn intent_metric(intent: &AggIntent) -> Option { + match intent { + AggIntent::Count { .. } | AggIntent::TopK { .. } => Some(ErrorMetric::ABSOLUTE_STREAM_L1), + AggIntent::Cardinality { .. } | AggIntent::FrequencyL2 { .. } => { + Some(ErrorMetric::Relative) + } + _ => None, + } +} + +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 { + let Operator::NonASAP(op) = &node.operator else { + return Ok(ErrorMetric::ABSOLUTE); + }; + match op { + NonASAPOp::Filter { child, .. } + | NonASAPOp::Sort { child, .. } + | NonASAPOp::Limit { child, .. } + | NonASAPOp::Dedup { child, .. } => node_metric(child), + NonASAPOp::Project { cols, child, .. } + if cols + .iter() + .all(|item| matches!(item.expr, ScalarExpr::Column(_))) => + { + node_metric(child) + } + NonASAPOp::Aggregate { measures, .. } => { + 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", + }) + } + 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((first, rest)) if rest.iter().all(|metric| metric == first) => *first, + _ => ErrorMetric::ABSOLUTE, + }) + } + _ => Ok(ErrorMetric::ABSOLUTE), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pre_asap::schema::{DataType, Field, Schema}; + use crate::pre_asap::{Reduction, Source}; + use std::rc::Rc; + + fn node(op: NonASAPOp) -> Rc { + Rc::new(OperatorNode::new(Operator::NonASAP(op)).expect("fixture derives a schema")) + } + + fn aggregate(measures: Vec) -> QueryRoot { + let scan = node(NonASAPOp::Scan { + source: Source::TimeSeries { metric: "m".into() }, + predicates: vec![], + schema: Schema::with_time_index( + vec![ + Field::plain("ts", DataType::Timestamp, false), + Field::plain("value", DataType::Float64, false), + ], + 0, + vec![], + ), + }); + QueryRoot::Operator(node(NonASAPOp::Aggregate { + reduction: Reduction::by(vec![]), + measures, + output_names: vec![], + filters: vec![], + having: None, + child: scan, + })) + } + + fn quantile() -> AggIntent { + AggIntent::Quantile { + col: None, + q: 0.99, + accuracy: AccuracyTarget::epsilon(0.01), + } + } + + /// The old workload forms still read, and name no metric; the spec + /// form may name one. + #[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()); + assert_eq!( + read(serde_json::json!({"Epsilon": 0.01})), + RequestedAccuracy::epsilon(0.01) + ); + assert_eq!( + read(serde_json::json!({"EpsilonDelta": {"epsilon": 0.01, "delta": 0.001}})), + RequestedAccuracy::epsilon_delta(0.01, 0.001) + ); + let rank = RequestedAccuracy::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}) + ), + 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!(serde_json::from_value::( + serde_json::json!({"error": 0.1, "typo": true}) + ) + .is_err()); + } + + /// Metrics have stable wire names. + #[test] + fn metric_wire_names() { + for (metric, name) in [ + (ErrorMetric::ABSOLUTE, "absolute"), + (ErrorMetric::ABSOLUTE_STREAM_L1, "absolute_stream_l1"), + (ErrorMetric::ABSOLUTE_STREAM_L2, "absolute_stream_l2"), + (ErrorMetric::Relative, "relative"), + (ErrorMetric::Rank, "rank"), + ] { + assert_eq!(serde_json::to_value(metric).unwrap(), name); + assert_eq!( + serde_json::from_value::(name.into()).unwrap(), + metric + ); + } + } + + /// A root's default metric follows its computation: counts and top-k + /// frequencies are L1-scaled absolute errors, distinct counts and L2 + /// frequency moments relative ones, anything else (a sum or an entropy + /// 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 exact = AccuracyTarget::exact(); + assert_eq!( + metric(AggIntent::Count { + accuracy: exact.clone() + }), + ErrorMetric::ABSOLUTE_STREAM_L1 + ); + assert_eq!(metric(AggIntent::Sum { col: None }), ErrorMetric::ABSOLUTE); + assert_eq!( + metric(AggIntent::TopK { + k: 5, + accuracy: exact.clone() + }), + ErrorMetric::ABSOLUTE_STREAM_L1 + ); + assert_eq!( + metric(AggIntent::Cardinality { + cols: vec![], + accuracy: exact.clone() + }), + ErrorMetric::Relative + ); + assert_eq!( + metric(AggIntent::FrequencyL2 { + col: None, + accuracy: exact.clone() + }), + ErrorMetric::Relative + ); + assert_eq!( + metric(AggIntent::FrequencyEntropy { + col: None, + accuracy: exact.clone() + }), + ErrorMetric::ABSOLUTE + ); + 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 + ); + } + + /// A quantile root's request must name its metric; a named metric or an + /// exact request needs no default. + #[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()); + } + + /// Exact means zero error with certainty, whatever the metric; an + /// ε of zero without a δ is not exact. + #[test] + fn exact_targets() { + assert!(AccuracyTarget::exact().is_exact()); + assert!(AccuracyTarget::exact() + .with_metric(ErrorMetric::Rank) + .is_exact()); + assert!(!AccuracyTarget::epsilon(0.0).is_exact()); + assert_eq!( + AccuracyTarget::epsilon_delta(0.01, 0.001).failure_probability, + Some(0.001) + ); + assert_eq!( + AccuracyTarget::epsilon(0.01).scope, + AccuracyScope::WholeResult + ); + } +} diff --git a/crates/types/src/workload.rs b/crates/types/src/workload.rs index c3da37ec3..06137ac12 100644 --- a/crates/types/src/workload.rs +++ b/crates/types/src/workload.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use crate::types::AccuracyTarget; +use crate::types::RequestedAccuracy; use serde::{Deserialize, Serialize}; // ── Query surface ───────────────────────────────────────────────────────────── @@ -57,19 +57,20 @@ pub enum QueryLanguage { // ── Per-query requirements ──────────────────────────────────────────────────── /// Whether the caller explicitly requested an accuracy target or inherited -/// the normalized exact default. +/// the normalized exact default. An explicit request may omit its metric +/// ([`RequestedAccuracy`]). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AccuracyRequirement { - Explicit(AccuracyTarget), + Explicit(RequestedAccuracy), ImplicitExact, } impl AccuracyRequirement { - pub fn target(&self) -> AccuracyTarget { + pub fn requested(&self) -> RequestedAccuracy { match self { - Self::Explicit(target) => target.clone(), - Self::ImplicitExact => AccuracyTarget::Exact, + Self::Explicit(requested) => requested.clone(), + Self::ImplicitExact => RequestedAccuracy::exact(), } } } @@ -935,10 +936,9 @@ mod tests { query: Query("rate(requests_total[5m])".into()), demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)), requirements: QueryRequirements { - accuracy: AccuracyRequirement::Explicit(AccuracyTarget::EpsilonDelta { - epsilon: 0.01, - delta: 0.001, - }), + accuracy: AccuracyRequirement::Explicit(RequestedAccuracy::epsilon_delta( + 0.01, 0.001, + )), response_latency: LatencyRequirement::ExplicitMaxMs(100.0), }, predictability: Predictability::Predictable { known_at: None }, @@ -1008,10 +1008,12 @@ 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!({ + "EpsilonDelta": {"epsilon": 0.1, "delta": 0.01, "typo": true} + })) + .is_err() + ); } #[test] diff --git a/tools/dag-viewer/app.js b/tools/dag-viewer/app.js index f1feb89d0..99f06be09 100644 --- a/tools/dag-viewer/app.js +++ b/tools/dag-viewer/app.js @@ -190,7 +190,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}` : ''}` : ''; + 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 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/editor.js b/tools/dag-viewer/editor.js index 2ee543559..b8d5bd20b 100644 --- a/tools/dag-viewer/editor.js +++ b/tools/dag-viewer/editor.js @@ -116,6 +116,7 @@ .filter((row) => row.querySelector('.schemaInclude').checked) .map(parseSchemaRow), epsilon: Number($('plannerEpsilon').value), + metric: $('plannerMetric').value, }), }); const result = await response.json(); diff --git a/tools/dag-viewer/examples/planner-layering-example1.json b/tools/dag-viewer/examples/planner-layering-example1.json index cfea60c0b..02716e1ec 100644 --- a/tools/dag-viewer/examples/planner-layering-example1.json +++ b/tools/dag-viewer/examples/planner-layering-example1.json @@ -544,10 +544,10 @@ "measures": [ { "accuracy": { - "EpsilonDelta": { - "delta": 0.001, - "epsilon": 0.01 - } + "error": 0.01, + "failure_probability": 0.001, + "metric": "absolute_stream_l1", + "scope": "whole_result" }, "k": 10, "kind": "top_k" @@ -610,19 +610,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -730,19 +733,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -884,19 +890,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1020,19 +1029,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1074,19 +1086,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1194,19 +1209,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1348,19 +1366,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1419,19 +1440,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1443,10 +1467,10 @@ "measures": [ { "accuracy": { - "EpsilonDelta": { - "delta": 0.001, - "epsilon": 0.01 - } + "error": 0.01, + "failure_probability": 0.001, + "metric": "absolute_stream_l1", + "scope": "whole_result" }, "k": 10, "kind": "top_k" @@ -1509,19 +1533,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1629,19 +1656,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1783,19 +1813,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1919,19 +1952,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -1973,19 +2009,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2127,19 +2166,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2198,19 +2240,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2222,10 +2267,10 @@ "measures": [ { "accuracy": { - "EpsilonDelta": { - "delta": 0.001, - "epsilon": 0.01 - } + "error": 0.01, + "failure_probability": 0.001, + "metric": "absolute_stream_l1", + "scope": "whole_result" }, "k": 10, "kind": "top_k" @@ -2288,19 +2333,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2408,19 +2456,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2562,19 +2613,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2698,19 +2752,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2830,19 +2887,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2901,19 +2961,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -2925,10 +2988,10 @@ "measures": [ { "accuracy": { - "EpsilonDelta": { - "delta": 0.001, - "epsilon": 0.01 - } + "error": 0.01, + "failure_probability": 0.001, + "metric": "absolute_stream_l1", + "scope": "whole_result" }, "k": 10, "kind": "top_k" @@ -2991,19 +3054,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3111,19 +3177,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3265,19 +3334,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3401,19 +3473,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3455,19 +3530,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3575,19 +3653,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3857,19 +3938,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -3977,19 +4061,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -4131,19 +4218,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -4267,19 +4357,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -4321,19 +4414,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -4603,19 +4699,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -4723,19 +4822,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -4877,19 +4979,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5013,19 +5118,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5273,19 +5381,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5393,19 +5504,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5547,19 +5661,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5683,19 +5800,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5737,19 +5857,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -5857,19 +5980,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -6139,19 +6265,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -6259,19 +6388,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -6413,19 +6545,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -6549,19 +6684,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -6603,19 +6741,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -6885,19 +7026,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -7005,19 +7149,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -7159,19 +7306,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -7295,19 +7445,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "materialization": "NotMaterialized", "operator": { @@ -8160,19 +8313,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -8283,19 +8439,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -8443,19 +8602,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -8585,19 +8747,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -8642,19 +8807,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 6, "materialization": "NotMaterialized", @@ -8765,19 +8933,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 7, "materialization": "NotMaterialized", @@ -8925,19 +9096,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 9, "materialization": "NotMaterialized", @@ -8999,19 +9173,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 10, "materialization": "NotMaterialized", @@ -9085,19 +9262,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 11, "materialization": "NotMaterialized", @@ -9164,19 +9344,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 12, "materialization": "NotMaterialized", @@ -9845,19 +10028,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -9968,19 +10154,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -10128,19 +10317,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -10270,19 +10462,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -10327,19 +10522,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 6, "materialization": "NotMaterialized", @@ -10487,19 +10685,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 8, "materialization": "NotMaterialized", @@ -10561,19 +10762,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 9, "materialization": "NotMaterialized", @@ -10647,19 +10851,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 10, "materialization": "NotMaterialized", @@ -10726,19 +10933,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 11, "materialization": "NotMaterialized", @@ -11352,19 +11562,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -11475,19 +11688,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -11635,19 +11851,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -11777,19 +11996,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -11915,19 +12137,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 7, "materialization": "NotMaterialized", @@ -11989,19 +12214,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 8, "materialization": "NotMaterialized", @@ -12075,19 +12303,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 9, "materialization": "NotMaterialized", @@ -12154,19 +12385,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 10, "materialization": "NotMaterialized", @@ -12701,19 +12935,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -12824,19 +13061,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -12984,19 +13224,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -13126,19 +13369,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -13183,19 +13429,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 6, "materialization": "NotMaterialized", @@ -13306,19 +13555,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 7, "materialization": "NotMaterialized", @@ -14063,19 +14315,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -14186,19 +14441,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -14346,19 +14604,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -14488,19 +14749,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -14545,19 +14809,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 6, "materialization": "NotMaterialized", @@ -15247,19 +15514,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -15370,19 +15640,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -15530,19 +15803,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -15672,19 +15948,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -16407,19 +16686,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -16530,19 +16812,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -16690,19 +16975,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -16832,19 +17120,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -16889,19 +17180,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 6, "materialization": "NotMaterialized", @@ -17012,19 +17306,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 7, "materialization": "NotMaterialized", @@ -17769,19 +18066,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -17892,19 +18192,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -18052,19 +18355,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -18194,19 +18500,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -18251,19 +18560,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 6, "materialization": "NotMaterialized", @@ -18953,19 +19265,22 @@ "nodes": [ { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 0, "materialization": "NotMaterialized", @@ -19076,19 +19391,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact over exact inputs" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 1, "materialization": "NotMaterialized", @@ -19236,19 +19554,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 3, "materialization": "NotMaterialized", @@ -19378,19 +19699,22 @@ }, { "guarantee": { - "bound": { - "op": "zero" - }, - "failure_probability": { - "op": "zero" - }, - "metric": "absolute_value", "provenance": [ { "kind": "exact", "reason": "exact accumulator over exact input" } - ] + ], + "spec": { + "error": { + "op": "zero" + }, + "failure_probability": { + "op": "zero" + }, + "metric": "absolute", + "scope": "per_value" + } }, "id": 5, "materialization": "NotMaterialized", @@ -19853,32 +20177,32 @@ "rejected": [ { "id": "P4", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }", + "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 }", "valid": false }, { "id": "P5", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }", + "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 }", "valid": false }, { "id": "P6", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }", + "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 }", "valid": false }, { "id": "P7", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }", + "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 }", "valid": false }, { "id": "P8", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }", + "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 }", "valid": false }, { "id": "P9", - "reason": "q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }", + "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 }", "valid": false }, { diff --git a/tools/dag-viewer/index.html b/tools/dag-viewer/index.html index 23ffce129..365fac965 100644 --- a/tools/dag-viewer/index.html +++ b/tools/dag-viewer/index.html @@ -151,6 +151,12 @@

ASAPPlanner Stage Viewer

+ Plans the checked queries as one workload.
diff --git a/tools/dag-viewer/server.py b/tools/dag-viewer/server.py index 16635c066..8519e313a 100644 --- a/tools/dag-viewer/server.py +++ b/tools/dag-viewer/server.py @@ -127,6 +127,15 @@ 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. + metric = payload.get("metric") + if metric not in (None, ""): + if metric not in ("absolute", "relative", "rank"): + raise ValueError("metric must be absolute, relative or rank") + if epsilon is None: + raise ValueError("metric needs epsilon") + args.extend(["--metric", metric]) interval = payload.get("interval_ms", 15_000) if isinstance(interval, bool) or not isinstance(interval, int) or interval <= 0: raise ValueError("interval_ms must be a positive integer") diff --git a/tools/dag-viewer/test_server.py b/tools/dag-viewer/test_server.py index 7bc92cfdd..a2fa8a2a9 100644 --- a/tools/dag-viewer/test_server.py +++ b/tools/dag-viewer/test_server.py @@ -34,6 +34,17 @@ def test_promql_queries_with_epsilon(self): ["--promql", "sum(rate(m[1m]))", "--name", "q1", "--epsilon", "0.01", "--interval-ms", "15000", "--out", "out.json"], ) + def test_metric_names_the_targets_error_metric(self): + query = {"language": "promql", "text": "quantile_over_time(0.99, m[1m])"} + for metric in ("absolute", "relative", "rank"): + with self.subTest(metric=metric): + args = server.plan_args({"queries": [query], "epsilon": 0.01, "metric": metric}, "out.json") + self.assertEqual(args[5:9], ["--epsilon", "0.01", "--metric", metric]) + for empty in ({}, {"metric": ""}, {"metric": None}): + with self.subTest(empty=empty): + args = server.plan_args({"queries": [query], "epsilon": 0.01, **empty}, "out.json") + self.assertNotIn("--metric", args) + def test_sql_queries_declare_their_tables(self): table = {"name": "metrics", "columns": [{"name": "ts", "type": "timestamp"}], "time_index": 0} args = server.plan_args( @@ -78,6 +89,9 @@ def test_field_types_are_checked(self): ({"queries": [{"language": "promql", "text": "m"}], "epsilon": True}, "epsilon must be a number"), ({"queries": [{"language": "promql", "text": "m"}], "epsilon": "0.1"}, "epsilon must be a number"), ({"queries": [{"language": "promql", "text": "m"}], "interval_ms": True}, "interval_ms must be a positive integer"), + ({"queries": [{"language": "promql", "text": "m"}], "epsilon": 0.01, "metric": "rnak"}, "metric must be absolute, relative or rank"), + ({"queries": [{"language": "promql", "text": "m"}], "epsilon": 0.01, "metric": 3}, "metric must be absolute, relative or rank"), + ({"queries": [{"language": "promql", "text": "m"}], "metric": "rank"}, "metric needs epsilon"), ({"schemas": [{"name": 3, "columns": [{}]}], "queries": [{"language": "sql", "text": "SELECT 1"}]}, "schema #1: name must be a non-empty string"), ] for payload, message in cases: @@ -206,27 +220,42 @@ def test_regenerate_rewrites_every_document(self): @unittest.skipUnless(server.BINARY.exists(), "needs a built stage_pipeline") +def post_plan(payload: dict) -> dict: + """POST `payload` to /api/plan on a fresh server and return the document.""" + httpd = ThreadingHTTPServer(("127.0.0.1", 0), server.Handler) + threading.Thread(target=httpd.serve_forever, daemon=True).start() + try: + request = urllib.request.Request( + f"http://127.0.0.1:{httpd.server_address[1]}/api/plan", + data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(request, timeout=120) as response: + return json.loads(response.read()) + finally: + httpd.shutdown() + httpd.server_close() + + class PlanEndpointTests(unittest.TestCase): def test_plan_endpoint_returns_the_stage_pipeline_document(self): - httpd = ThreadingHTTPServer(("127.0.0.1", 0), server.Handler) - threading.Thread(target=httpd.serve_forever, daemon=True).start() - try: - body = json.dumps({"queries": [{"name": "job_rate", "language": "promql", "text": "sum by (job) (rate(m[1m]))"}]}).encode() - request = urllib.request.Request( - f"http://127.0.0.1:{httpd.server_address[1]}/api/plan", - data=body, - headers={"Content-Type": "application/json"}, - ) - with urllib.request.urlopen(request, timeout=120) as response: - document = json.loads(response.read()) - finally: - httpd.shutdown() - httpd.server_close() + document = post_plan({"queries": [{"name": "job_rate", "language": "promql", "text": "sum by (job) (rate(m[1m]))"}]}) self.assertEqual(document["format"], "asap-stage-pipeline/v1") self.assertEqual(document["workload"]["queries"][0]["text"], "sum by (job) (rate(m[1m]))") 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.""" + 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"]) + 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"]) + if __name__ == "__main__": unittest.main() diff --git a/tools/dag-viewer/test_viewer.py b/tools/dag-viewer/test_viewer.py index 556bc55af..b77801cd2 100644 --- a/tools/dag-viewer/test_viewer.py +++ b/tools/dag-viewer/test_viewer.py @@ -368,6 +368,15 @@ def test_every_element_the_scripts_use_is_on_the_page(self): with self.subTest(script): self.assertLessEqual(used, ids) + def test_the_editor_names_a_target_metric(self): + """The metric dropdown offers the default (empty) and the three + metrics, and the editor sends its value to /api/plan.""" + page = (HERE / "index.html").read_text() + select = re.search(r'', page, re.S) + self.assertIsNotNone(select) + self.assertEqual(re.findall(r'