From 583112d6d7f69780812b7cdc5eb1d88400c832c5 Mon Sep 17 00:00:00 2001 From: zz_y Date: Mon, 24 Aug 2026 06:47:25 -0600 Subject: [PATCH 1/2] refactor(types): split SketchKind into category (SketchKind) + algorithm (SketchAlgorithm) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilt on top of feat/replacement-strategy-251's latest tip (the bind::implement_tree/_with deletion). Same split as before: SketchKind becomes a nested category type (Quantile/Cardinality/Frequency/TopK, each wrapping a committed (SketchAlgorithm, SketchParams) pair) with SketchAlgorithm holding the old flat variant list (Kll/Cms/Hll/ DDSketch/CmsWithHeap/Kmv/Theta/CountSketch/CountSketchWithHeap). SketchKind::new(algorithm, params) is the one classifier; .algorithm()/ .params() extract the committed pair back out. SketchFamilyStrategy renamed to SketchAlgorithmStrategy throughout, to free up "family" to mean what SummaryFamilyType names (Sketch/Sample/ Wavelet/StatModel/ExactAggregate) and "kind" to mean the new sub-family category, keeping "algorithm" for the concrete realization. Implementation::Sketch and SummaryFamilyType::Sketch both collapse from a (kind, params) pair to a single SketchKind field, since a SketchKind already carries its own committed (algorithm, params). Reapplied by hand against the current tree rather than via rebase/ cherry-pick (a prior rebase across these same overlapping renames silently dropped hunks without conflict markers) — guided by `git diff` against the previous build of this branch to find every site, then verified end to end. Verified: cargo build --workspace --all-targets, cargo test --workspace (0 failures), cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings — all clean. --- crates/asap-aware-mapping/src/bind.rs | 79 ++++--- crates/asap-aware-mapping/src/cost_model.rs | 55 +++-- .../asap-aware-mapping/src/implementation.rs | 203 ++++++++++-------- crates/asap-aware-mapping/src/lib.rs | 10 +- crates/asap-aware-mapping/src/replacement.rs | 110 +++++----- crates/devtools/src/bin/show_post_asap_ir.rs | 6 +- .../tests/observability/promql_corpus.rs | 6 +- crates/integration-tests/tests/l4_binding.rs | 22 +- .../integration-tests/tests/l4_binding_sql.rs | 27 ++- crates/types/src/post_asap/mod.rs | 17 +- crates/types/src/post_asap/schema.rs | 9 +- crates/types/src/post_asap/sketch.rs | 82 ++++++- .../ASAP-aware-mapping-developer-guide.md | 130 ++++++----- docs/user-guide/user-guide.md | 10 +- 14 files changed, 465 insertions(+), 301 deletions(-) diff --git a/crates/asap-aware-mapping/src/bind.rs b/crates/asap-aware-mapping/src/bind.rs index d64619e2..2ddc9ae5 100644 --- a/crates/asap-aware-mapping/src/bind.rs +++ b/crates/asap-aware-mapping/src/bind.rs @@ -1,7 +1,7 @@ //! The pre-ASAP → post-ASAP binding primitives (issue #98). //! //! This module does **not** expose a "bind me one tree" entry point. -//! [`replacement::SketchFamilyStrategy`](crate::replacement::SketchFamilyStrategy) +//! [`replacement::SketchAlgorithmStrategy`](crate::replacement::SketchAlgorithmStrategy) //! is the only public way to get bound output for a target — it always //! returns every candidate [`ReplacementSubDAG`](crate::replacement::ReplacementSubDAG), //! ranked; a caller that wants a single executable answer takes the first @@ -41,7 +41,7 @@ //! rank-and-take-first behavior internally (via a private selector), scoped //! to workload-wide CSE memoization specifically. It is not a general //! "bind me one tree" API — for a single target, go through -//! `SketchFamilyStrategy` and decide what to keep yourself. +//! `SketchAlgorithmStrategy` and decide what to keep yourself. //! //! ## Conservative fallbacks //! @@ -68,7 +68,7 @@ use thiserror::Error; use crate::cost_model::{CostModel, CseCandidate, DefaultCostModel, ShareDecision}; use crate::implementation::Implementation; use crate::replacement::{ - Replacement, ReplacementStrategy, ReplacementSubDAG, SketchFamilyStrategy, TargetSubDAG, + Replacement, ReplacementStrategy, ReplacementSubDAG, SketchAlgorithmStrategy, TargetSubDAG, }; /// Errors from the pre-ASAP → post-ASAP binding pass. @@ -79,7 +79,7 @@ pub enum ImplementError { Schema(#[from] QueryExprError), } -/// The bindable shape [`crate::replacement::SketchFamilyStrategy`] targets: +/// The bindable shape [`crate::replacement::SketchAlgorithmStrategy`] targets: /// a single intent, no `HAVING`. A multi-intent node /// (SQL `SELECT SUM(a), AVG(b)`), or one with a `HAVING` predicate (the /// filter would need the estimate first), stays logical — see the module @@ -107,7 +107,7 @@ fn select_and_bind( cost_model: &dyn CostModel, ) -> Result, ImplementError> { let target = TargetSubDAG::new(root); - match SketchFamilyStrategy::new(cost_model) + match SketchAlgorithmStrategy::new(cost_model) .replacements(&target) .into_iter() .next() @@ -120,12 +120,12 @@ fn select_and_bind( replacement: Replacement::Rewrite(_), .. }) => { - unreachable!("SketchFamilyStrategy never returns a Rewrite candidate") + unreachable!("SketchAlgorithmStrategy never returns a Rewrite candidate") } // No candidate at all: `root` isn't `bindable_intent` shape (or its // intent has no realization `implementations_for_with` can't // produce — never happens, that match is exhaustive) — the same - // conservative fallback `SketchFamilyStrategy::matches` uses. + // conservative fallback `SketchAlgorithmStrategy::matches` uses. None => logical(root), } } @@ -133,7 +133,7 @@ fn select_and_bind( /// Bind `expr` to an already-decided [`Implementation`] for its top intent. /// The shared low-level primitive: [`select_and_bind`] (workload-CSE /// memoization and this module's own recursion) calls this with whichever -/// candidate it kept; [`crate::replacement::SketchFamilyStrategy`] calls +/// candidate it kept; [`crate::replacement::SketchAlgorithmStrategy`] calls /// this once per candidate it enumerates. One function turns a chosen /// `Implementation` into a `SummaryNode`, used identically by both. /// @@ -272,7 +272,7 @@ fn summary_family(implementation: Implementation) -> Option<(SummaryFamilyType, Implementation::ExactAggregate { kind, params } => { (SummaryFamilyType::ExactAggregate(kind, params), false) } - Implementation::Sketch { kind, params } => (SummaryFamilyType::Sketch(kind, params), true), + Implementation::Sketch(kind) => (SummaryFamilyType::Sketch(kind), true), Implementation::Sample { kind, params } => (SummaryFamilyType::Sample(kind, params), true), Implementation::Wavelet { kind, params } => { (SummaryFamilyType::Wavelet(kind, params), true) @@ -408,7 +408,7 @@ fn readout(intent: &AggIntent, col: &ColumnRef, cost_model: &dyn CostModel) -> S /// Wrap an unrewritten pre-ASAP subtree, lifting its schema with every column /// `SummaryFamilyType::Plain`. Public so a caller can fall back to this -/// explicitly — e.g. when `SketchFamilyStrategy::replacements()` returns no +/// explicitly — e.g. when `SketchAlgorithmStrategy::replacements()` returns no /// candidate for a target, or a deployment wants to force a node its own /// runtime can't actually implement — through the same fallback this /// crate's own dispatch uses, without duplicating the schema-lift logic. @@ -438,7 +438,9 @@ fn lift(schema: &Schema) -> SummarySchema { #[cfg(test)] mod tests { use super::*; - use asap_types::post_asap::{ExactKind, ExactParams, SketchKind, SketchParams}; + use asap_types::post_asap::{ + ExactKind, ExactParams, SketchAlgorithm, SketchKind, SketchParams, + }; use asap_types::pre_asap::agg_intent::default_quantile; use asap_types::pre_asap::expr_ir::{CompareOpKind, ScalarValue}; use asap_types::pre_asap::query_expr::{Predicate, Source}; @@ -495,7 +497,7 @@ mod tests { /// pattern `select_and_bind` already implements, since `bind.rs`'s own /// tests are the one internal caller allowed to reach it directly. /// An external caller doesn't have this shortcut — it goes through - /// `SketchFamilyStrategy::replacements()` itself (see the module docs). + /// `SketchAlgorithmStrategy::replacements()` itself (see the module docs). fn bind_first( expr: &QueryExpr, cost_model: &dyn CostModel, @@ -543,14 +545,20 @@ mod tests { }; assert_eq!( family, - &SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 }) + &SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 } + )) ); assert_eq!(col, &ColumnRef::SampleValue); assert_eq!(reduction, &Reduction::by(vec![2])); // SummaryAgg edge: the state column carries the committed family. assert_eq!( field(&summary_input.schema, "quantile_0_99").dtype, - SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 }) + SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 } + )) ); assert!(matches!(child.expr, SummaryExpr::Logical(ref e) if matches!(**e, QueryExpr::Scan { .. }))); @@ -566,10 +574,10 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { let mut v = candidates.to_vec(); - if let Some(pos) = v.iter().position(|k| *k == SketchKind::DDSketch) { + if let Some(pos) = v.iter().position(|k| *k == SketchAlgorithm::DDSketch) { let ddsketch = v.remove(pos); v.insert(0, ddsketch); } @@ -591,7 +599,7 @@ mod tests { }; assert!(matches!( family, - SummaryFamilyType::Sketch(SketchKind::Kll, _) + SummaryFamilyType::Sketch(SketchKind::Quantile(SketchAlgorithm::Kll, _)) )); // With `PreferDDSketch`: DDSketch instead, same query. @@ -604,10 +612,10 @@ mod tests { }; assert_eq!( family, - &SummaryFamilyType::Sketch( - SketchKind::DDSketch, + &SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::DDSketch, SketchParams::DDSketch { alpha: 0.01 } - ) + )) ); } @@ -622,8 +630,8 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { candidates.to_vec() } @@ -633,13 +641,13 @@ mod tests { _payload: &serde_json::Value, ) -> crate::implementation::Implementation { if ext_kind == "frequency" { - crate::implementation::Implementation::Sketch { - kind: SketchKind::CountSketch, - params: SketchParams::CountSketch { + crate::implementation::Implementation::Sketch(SketchKind::Frequency( + SketchAlgorithm::CountSketch, + SketchParams::CountSketch { width: 256, depth: 4, }, - } + )) } else { crate::implementation::Implementation::PassThrough } @@ -701,13 +709,13 @@ mod tests { }; assert_eq!( family, - &SummaryFamilyType::Sketch( - SketchKind::CountSketch, + &SummaryFamilyType::Sketch(SketchKind::Frequency( + SketchAlgorithm::CountSketch, SketchParams::CountSketch { width: 256, depth: 4 } - ) + )) ); } @@ -828,7 +836,7 @@ mod tests { }; assert!(matches!( family, - SummaryFamilyType::Sketch(SketchKind::Kll, _) + SummaryFamilyType::Sketch(SketchKind::Quantile(SketchAlgorithm::Kll, _)) )); let SummaryExpr::SummaryAgg { family: inner_family, @@ -964,7 +972,10 @@ mod tests { assert!(matches!( &summary_input.expr, SummaryExpr::SummaryAgg { - family: SummaryFamilyType::Sketch(SketchKind::CmsWithHeap, _), + family: SummaryFamilyType::Sketch(SketchKind::TopK( + SketchAlgorithm::CmsWithHeap, + _ + )), .. } )); @@ -1010,8 +1021,8 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { candidates.to_vec() } fn cse_share_decision(&self, _candidate: &CseCandidate) -> ShareDecision { diff --git a/crates/asap-aware-mapping/src/cost_model.rs b/crates/asap-aware-mapping/src/cost_model.rs index 027d626a..84636192 100644 --- a/crates/asap-aware-mapping/src/cost_model.rs +++ b/crates/asap-aware-mapping/src/cost_model.rs @@ -13,7 +13,7 @@ //! //! This trait is scoped to the approximate-**sketch** family specifically //! ([`CostModel::rank_candidates`]/[`size_params`](CostModel::size_params) -//! take/return [`SketchKind`]/[`SketchParams`]) — `asap_sketch` also has +//! take/return [`SketchAlgorithm`]/[`SketchParams`]) — `asap_sketch` also has //! sibling families for sampling-based, wavelet-transform, and fitted //! statistical-model summaries //! ([`asap_types::post_asap::SamplingKind`]/…/[`asap_types::post_asap::StatModelKind`]), @@ -26,7 +26,7 @@ //! than overloading these ones across incompatible `Kind`/`Params` types. //! //! Every entry point that doesn't take an explicit `&dyn CostModel` -//! ([`SketchFamilyStrategy::default_cost_model`](crate::replacement::SketchFamilyStrategy::default_cost_model), +//! ([`SketchAlgorithmStrategy::default_cost_model`](crate::replacement::SketchAlgorithmStrategy::default_cost_model), //! [`implement_workload`](crate::bind::implement_workload)) runs against //! [`DefaultCostModel`], so a deployment that never plugs in its own cost //! model keeps today's static-preference-order behavior exactly, byte for @@ -45,7 +45,7 @@ //! is the caller. use asap_types::post_asap::{ - SketchKind, SketchParams, SketchQuery, SummaryFamilyType, SummaryNode, + SketchAlgorithm, SketchParams, SketchQuery, SummaryFamilyType, SummaryNode, }; use asap_types::pre_asap::agg_intent::AggIntent; use asap_types::pre_asap::expr_ir::ColumnRef; @@ -181,13 +181,17 @@ pub trait CostModel { /// /// Implementations MAY reorder freely and MAY drop entries that aren't /// available in their deployment, but MUST NOT invent a candidate that - /// wasn't in the input — an unknown [`SketchKind`] has no + /// wasn't in the input — an unknown [`SketchAlgorithm`] has no /// [`SketchParams`](asap_types::post_asap::SketchParams) sizing logic in /// [`implementation::implementations_for_with`] and binding it will panic. /// Returning an empty `Vec` means "no candidate is acceptable"; /// `implementations_for_with` treats that the same as `candidates` /// having been empty to begin with. - fn rank_candidates(&self, intent: &AggIntent, candidates: &[SketchKind]) -> Vec; + fn rank_candidates( + &self, + intent: &AggIntent, + candidates: &[SketchAlgorithm], + ) -> Vec; /// Size [`SketchParams`] for `kind` (one of the candidates /// [`rank_candidates`](Self::rank_candidates) put first) under the @@ -203,7 +207,7 @@ pub trait CostModel { /// not resize them, can leave this method unimplemented. fn size_params( &self, - kind: SketchKind, + kind: SketchAlgorithm, intent: &AggIntent, eps: f64, delta: f64, @@ -305,7 +309,11 @@ pub trait CostModel { pub struct DefaultCostModel; impl CostModel for DefaultCostModel { - fn rank_candidates(&self, _intent: &AggIntent, candidates: &[SketchKind]) -> Vec { + fn rank_candidates( + &self, + _intent: &AggIntent, + candidates: &[SketchAlgorithm], + ) -> Vec { candidates.to_vec() } } @@ -332,8 +340,8 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { let mut v = candidates.to_vec(); v.reverse(); v @@ -355,8 +363,8 @@ mod tests { fn size_params_default_body_matches_default_size_params() { let intent = default_cardinality(); assert_eq!( - AlwaysPreferLast.size_params(SketchKind::Hll, &intent, 0.01, 0.01), - crate::implementation::default_size_params(SketchKind::Hll, &intent, 0.01, 0.01), + AlwaysPreferLast.size_params(SketchAlgorithm::Hll, &intent, 0.01, 0.01), + crate::implementation::default_size_params(SketchAlgorithm::Hll, &intent, 0.01, 0.01), ); } @@ -370,20 +378,20 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { candidates.to_vec() } fn size_params( &self, - kind: SketchKind, + kind: SketchAlgorithm, intent: &AggIntent, eps: f64, delta: f64, ) -> SketchParams { match kind { - SketchKind::Kll => { + SketchAlgorithm::Kll => { let k = if eps >= 0.01 { 200 } else { 2048 }; SketchParams::Kll { k } } @@ -398,19 +406,21 @@ mod tests { let intent = default_quantile(0.99); assert_eq!( - DiscreteKllRungs.size_params(SketchKind::Kll, &intent, 0.001, 0.01), + DiscreteKllRungs.size_params(SketchAlgorithm::Kll, &intent, 0.001, 0.01), SketchParams::Kll { k: 2048 }, ); // Untouched kinds still fall through to the default formula. assert_eq!( - DiscreteKllRungs.size_params(SketchKind::Hll, &intent, 0.01, 0.01), - crate::implementation::default_size_params(SketchKind::Hll, &intent, 0.01, 0.01), + DiscreteKllRungs.size_params(SketchAlgorithm::Hll, &intent, 0.01, 0.01), + crate::implementation::default_size_params(SketchAlgorithm::Hll, &intent, 0.01, 0.01), ); } // ── CSE sharing (issue #237, #223 stage 4) ────────────────────────── - use asap_types::post_asap::{ExactKind, ExactParams, SummaryExpr, SummaryField, SummarySchema}; + use asap_types::post_asap::{ + ExactKind, ExactParams, SketchKind, SummaryExpr, SummaryField, SummarySchema, + }; use asap_types::pre_asap::query_expr::Source; use asap_types::pre_asap::schema::{Column, DataType, Schema}; @@ -515,8 +525,7 @@ mod tests { ExactParams::Sum, )); let sketch = default_cse_shared_maintenance_cost(&SummaryFamilyType::Sketch( - SketchKind::Hll, - SketchParams::Hll { precision: 12 }, + SketchKind::Cardinality(SketchAlgorithm::Hll, SketchParams::Hll { precision: 12 }), )); assert!( exact < sketch, @@ -572,8 +581,8 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { candidates.to_vec() } fn cse_recompute_cost(&self, _candidate: &CseCandidate) -> Cost { diff --git a/crates/asap-aware-mapping/src/implementation.rs b/crates/asap-aware-mapping/src/implementation.rs index dd14d9d9..1e9366de 100644 --- a/crates/asap-aware-mapping/src/implementation.rs +++ b/crates/asap-aware-mapping/src/implementation.rs @@ -10,7 +10,7 @@ //! enumerated, exhaustive and ranked (most-preferred first) — this crate has //! no separate function that computes just "the one" `Implementation` //! independently of that list. -//! [`crate::replacement::SketchFamilyStrategy`] is the sole public consumer: +//! [`crate::replacement::SketchAlgorithmStrategy`] is the sole public consumer: //! it wraps every entry of this list into its own bound //! [`SummaryNode`](asap_types::post_asap::SummaryNode) and returns all of //! them, ranked — a caller wanting a single answer keeps the first one @@ -42,8 +42,8 @@ //! opinion on when that's the right choice. use asap_types::post_asap::{ - ExactKind, ExactParams, SamplingKind, SamplingParams, SketchKind, SketchParams, StatModelKind, - StatModelParams, WaveletKind, WaveletParams, + ExactKind, ExactParams, SamplingKind, SamplingParams, SketchAlgorithm, SketchKind, + SketchParams, StatModelKind, StatModelParams, WaveletKind, WaveletParams, }; use asap_types::pre_asap::agg_intent::{agg_is_mergeable, AggIntent}; use asap_types::types::AccuracyTarget; @@ -62,11 +62,11 @@ pub enum Implementation { params: ExactParams, }, /// An approximate sketch sized to the intent's [`AccuracyTarget`]. - /// Needs a `SummaryEstimate` readout to recover a value. - Sketch { - kind: SketchKind, - params: SketchParams, - }, + /// Needs a `SummaryEstimate` readout to recover a value. Already + /// classified into its [`SketchKind`] category (`SketchKind::new` + /// having been called) — construction always goes through that + /// classifier, never this variant directly. + Sketch(SketchKind), /// A sampling-based summary (a retained row subset). Needs a /// `SummaryEstimate` readout. Not chosen by any core `AggIntent` /// dispatch today — see the module docs. @@ -138,17 +138,24 @@ pub trait Matcher { pub const DEFAULT_DELTA: f64 = 0.01; /// The sketch kinds that can serve an intent, most-preferred first. -/// This is the `AggIntent → SketchKind` map of issue #98; +/// This is the `AggIntent → SketchAlgorithm` map of issue #98; /// [`implementations_for_with`] sizes and ranks every entry via `cost_model`. /// Listed here so the candidate set has one home. -pub fn summary_candidates(intent: &AggIntent) -> &'static [SketchKind] { +pub fn summary_candidates(intent: &AggIntent) -> &'static [SketchAlgorithm] { match intent { - AggIntent::Quantile { .. } => &[SketchKind::Kll, SketchKind::DDSketch], - AggIntent::Cardinality { .. } => &[SketchKind::Hll, SketchKind::Theta, SketchKind::Kmv], + AggIntent::Quantile { .. } => &[SketchAlgorithm::Kll, SketchAlgorithm::DDSketch], + AggIntent::Cardinality { .. } => &[ + SketchAlgorithm::Hll, + SketchAlgorithm::Theta, + SketchAlgorithm::Kmv, + ], // Count-Sketch-with-heap is CMS-with-heap's balanced/zero-mean-error // alternative for the same heavy-hitter shape. - AggIntent::TopK { .. } => &[SketchKind::CmsWithHeap, SketchKind::CountSketchWithHeap], - AggIntent::Count { .. } => &[SketchKind::Cms, SketchKind::CountSketch], + AggIntent::TopK { .. } => &[ + SketchAlgorithm::CmsWithHeap, + SketchAlgorithm::CountSketchWithHeap, + ], + AggIntent::Count { .. } => &[SketchAlgorithm::Cms, SketchAlgorithm::CountSketch], _ => &[], } } @@ -157,7 +164,7 @@ pub fn summary_candidates(intent: &AggIntent) -> &'static [SketchKind] { /// (`Quantile`/`Cardinality`/`Count`/`TopK`), or `None` for every other /// intent (no sketch candidate applies — [`implementations_for_with`]'s own /// match routes those elsewhere). Exposed so -/// [`crate::replacement::SketchFamilyStrategy`] and +/// [`crate::replacement::SketchAlgorithmStrategy`] and /// [`implementations_for_with`] resolve the exact same accuracy target, /// without either re-deriving it from scratch. pub fn accuracy_target(intent: &AggIntent) -> Option<&AccuracyTarget> { @@ -174,7 +181,7 @@ pub fn accuracy_target(intent: &AggIntent) -> Option<&AccuracyTarget> { /// (most-preferred first via `cost_model`) — the *only* place this crate /// decides what an `AggIntent` may become. Nothing in this crate computes /// "the one" `Implementation` independently of this list: -/// [`crate::replacement::SketchFamilyStrategy`] keeps every entry as a +/// [`crate::replacement::SketchAlgorithmStrategy`] keeps every entry as a /// candidate, and a caller that wants a single executable answer takes the /// head of *that* strategy's output itself — see its own module docs. /// @@ -327,9 +334,9 @@ fn sketch_implementations( let ranked = cost_model.rank_candidates(intent, summary_candidates(intent)); ranked .into_iter() - .map(|kind| { - let params = cost_model.size_params(kind.clone(), intent, eps, delta); - Implementation::Sketch { kind, params } + .map(|algorithm| { + let params = cost_model.size_params(algorithm.clone(), intent, eps, delta); + Implementation::Sketch(SketchKind::new(algorithm, params)) }) .collect() } @@ -345,21 +352,21 @@ fn sketch_implementations( /// range. A non-positive ε saturates to the clamp maximum (tightest /// allowed). pub fn default_size_params( - kind: SketchKind, + kind: SketchAlgorithm, intent: &AggIntent, eps: f64, delta: f64, ) -> SketchParams { match kind { - SketchKind::Kll => SketchParams::Kll { k: kll_k(eps) }, - SketchKind::Cms => SketchParams::Cms { + SketchAlgorithm::Kll => SketchParams::Kll { k: kll_k(eps) }, + SketchAlgorithm::Cms => SketchParams::Cms { width: cms_width(eps), depth: cms_depth(delta), }, - SketchKind::Hll => SketchParams::Hll { + SketchAlgorithm::Hll => SketchParams::Hll { precision: hll_precision(eps), }, - SketchKind::CmsWithHeap => { + SketchAlgorithm::CmsWithHeap => { let k = match intent { AggIntent::TopK { k, .. } => *k, _ => unreachable!("CmsWithHeap is only a TopK candidate"), @@ -373,20 +380,20 @@ pub fn default_size_params( // Non-preferred candidates (DDSketch / Theta / Kmv / CountSketch / // CountSketchWithHeap) are only reachable once a cost model picks // them; sized here so that wiring is local. - SketchKind::DDSketch => SketchParams::DDSketch { alpha: eps }, - SketchKind::Theta => SketchParams::Theta { k: kmv_k(eps) }, - SketchKind::Kmv => SketchParams::Kmv { k: kmv_k(eps) }, + SketchAlgorithm::DDSketch => SketchParams::DDSketch { alpha: eps }, + SketchAlgorithm::Theta => SketchParams::Theta { k: kmv_k(eps) }, + SketchAlgorithm::Kmv => SketchParams::Kmv { k: kmv_k(eps) }, // Count-Sketch is CMS's balanced/zero-mean-error alternative — // same (width, depth) shape, sized the same way for now (a // Count-Sketch-specific bound uses an L2-norm error guarantee // rather than CMS's L1-norm one; this is a placeholder pending // that refinement, same status as the other non-preferred // candidates above). - SketchKind::CountSketch => SketchParams::CountSketch { + SketchAlgorithm::CountSketch => SketchParams::CountSketch { width: cms_width(eps), depth: cms_depth(delta), }, - SketchKind::CountSketchWithHeap => { + SketchAlgorithm::CountSketchWithHeap => { let k = match intent { AggIntent::TopK { k, .. } => *k, _ => unreachable!("CountSketchWithHeap is only a TopK candidate"), @@ -457,7 +464,7 @@ pub struct ExpectedCaseSizing { /// stated assumption, not a full redesign of the depth/width tradeoff /// space, so depth relaxation is left as explicit future scope. /// -/// For every `SketchKind` outside the CMS family, this is identical to +/// For every `SketchAlgorithm` outside the CMS family, this is identical to /// [`default_size_params`] — `width_relaxation` only ever touches the /// [`cms_width`]-sized formulas this issue is about. /// @@ -465,7 +472,7 @@ pub struct ExpectedCaseSizing { /// function's existence — this is a separate, additive entry point, never /// called from [`default_size_params`] or [`implementations_for_with`]. pub fn posterior_aware_size_params( - kind: SketchKind, + kind: SketchAlgorithm, intent: &AggIntent, eps: f64, delta: f64, @@ -480,11 +487,11 @@ pub fn posterior_aware_size_params( saturating_ceil(base as f64 * f, 2, base) }; match kind { - SketchKind::Cms => SketchParams::Cms { + SketchAlgorithm::Cms => SketchParams::Cms { width: relaxed_width(eps), depth: cms_depth(delta), }, - SketchKind::CmsWithHeap => { + SketchAlgorithm::CmsWithHeap => { let k = match intent { AggIntent::TopK { k, .. } => *k, _ => unreachable!("CmsWithHeap is only a TopK candidate"), @@ -495,11 +502,11 @@ pub fn posterior_aware_size_params( heap_size: k as u32, } } - SketchKind::CountSketch => SketchParams::CountSketch { + SketchAlgorithm::CountSketch => SketchParams::CountSketch { width: relaxed_width(eps), depth: cms_depth(delta), }, - SketchKind::CountSketchWithHeap => { + SketchAlgorithm::CountSketchWithHeap => { let k = match intent { AggIntent::TopK { k, .. } => *k, _ => unreachable!("CountSketchWithHeap is only a TopK candidate"), @@ -513,14 +520,14 @@ pub fn posterior_aware_size_params( // Every other kind is untouched by this issue's CMS-specific // relaxation — defer to the existing formula verbatim. Spelled out // exhaustively, matching `default_size_params`'s own match, rather - // than a wildcard arm: a future `SketchKind` variant then fails to + // than a wildcard arm: a future `SketchAlgorithm` variant then fails to // compile *here* too, instead of silently inheriting worst-case // sizing with no signal that this function never considered it. - SketchKind::Kll => default_size_params(kind, intent, eps, delta), - SketchKind::Hll => default_size_params(kind, intent, eps, delta), - SketchKind::DDSketch => default_size_params(kind, intent, eps, delta), - SketchKind::Theta => default_size_params(kind, intent, eps, delta), - SketchKind::Kmv => default_size_params(kind, intent, eps, delta), + SketchAlgorithm::Kll => default_size_params(kind, intent, eps, delta), + SketchAlgorithm::Hll => default_size_params(kind, intent, eps, delta), + SketchAlgorithm::DDSketch => default_size_params(kind, intent, eps, delta), + SketchAlgorithm::Theta => default_size_params(kind, intent, eps, delta), + SketchAlgorithm::Kmv => default_size_params(kind, intent, eps, delta), } } @@ -592,7 +599,7 @@ mod tests { /// Shorthand for asserting the realization *category*. #[derive(Debug, PartialEq)] enum Cat { - Sketch(SketchKind), + Sketch(SketchAlgorithm), Acc(ExactKind), Pass, } @@ -600,7 +607,7 @@ mod tests { fn cat(intent: &AggIntent) -> Cat { match preferred(intent) { Implementation::ExactAggregate { kind, .. } => Cat::Acc(kind), - Implementation::Sketch { kind, .. } => Cat::Sketch(kind), + Implementation::Sketch(kind) => Cat::Sketch(kind.algorithm().clone()), Implementation::PassThrough => Cat::Pass, other => { panic!("this coverage matrix expects only Exact/Sketch/PassThrough, got {other:?}") @@ -618,7 +625,7 @@ mod tests { use AggIntent as A; use Cat::*; use ExactKind as E; - use SketchKind as K; + use SketchAlgorithm as K; let matrix: Vec<(A, Cat)> = vec![ // approximate-capable, at an ε target → sketch (default_quantile(0.99), Sketch(K::Kll)), @@ -765,10 +772,10 @@ mod tests { let approx = default_quantile(0.99); // ε = 0.01 assert_eq!( preferred(&approx), - Implementation::Sketch { - kind: SketchKind::Kll, - params: SketchParams::Kll { k: 200 }, // design.md worked example - } + Implementation::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 }, // design.md worked example + )) ); let looser = AggIntent::Quantile { @@ -778,10 +785,10 @@ mod tests { }; assert_eq!( preferred(&looser), - Implementation::Sketch { - kind: SketchKind::Kll, - params: SketchParams::Kll { k: 40 }, // ⌈2/0.05⌉ - } + Implementation::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 40 }, // ⌈2/0.05⌉ + )) ); } @@ -791,10 +798,10 @@ mod tests { // sizing must invert it back exactly. assert_eq!( preferred(&default_cardinality()), - Implementation::Sketch { - kind: SketchKind::Hll, - params: SketchParams::Hll { precision: 14 }, - } + Implementation::Sketch(SketchKind::Cardinality( + SketchAlgorithm::Hll, + SketchParams::Hll { precision: 14 }, + )) ); } @@ -808,13 +815,13 @@ mod tests { }; assert_eq!( preferred(&intent), - Implementation::Sketch { - kind: SketchKind::Cms, - params: SketchParams::Cms { + Implementation::Sketch(SketchKind::Frequency( + SketchAlgorithm::Cms, + SketchParams::Cms { width: 2719, depth: 7 }, // ⌈e/0.001⌉, ⌈ln 1000⌉ - } + )) ); // Epsilon-only falls back to DEFAULT_DELTA → depth 5. let intent = AggIntent::Count { @@ -822,13 +829,13 @@ mod tests { }; assert_eq!( preferred(&intent), - Implementation::Sketch { - kind: SketchKind::Cms, - params: SketchParams::Cms { + Implementation::Sketch(SketchKind::Frequency( + SketchAlgorithm::Cms, + SketchParams::Cms { width: 2719, depth: 5 }, - } + )) ); } @@ -839,15 +846,14 @@ mod tests { accuracy: eps(0.01), }; match preferred(&intent) { - Implementation::Sketch { - kind: SketchKind::CmsWithHeap, - params: - SketchParams::CmsWithHeap { - width, - depth, - heap_size, - }, - } => { + Implementation::Sketch(SketchKind::TopK( + SketchAlgorithm::CmsWithHeap, + SketchParams::CmsWithHeap { + width, + depth, + heap_size, + }, + )) => { assert_eq!(heap_size, 25); assert_eq!(width, 272); // ⌈e/0.01⌉ assert_eq!(depth, 5); @@ -860,24 +866,31 @@ mod tests { fn candidate_lists_match_the_issue_map() { assert_eq!( summary_candidates(&default_quantile(0.5)), - &[SketchKind::Kll, SketchKind::DDSketch] + &[SketchAlgorithm::Kll, SketchAlgorithm::DDSketch] ); assert_eq!( summary_candidates(&default_cardinality()), - &[SketchKind::Hll, SketchKind::Theta, SketchKind::Kmv] + &[ + SketchAlgorithm::Hll, + SketchAlgorithm::Theta, + SketchAlgorithm::Kmv + ] ); assert_eq!( summary_candidates(&AggIntent::TopK { k: 5, accuracy: eps(0.01) }), - &[SketchKind::CmsWithHeap, SketchKind::CountSketchWithHeap] + &[ + SketchAlgorithm::CmsWithHeap, + SketchAlgorithm::CountSketchWithHeap + ] ); assert_eq!( summary_candidates(&AggIntent::Count { accuracy: eps(0.01) }), - &[SketchKind::Cms, SketchKind::CountSketch] + &[SketchAlgorithm::Cms, SketchAlgorithm::CountSketch] ); assert!(summary_candidates(&AggIntent::Rate).is_empty()); } @@ -887,15 +900,15 @@ mod tests { // Quantile's candidate list is [Kll, DDSketch] — implementations_for_with // must return both, ranked with the DefaultCostModel's preferred // (Kll) first. - let kinds: Vec = + let kinds: Vec = implementations_for_with(&default_quantile(0.99), &DefaultCostModel) .into_iter() .map(|implementation| match implementation { - Implementation::Sketch { kind, .. } => kind, + Implementation::Sketch(kind) => kind.algorithm().clone(), other => panic!("expected Sketch, got {other:?}"), }) .collect(); - assert_eq!(kinds, vec![SketchKind::Kll, SketchKind::DDSketch]); + assert_eq!(kinds, vec![SketchAlgorithm::Kll, SketchAlgorithm::DDSketch]); } #[test] @@ -907,10 +920,10 @@ mod tests { }; assert_eq!( preferred(&intent), - Implementation::Sketch { - kind: SketchKind::Kll, - params: SketchParams::Kll { k: 65_535 }, - } + Implementation::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 65_535 }, + )) ); } @@ -923,9 +936,9 @@ mod tests { #[test] fn posterior_aware_sizing_shrinks_width_under_stated_assumption() { let intent = count_intent(0.01); - let worst_case = default_size_params(SketchKind::Cms, &intent, 0.01, 0.01); + let worst_case = default_size_params(SketchAlgorithm::Cms, &intent, 0.01, 0.01); let relaxed = posterior_aware_size_params( - SketchKind::Cms, + SketchAlgorithm::Cms, &intent, 0.01, 0.01, @@ -959,9 +972,9 @@ mod tests { // width_relaxation = 1.0 must reproduce default_size_params exactly // — the "no risk taken" boundary. let intent = count_intent(0.01); - let worst_case = default_size_params(SketchKind::Cms, &intent, 0.01, 0.01); + let worst_case = default_size_params(SketchAlgorithm::Cms, &intent, 0.01, 0.01); let relaxed = posterior_aware_size_params( - SketchKind::Cms, + SketchAlgorithm::Cms, &intent, 0.01, 0.01, @@ -975,10 +988,10 @@ mod tests { #[test] fn posterior_aware_sizing_invalid_relaxation_falls_back_to_worst_case() { let intent = count_intent(0.01); - let worst_case = default_size_params(SketchKind::Cms, &intent, 0.01, 0.01); + let worst_case = default_size_params(SketchAlgorithm::Cms, &intent, 0.01, 0.01); for bad in [0.0, -0.5, 1.5, f64::NAN, f64::INFINITY] { let relaxed = posterior_aware_size_params( - SketchKind::Cms, + SketchAlgorithm::Cms, &intent, 0.01, 0.01, @@ -1005,7 +1018,7 @@ mod tests { // CountSketch assert_eq!( posterior_aware_size_params( - SketchKind::CountSketch, + SketchAlgorithm::CountSketch, &count_intent(0.01), 0.01, 0.01, @@ -1018,7 +1031,7 @@ mod tests { ); // CmsWithHeap / CountSketchWithHeap carry k through untouched. match posterior_aware_size_params( - SketchKind::CmsWithHeap, + SketchAlgorithm::CmsWithHeap, &cms_heap_intent, 0.01, 0.01, @@ -1046,8 +1059,8 @@ mod tests { width_relaxation: 0.1, }; assert_eq!( - posterior_aware_size_params(SketchKind::Kll, &intent, 0.01, 0.01, assumption), - default_size_params(SketchKind::Kll, &intent, 0.01, 0.01), + posterior_aware_size_params(SketchAlgorithm::Kll, &intent, 0.01, 0.01, assumption), + default_size_params(SketchAlgorithm::Kll, &intent, 0.01, 0.01), ); } @@ -1057,7 +1070,7 @@ mod tests { // existing callers must be untouched by adding // posterior_aware_size_params alongside it. assert_eq!( - default_size_params(SketchKind::Cms, &count_intent(0.001), 0.001, 0.001), + default_size_params(SketchAlgorithm::Cms, &count_intent(0.001), 0.001, 0.001), SketchParams::Cms { width: 2719, depth: 7 diff --git a/crates/asap-aware-mapping/src/lib.rs b/crates/asap-aware-mapping/src/lib.rs index 1cf35bc2..65a6cea4 100644 --- a/crates/asap-aware-mapping/src/lib.rs +++ b/crates/asap-aware-mapping/src/lib.rs @@ -8,7 +8,7 @@ //! **Common sub-expression elimination (CSE) is not this crate's job.** //! Detection is a primary pass over the pre-ASAP `QueryExpr` IR itself //! (`asap_types::pre_asap`, design tracked in issue #223), run before a -//! tree ever reaches [`replacement::SketchFamilyStrategy`] — see issue #222 +//! tree ever reaches [`replacement::SketchAlgorithmStrategy`] — see issue #222 //! for why (batch query optimization needs to see shared work across a //! `QueryWorkload` before summary binding, not after). This crate may //! eventually run a second, narrower CSE pass of its own over an @@ -39,7 +39,7 @@ //! - [`replacement`] — the `TargetSubDAG`/`ReplacementSubDAG`/ //! `ReplacementStrategy` vocabulary `docs/design_docs/asap_aware_mapping.md` stubs out //! under "Key concepts (not yet implemented)", implemented for real (issue -//! #251, part of #33): [`replacement::SketchFamilyStrategy`] wraps +//! #251, part of #33): [`replacement::SketchAlgorithmStrategy`] wraps //! [`implementation::implementations_for_with`]'s list directly, keeping //! *every* candidate as its own bound [`SummaryNode`](asap_types::post_asap::SummaryNode) //! instead of just one — [`replacement::SharedSubtreeStrategy`] does the @@ -48,7 +48,7 @@ //! lives here — see that module's docs for what's deliberately left to a //! future Cascades/Volcano-style search engine. //! - [`bind`] — has no "bind me one tree" entry point of its own. -//! [`replacement::SketchFamilyStrategy`] is the only public way to get +//! [`replacement::SketchAlgorithmStrategy`] is the only public way to get //! bound output for a target, and it always returns *every* candidate; a //! caller that wants one answer takes the first entry itself. What //! `bind` provides is the shared low-level primitive @@ -89,7 +89,7 @@ //! | **Parse** | parse | text (PromQL/SQL) → AST | `asap-frontend-promql` / `asap-frontend-sql` | //! | **Bind #1** | name resolution | `ColumnRef` (a name) → `ColumnId` (a concrete schema column) — the classic RDBMS "Parse → **Bind** → Optimize" pipeline sense (e.g. SQL Server's query-processor terminology) | [`asap_types::pre_asap::binder::Binder`](https://docs.rs/asap-types) | //! | **Implementation** — [`implementation::implementations_for_with`] | pre-ASAP → post-ASAP, *one node* | enumerating every concrete physical realization (a sketch family, an exact accumulator, or pass-through) for one [`AggIntent`](asap_types::pre_asap::agg_intent::AggIntent) | [`implementation`] | -//! | **Replacement** — [`replacement::SketchFamilyStrategy::replacements`] | pre-ASAP → post-ASAP, *one target, every candidate* | wrap each [`implementation::implementations_for_with`] candidate into its own bound [`SummaryNode`](asap_types::post_asap::SummaryNode), ranked — a caller wanting one answer takes the first entry itself | [`replacement`] | +//! | **Replacement** — [`replacement::SketchAlgorithmStrategy::replacements`] | pre-ASAP → post-ASAP, *one target, every candidate* | wrap each [`implementation::implementations_for_with`] candidate into its own bound [`SummaryNode`](asap_types::post_asap::SummaryNode), ranked — a caller wanting one answer takes the first entry itself | [`replacement`] | //! | **`implement_workload`** — [`bind::implement_workload`] | pre-ASAP → post-ASAP, *whole workload* | walk every root of a `QueryWorkload`, keeping the first (`cost_model`-preferred) candidate per node, sharing one bound `SummaryNode` across roots CSE already collapsed onto one `Rc` — emits the complete post-ASAP `SummaryExpr`/`SummaryNode` DAG | [`bind`] | //! | **Bind #2** (downstream, not in this crate) | post-ASAP → deployment placement | a *deployment's* own physical binder, additionally deciding **placement** (edge vs. backend, wire format, …) — a genuinely different, deployment-specific decision this crate doesn't model at all | e.g. `control_plane::sketch_algebra::rules::bind_*` (as of this writing; expected to fold into that deployment's cost-model layer rather than stay a separate "bind" concept) | //! @@ -120,5 +120,5 @@ pub use cost_model::{CostModel, DefaultCostModel}; pub use implementation::{implementations_for_with, summary_candidates, Implementation, Matcher}; pub use replacement::{ Replacement, ReplacementStrategy, ReplacementSubDAG, SharedSubtreeStrategy, - SketchFamilyStrategy, TargetSubDAG, + SketchAlgorithmStrategy, TargetSubDAG, }; diff --git a/crates/asap-aware-mapping/src/replacement.rs b/crates/asap-aware-mapping/src/replacement.rs index 66f97939..ef5cb6c9 100644 --- a/crates/asap-aware-mapping/src/replacement.rs +++ b/crates/asap-aware-mapping/src/replacement.rs @@ -33,7 +33,7 @@ //! //! ## Selection: this crate reports every candidate; a caller keeps what it wants //! -//! [`SketchFamilyStrategy::replacements`] builds a `TargetSubDAG` for the +//! [`SketchAlgorithmStrategy::replacements`] builds a `TargetSubDAG` for the //! node being replaced, then enumerates [`implementation::implementations_for_with`]'s //! ranked list, calling [`bind::bind_with_implementation`] once per //! candidate — given an *already-decided* `Implementation`, turn it into a @@ -46,7 +46,7 @@ //! one place inside this crate that still performs that take-first step //! internally, because workload-wide CSE memoization needs one canonical //! decision per shared root to key sharing on (see `bind.rs`'s own module -//! docs). Every other caller goes through `SketchFamilyStrategy::replacements` +//! docs). Every other caller goes through `SketchAlgorithmStrategy::replacements` //! directly and decides for itself. //! //! This means an ordinary single-target bind now sizes and fully constructs @@ -57,7 +57,7 @@ //! //! ## The two strategies, and why these two //! -//! - [`SketchFamilyStrategy`] wraps [`implementation::implementations_for_with`]'s +//! - [`SketchAlgorithmStrategy`] wraps [`implementation::implementations_for_with`]'s //! exhaustive, ranked list directly: for the same bindable-`Aggregate` //! shape this crate binds (single intent, no `HAVING`), every entry //! becomes its own bound candidate. @@ -85,7 +85,7 @@ //! against every candidate plan, deduplicating, iterating to a fixpoint, //! then ranking by a `CostModel` — is a Cascades/Volcano-style search //! engine, tracked as a separate follow-up. Taking the first candidate off -//! [`SketchFamilyStrategy::replacements`] is a single-node stand-in for +//! [`SketchAlgorithmStrategy::replacements`] is a single-node stand-in for //! that, not the real thing: it never compares whole candidate *plans*, //! only one node's own candidates against each other via //! `cost_model.rank_candidates`. @@ -126,7 +126,7 @@ use crate::implementation::{implementations_for_with, Implementation}; /// reference this exact `Rc` — 1 for an ordinary single-use node, 2+ when the /// caller already ran `share_common_subtrees` and found this subtree shared. /// A strategy that only cares about `root`'s own shape (e.g. -/// [`SketchFamilyStrategy`]) can ignore it entirely; [`SharedSubtreeStrategy`] +/// [`SketchAlgorithmStrategy`]) can ignore it entirely; [`SharedSubtreeStrategy`] /// is the one strategy that consults it. Computing a *real* consumer count /// across a whole workload is a traversal this module deliberately does not /// own (see the module docs' "Non-goals") — [`TargetSubDAG::new`] defaults it @@ -197,7 +197,7 @@ pub struct ReplacementSubDAG { /// no restructuring of this trait or any existing strategy required. /// /// `replacements` is only meaningful when `matches` would return `true` for -/// the same target; both [`SketchFamilyStrategy`] and [`SharedSubtreeStrategy`] +/// the same target; both [`SketchAlgorithmStrategy`] and [`SharedSubtreeStrategy`] /// return an empty `Vec` rather than panicking when called on a target they /// don't match, so a caller that skips the `matches` check first still gets a /// safe (merely uninformative) answer instead of a crash. @@ -211,9 +211,9 @@ pub trait ReplacementStrategy { fn replacements(&self, target: &TargetSubDAG<'_>) -> Vec; } -// ── SketchFamilyStrategy ───────────────────────────────────────────────── +// ── SketchAlgorithmStrategy ───────────────────────────────────────────────── -/// A single static instance so [`SketchFamilyStrategy::default_cost_model`] +/// A single static instance so [`SketchAlgorithmStrategy::default_cost_model`] /// can hand out a `&'static dyn CostModel` without heap-allocating one — /// `DefaultCostModel` is a unit struct with no state, so one instance serves /// every caller (same pattern `applicability::SketchApplicabilityRule` uses). @@ -225,14 +225,14 @@ static DEFAULT_COST_MODEL: DefaultCostModel = DefaultCostModel; /// /// Ranked (only to *order the enumeration*, never to drop a candidate) via a /// [`CostModel`] — [`DefaultCostModel`] unless constructed with -/// [`SketchFamilyStrategy::new`] — so a deployment-specific cost model's +/// [`SketchAlgorithmStrategy::new`] — so a deployment-specific cost model's /// other hooks (`size_params`, `realize_extension`, `readout_extension`) are /// still consulted while binding each candidate. -pub struct SketchFamilyStrategy<'a> { +pub struct SketchAlgorithmStrategy<'a> { cost_model: &'a dyn CostModel, } -impl SketchFamilyStrategy<'static> { +impl SketchAlgorithmStrategy<'static> { /// A strategy that ranks/binds via the built-in [`DefaultCostModel`] — /// what a deployment gets with no custom cost model plugged in. pub fn default_cost_model() -> Self { @@ -242,7 +242,7 @@ impl SketchFamilyStrategy<'static> { } } -impl<'a> SketchFamilyStrategy<'a> { +impl<'a> SketchAlgorithmStrategy<'a> { /// A strategy that ranks/binds via `cost_model` instead of the built-in /// static preference order — the same customization point /// [`implementation::implementations_for_with`] already offers. @@ -251,7 +251,7 @@ impl<'a> SketchFamilyStrategy<'a> { } } -impl ReplacementStrategy for SketchFamilyStrategy<'_> { +impl ReplacementStrategy for SketchAlgorithmStrategy<'_> { fn matches(&self, target: &TargetSubDAG<'_>) -> bool { bindable_intent(target.root).is_some() } @@ -284,11 +284,12 @@ impl ReplacementStrategy for SketchFamilyStrategy<'_> { /// [`ReplacementSubDAG::rationale`] text. fn describe_implementation(intent: &AggIntent, implementation: &Implementation) -> String { match implementation { - Implementation::Sketch { kind, .. } => format!( - "{} realizes as a {kind:?} sketch — one of implementation::summary_candidates' \ + Implementation::Sketch(kind) => format!( + "{} realizes as a {:?} sketch — one of implementation::summary_candidates' \ alternatives for this intent \ (asap_aware_mapping::implementation::implementations_for_with)", - describe_intent(intent) + describe_intent(intent), + kind.algorithm() ), Implementation::ExactAggregate { kind, .. } => format!( "{} realizes as an exact {kind:?} accumulator — the only realization \ @@ -398,7 +399,7 @@ impl ReplacementStrategy for SharedSubtreeStrategy { #[cfg(test)] mod tests { use super::*; - use asap_types::post_asap::SketchKind; + use asap_types::post_asap::SketchAlgorithm; use asap_types::pre_asap::agg_intent::{default_cardinality, default_quantile}; use asap_types::pre_asap::query_expr::{Reduction, Source}; use asap_types::pre_asap::schema::{Column, DataType, Schema}; @@ -428,18 +429,18 @@ mod tests { } } - // ── SketchFamilyStrategy ───────────────────────────────────────────── + // ── SketchAlgorithmStrategy ───────────────────────────────────────────── #[test] fn matches_a_bindable_aggregate() { let q = Rc::new(agg(vec![2], default_quantile(0.99), metric_scan(&["job"]))); let target = TargetSubDAG::new(&q); - assert!(SketchFamilyStrategy::default_cost_model().matches(&target)); + assert!(SketchAlgorithmStrategy::default_cost_model().matches(&target)); } #[test] fn does_not_match_a_multi_intent_or_having_aggregate() { - let strategy = SketchFamilyStrategy::default_cost_model(); + let strategy = SketchAlgorithmStrategy::default_cost_model(); let multi = Rc::new(QueryExpr::Aggregate { reduction: Reduction::by(vec![2]), @@ -468,8 +469,8 @@ mod tests { fn does_not_match_a_non_aggregate_node() { let scan = Rc::new(metric_scan(&["job"])); let target = TargetSubDAG::new(&scan); - assert!(!SketchFamilyStrategy::default_cost_model().matches(&target)); - assert!(SketchFamilyStrategy::default_cost_model() + assert!(!SketchAlgorithmStrategy::default_cost_model().matches(&target)); + assert!(SketchAlgorithmStrategy::default_cost_model() .replacements(&target) .is_empty()); } @@ -481,22 +482,22 @@ mod tests { // not just Kll (the CostModel-ranked head implementations_for_with commits to). let q = Rc::new(agg(vec![2], default_quantile(0.99), metric_scan(&["job"]))); let target = TargetSubDAG::new(&q); - let replacements = SketchFamilyStrategy::default_cost_model().replacements(&target); + let replacements = SketchAlgorithmStrategy::default_cost_model().replacements(&target); assert_eq!( replacements.len(), 2, "expected 2 candidates, got {replacements:?}" ); - let kinds: Vec = replacements + let kinds: Vec = replacements .iter() .map(|r| match &r.replacement { - Replacement::Summary(node) => summary_family_kind(node), + Replacement::Summary(node) => summary_family_algorithm(node), Replacement::Rewrite(_) => panic!("expected a Summary replacement"), }) .collect(); - assert!(kinds.contains(&SketchKind::Kll), "{kinds:?}"); - assert!(kinds.contains(&SketchKind::DDSketch), "{kinds:?}"); + assert!(kinds.contains(&SketchAlgorithm::Kll), "{kinds:?}"); + assert!(kinds.contains(&SketchAlgorithm::DDSketch), "{kinds:?}"); assert!( replacements.iter().all(|r| !r.rationale.is_empty()), "every candidate must carry a rationale" @@ -507,17 +508,21 @@ mod tests { fn cardinality_enumerates_all_three_summary_candidates() { let q = Rc::new(agg(vec![2], default_cardinality(), metric_scan(&["job"]))); let target = TargetSubDAG::new(&q); - let replacements = SketchFamilyStrategy::default_cost_model().replacements(&target); - let kinds: Vec = replacements + let replacements = SketchAlgorithmStrategy::default_cost_model().replacements(&target); + let kinds: Vec = replacements .iter() .map(|r| match &r.replacement { - Replacement::Summary(node) => summary_family_kind(node), + Replacement::Summary(node) => summary_family_algorithm(node), Replacement::Rewrite(_) => panic!("expected a Summary replacement"), }) .collect(); assert_eq!( kinds, - vec![SketchKind::Hll, SketchKind::Theta, SketchKind::Kmv], + vec![ + SketchAlgorithm::Hll, + SketchAlgorithm::Theta, + SketchAlgorithm::Kmv + ], "expected every implementation::summary_candidates entry for Cardinality" ); } @@ -533,7 +538,7 @@ mod tests { }; let q = Rc::new(agg(vec![2], intent, metric_scan(&["job"]))); let target = TargetSubDAG::new(&q); - let replacements = SketchFamilyStrategy::default_cost_model().replacements(&target); + let replacements = SketchAlgorithmStrategy::default_cost_model().replacements(&target); assert_eq!(replacements.len(), 1, "{replacements:?}"); assert!(matches!( &replacements[0].replacement, @@ -553,7 +558,7 @@ mod tests { metric_scan(&["job"]), )); let target = TargetSubDAG::new(&q); - let replacements = SketchFamilyStrategy::default_cost_model().replacements(&target); + let replacements = SketchAlgorithmStrategy::default_cost_model().replacements(&target); assert_eq!(replacements.len(), 1, "{replacements:?}"); assert!(matches!( &replacements[0].replacement, @@ -573,10 +578,10 @@ mod tests { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { let mut v = candidates.to_vec(); - if let Some(pos) = v.iter().position(|k| *k == SketchKind::DDSketch) { + if let Some(pos) = v.iter().position(|k| *k == SketchAlgorithm::DDSketch) { let dd = v.remove(pos); v.insert(0, dd); } @@ -589,16 +594,16 @@ mod tests { let q = Rc::new(agg(vec![2], default_quantile(0.99), metric_scan(&["job"]))); let target = TargetSubDAG::new(&q); let custom = PreferDDSketch; - let replacements = SketchFamilyStrategy::new(&custom).replacements(&target); - let kinds: Vec = replacements + let replacements = SketchAlgorithmStrategy::new(&custom).replacements(&target); + let kinds: Vec = replacements .iter() .map(|r| match &r.replacement { - Replacement::Summary(node) => summary_family_kind(node), + Replacement::Summary(node) => summary_family_algorithm(node), Replacement::Rewrite(_) => panic!("expected a Summary replacement"), }) .collect(); - assert!(kinds.contains(&SketchKind::Kll)); - assert!(kinds.contains(&SketchKind::DDSketch)); + assert!(kinds.contains(&SketchAlgorithm::Kll)); + assert!(kinds.contains(&SketchAlgorithm::DDSketch)); assert_eq!(kinds.len(), 2); } @@ -617,21 +622,21 @@ mod tests { let inner = agg(vec![2], default_quantile(0.5), metric_scan(&["job"])); let outer = Rc::new(agg(vec![], default_quantile(0.99), inner)); let target = TargetSubDAG::new(&outer); - let replacements = SketchFamilyStrategy::default_cost_model().replacements(&target); + let replacements = SketchAlgorithmStrategy::default_cost_model().replacements(&target); let ddsketch = replacements .iter() .find(|r| { matches!(&r.replacement, Replacement::Summary(node) - if summary_family_kind(node) == SketchKind::DDSketch) + if summary_family_algorithm(node) == SketchAlgorithm::DDSketch) }) .expect("the outer target's DDSketch candidate must be present"); let Replacement::Summary(node) = &ddsketch.replacement else { unreachable!("filtered on Replacement::Summary above"); }; assert_eq!( - summary_family_kind(node), - SketchKind::DDSketch, + summary_family_algorithm(node), + SketchAlgorithm::DDSketch, "the outer (target) node must be the DDSketch candidate" ); @@ -644,22 +649,23 @@ mod tests { panic!("expected SummaryAgg, got {:?}", summary_input.expr); }; assert_eq!( - summary_family_kind(child), - SketchKind::Kll, + summary_family_algorithm(child), + SketchAlgorithm::Kll, "the nested inner aggregate must still get the cost-model-ranked \ default (Kll), not inherit the outer target's DDSketch candidate" ); } - /// The `SummaryFamilyType`'s `SketchKind`, from the top `SummaryAgg` - /// reachable under a (possibly `SummaryEstimate`-wrapped) bound root. - fn summary_family_kind(node: &SummaryNode) -> SketchKind { + /// The `SummaryFamilyType`'s committed `SketchAlgorithm`, from the top + /// `SummaryAgg` reachable under a (possibly `SummaryEstimate`-wrapped) + /// bound root. + fn summary_family_algorithm(node: &SummaryNode) -> SketchAlgorithm { match &node.expr { asap_types::post_asap::SummaryExpr::SummaryEstimate { summary_input, .. } => { - summary_family_kind(summary_input) + summary_family_algorithm(summary_input) } asap_types::post_asap::SummaryExpr::SummaryAgg { family, .. } => match family { - asap_types::post_asap::SummaryFamilyType::Sketch(kind, _) => kind.clone(), + asap_types::post_asap::SummaryFamilyType::Sketch(kind) => kind.algorithm().clone(), other => panic!("expected a Sketch family, got {other:?}"), }, other => panic!("expected SummaryAgg/SummaryEstimate, got {other:?}"), diff --git a/crates/devtools/src/bin/show_post_asap_ir.rs b/crates/devtools/src/bin/show_post_asap_ir.rs index ec50b8ca..afed969c 100644 --- a/crates/devtools/src/bin/show_post_asap_ir.rs +++ b/crates/devtools/src/bin/show_post_asap_ir.rs @@ -22,7 +22,7 @@ use asap_aware_mapping::bind::logical; use asap_aware_mapping::{ - Replacement, ReplacementStrategy, ReplacementSubDAG, SketchFamilyStrategy, TargetSubDAG, + Replacement, ReplacementStrategy, ReplacementSubDAG, SketchAlgorithmStrategy, TargetSubDAG, }; use asap_devtools::{lower_promql, lower_sql, SqlCatalog}; use asap_types::pre_asap::query_expr::QueryExpr; @@ -34,7 +34,7 @@ use std::rc::Rc; const ACCURACY: AccuracyTarget = AccuracyTarget::Epsilon(0.01); /// `asap-aware-mapping` has no "bind me one tree" public API any more — -/// `SketchFamilyStrategy::replacements` always returns every candidate, and +/// `SketchAlgorithmStrategy::replacements` always returns every candidate, and /// a caller decides what to keep. This debug tool just wants one /// representative binding per query, so it takes the first /// (`cost_model`-preferred) candidate the same way a production caller @@ -42,7 +42,7 @@ const ACCURACY: AccuracyTarget = AccuracyTarget::Epsilon(0.01); fn bind(expr: &QueryExpr) -> Result, String> { let root = Rc::new(expr.clone()); let target = TargetSubDAG::new(&root); - match SketchFamilyStrategy::default_cost_model() + match SketchAlgorithmStrategy::default_cost_model() .replacements(&target) .into_iter() .next() diff --git a/crates/frontend-promql/tests/observability/promql_corpus.rs b/crates/frontend-promql/tests/observability/promql_corpus.rs index babde60a..461eb1b4 100644 --- a/crates/frontend-promql/tests/observability/promql_corpus.rs +++ b/crates/frontend-promql/tests/observability/promql_corpus.rs @@ -17,7 +17,7 @@ use std::rc::Rc; use asap_aware_mapping::bind::{logical, ImplementError}; use asap_aware_mapping::{ - Replacement, ReplacementStrategy, ReplacementSubDAG, SketchFamilyStrategy, TargetSubDAG, + Replacement, ReplacementStrategy, ReplacementSubDAG, SketchAlgorithmStrategy, TargetSubDAG, }; use asap_frontend_promql::{lower_promql, PromqlError as LoweringError}; use asap_types::post_asap::{SummaryExpr, SummaryNode}; @@ -25,7 +25,7 @@ use asap_types::pre_asap::query_expr::QueryExpr; use asap_types::types::AccuracyTarget; /// This crate has no "bind me one tree" public API any more — -/// `SketchFamilyStrategy::replacements` always returns every candidate, and +/// `SketchAlgorithmStrategy::replacements` always returns every candidate, and /// a caller decides what to keep. This test-only helper reproduces the /// take-the-first-(`cost_model`-preferred)-candidate pattern so [`bind_tally`] /// gets one representative `Result` per query, matching what a totality @@ -33,7 +33,7 @@ use asap_types::types::AccuracyTarget; fn bind(expr: &QueryExpr) -> Result, ImplementError> { let root = Rc::new(expr.clone()); let target = TargetSubDAG::new(&root); - match SketchFamilyStrategy::default_cost_model() + match SketchAlgorithmStrategy::default_cost_model() .replacements(&target) .into_iter() .next() diff --git a/crates/integration-tests/tests/l4_binding.rs b/crates/integration-tests/tests/l4_binding.rs index 470feb8b..6a72a77b 100644 --- a/crates/integration-tests/tests/l4_binding.rs +++ b/crates/integration-tests/tests/l4_binding.rs @@ -2,7 +2,7 @@ //! //! Drives the full pipeline — PromQL text → pre-ASAP `QueryExpr` //! (`lower_promql`) → post-ASAP `SummaryExpr` DAG (via -//! `SketchFamilyStrategy::replacements`, see [`bind`] below) — and pins the +//! `SketchAlgorithmStrategy::replacements`, see [`bind`] below) — and pins the //! summary-bound shape node by node, including the family `(Kind, Params)` //! committed on each edge's schema. This is the design doc's §"L4 — sketch //! algebra" worked example, running for real. @@ -11,12 +11,12 @@ use std::rc::Rc; use asap_aware_mapping::bind::{logical, ImplementError}; use asap_aware_mapping::{ - Replacement, ReplacementStrategy, ReplacementSubDAG, SketchFamilyStrategy, TargetSubDAG, + Replacement, ReplacementStrategy, ReplacementSubDAG, SketchAlgorithmStrategy, TargetSubDAG, }; use asap_frontend_promql::lower_promql; use asap_types::post_asap::{ - ExactKind, ExactParams, SketchKind, SketchParams, SketchQuery, SummaryExpr, SummaryFamilyType, - SummaryNode, SummarySchema, + ExactKind, ExactParams, SketchAlgorithm, SketchKind, SketchParams, SketchQuery, SummaryExpr, + SummaryFamilyType, SummaryNode, SummarySchema, }; use asap_types::pre_asap::expr_ir::ColumnRef; use asap_types::pre_asap::query_expr::{QueryExpr, Reduction}; @@ -24,14 +24,14 @@ use asap_types::pre_asap::schema::DataType; use asap_types::types::AccuracyTarget; /// This crate has no "bind me one tree" public API any more — -/// `SketchFamilyStrategy::replacements` always returns every candidate, and +/// `SketchAlgorithmStrategy::replacements` always returns every candidate, and /// a caller decides what to keep. This test-only helper reproduces the /// take-the-first-(`cost_model`-preferred)-candidate pattern so the /// single-answer pins below don't all repeat it by hand. fn bind(expr: &QueryExpr) -> Result, ImplementError> { let root = Rc::new(expr.clone()); let target = TargetSubDAG::new(&root); - match SketchFamilyStrategy::default_cost_model() + match SketchAlgorithmStrategy::default_cost_model() .replacements(&target) .into_iter() .next() @@ -105,7 +105,10 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { }; assert_eq!( family, - &SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 }) + &SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 } + )) ); assert_eq!(col, &ColumnRef::SampleValue); assert_eq!( @@ -115,7 +118,10 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { ); assert_eq!( dtype(&summary_input.schema, "quantile_0_99"), - &SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 }) + &SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 } + )) ); // The rate: exact counter-reset-aware accumulator, per-series (labels diff --git a/crates/integration-tests/tests/l4_binding_sql.rs b/crates/integration-tests/tests/l4_binding_sql.rs index 6cfa97ca..56e5f84e 100644 --- a/crates/integration-tests/tests/l4_binding_sql.rs +++ b/crates/integration-tests/tests/l4_binding_sql.rs @@ -1,7 +1,7 @@ //! End-to-end SQL query-string → post-ASAP IR pin (issue #191). //! //! The SQL counterpart of `l4_binding.rs`: drives SQL text — `lower_sql` -//! (text → pre-ASAP `QueryExpr`) → `SketchFamilyStrategy::replacements` +//! (text → pre-ASAP `QueryExpr`) → `SketchAlgorithmStrategy::replacements` //! (pre-ASAP → post-ASAP `SummaryExpr`, see [`bind`] below) — and pins the //! resulting sketch-vs-exact-accumulator binding shape node by node, the way //! `l4_binding.rs` does for PromQL. @@ -32,12 +32,12 @@ use std::rc::Rc; use asap_aware_mapping::bind::{logical, ImplementError}; use asap_aware_mapping::{ - Replacement, ReplacementStrategy, ReplacementSubDAG, SketchFamilyStrategy, TargetSubDAG, + Replacement, ReplacementStrategy, ReplacementSubDAG, SketchAlgorithmStrategy, TargetSubDAG, }; use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_types::post_asap::{ - ExactKind, ExactParams, SketchKind, SketchParams, SketchQuery, SummaryExpr, SummaryFamilyType, - SummaryNode, SummarySchema, + ExactKind, ExactParams, SketchAlgorithm, SketchKind, SketchParams, SketchQuery, SummaryExpr, + SummaryFamilyType, SummaryNode, SummarySchema, }; use asap_types::pre_asap::expr_ir::ColumnRef; use asap_types::pre_asap::query_expr::{QueryExpr, Reduction}; @@ -45,14 +45,14 @@ use asap_types::pre_asap::schema::{Column, DataType, Schema}; use asap_types::types::AccuracyTarget; /// This crate has no "bind me one tree" public API any more — -/// `SketchFamilyStrategy::replacements` always returns every candidate, and +/// `SketchAlgorithmStrategy::replacements` always returns every candidate, and /// a caller decides what to keep. This test-only helper reproduces the /// take-the-first-(`cost_model`-preferred)-candidate pattern so the /// single-answer pins below don't all repeat it by hand. fn bind(expr: &QueryExpr) -> Result, ImplementError> { let root = Rc::new(expr.clone()); let target = TargetSubDAG::new(&root); - match SketchFamilyStrategy::default_cost_model() + match SketchAlgorithmStrategy::default_cost_model() .replacements(&target) .into_iter() .next() @@ -190,7 +190,10 @@ async fn sql_quantile_binds_kll_sketch_over_named_column() { }; assert_eq!( family, - &SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 }) + &SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 } + )) ); assert_eq!( col, @@ -207,7 +210,10 @@ async fn sql_quantile_binds_kll_sketch_over_named_column() { ); assert_eq!( summary_input.schema.fields[0].dtype, - SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 }) + SummaryFamilyType::Sketch(SketchKind::Quantile( + SketchAlgorithm::Kll, + SketchParams::Kll { k: 200 } + )) ); let SummaryExpr::Logical(logical_leaf) = &child.expr else { @@ -265,7 +271,10 @@ async fn sql_count_distinct_binds_hll_sketch_over_named_column() { }; assert_eq!( family, - &SummaryFamilyType::Sketch(SketchKind::Hll, SketchParams::Hll { precision: 14 }) + &SummaryFamilyType::Sketch(SketchKind::Cardinality( + SketchAlgorithm::Hll, + SketchParams::Hll { precision: 14 } + )) ); assert_eq!( col, diff --git a/crates/types/src/post_asap/mod.rs b/crates/types/src/post_asap/mod.rs index d449dc96..6ca27740 100644 --- a/crates/types/src/post_asap/mod.rs +++ b/crates/types/src/post_asap/mod.rs @@ -3,14 +3,19 @@ //! //! Where [`crate::pre_asap`] carries *intent* only ("compute a //! quantile to ε accuracy"), this module is the summary-bound IR: the -//! summary family, kind, and parameters are committed (one `(Kind, Params)` -//! pair per family — [`sketch::ExactKind`]/[`sketch::ExactParams`], -//! [`sketch::SketchKind`]/[`sketch::SketchParams`], +//! summary family, kind/algorithm, and parameters are committed (one +//! `(Kind, Params)` pair per family — [`sketch::ExactKind`]/[`sketch::ExactParams`], //! [`sketch::SamplingKind`]/[`sketch::SamplingParams`], //! [`sketch::WaveletKind`]/[`sketch::WaveletParams`], //! [`sketch::StatModelKind`]/[`sketch::StatModelParams`]), and //! [`expr::SummaryNode`] / [`expr::SummaryExpr`] describe the summary -//! computation. +//! computation. The `Sketch` family is the one exception to that +//! one-pair-per-family shape: it nests a third level, [`sketch::SketchKind`] +//! (quantile/cardinality/frequency/top-k), which itself carries the +//! committed [`sketch::SketchAlgorithm`] and [`sketch::SketchParams`] — +//! `SummaryFamilyType::Sketch(SketchKind)`, not a flat `(kind, params)` pair +//! — because `Sketch` is the one family with more than one algorithm per +//! purpose today; no other family needs that extra level yet. pub mod expr; pub mod query_time; @@ -24,6 +29,6 @@ pub use query_time::{ }; pub use schema::{SummaryFamilyType, SummaryField, SummarySchema}; pub use sketch::{ - ExactKind, ExactParams, SamplingKind, SamplingParams, SketchKind, SketchParams, SketchQuery, - StatModelKind, StatModelParams, WaveletKind, WaveletParams, + ExactKind, ExactParams, SamplingKind, SamplingParams, SketchAlgorithm, SketchKind, + SketchParams, SketchQuery, StatModelKind, StatModelParams, WaveletKind, WaveletParams, }; diff --git a/crates/types/src/post_asap/schema.rs b/crates/types/src/post_asap/schema.rs index 20599137..0f51b42b 100644 --- a/crates/types/src/post_asap/schema.rs +++ b/crates/types/src/post_asap/schema.rs @@ -1,5 +1,5 @@ use super::sketch::{ - ExactKind, ExactParams, SamplingKind, SamplingParams, SketchKind, SketchParams, StatModelKind, + ExactKind, ExactParams, SamplingKind, SamplingParams, SketchKind, StatModelKind, StatModelParams, WaveletKind, WaveletParams, }; use crate::pre_asap::DataType; @@ -27,8 +27,11 @@ pub enum SummaryFamilyType { /// `Increase`) — the partial state *is* the value; no readout needed. ExactAggregate(ExactKind, ExactParams), /// Approximate sketch state (KLL/CMS/HLL/…), read out via a - /// `SummaryEstimate`. - Sketch(SketchKind, SketchParams), + /// `SummaryEstimate`. A [`SketchKind`] already carries the concrete + /// algorithm and params committed to, not just its category — a bound + /// node needs to know it's specifically KLL, not merely "some quantile + /// sketch". + Sketch(SketchKind), /// Sampling-based summary state (a retained row subset). Sample(SamplingKind, SamplingParams), /// Wavelet-transform summary state (a coefficient vector). diff --git a/crates/types/src/post_asap/sketch.rs b/crates/types/src/post_asap/sketch.rs index 88d358bc..b0458780 100644 --- a/crates/types/src/post_asap/sketch.rs +++ b/crates/types/src/post_asap/sketch.rs @@ -34,10 +34,12 @@ pub enum ExactParams { // ── Approximate sketches ───────────────────────────────────────────────────── -/// An approximate, mergeable sketch family — bounded error, sized by its -/// [`SketchParams`]. +/// A specific sketch algorithm — bounded error, sized by its +/// [`SketchParams`]. Each algorithm belongs to exactly one [`SketchKind`] +/// category (e.g. `Kll` and `DDSketch` both realize `SketchKind::Quantile`); +/// [`SketchKind::new`] is where that classification is made. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum SketchKind { +pub enum SketchAlgorithm { /// KLL quantile sketch (mergeable, ε-accurate rank queries). Kll, /// Count-Min Sketch (mergeable, (ε,δ)-accurate frequency queries). @@ -61,8 +63,8 @@ pub enum SketchKind { CountSketchWithHeap, } -/// Concrete, catalog-validated parameters for a specific [`SketchKind`] -/// instance. The variant must correspond to the associated `SketchKind`; +/// Concrete, catalog-validated parameters for a specific [`SketchAlgorithm`] +/// instance. The variant must correspond to the associated `SketchAlgorithm`; /// mismatches are caught at post-ASAP bind time, before any later, /// deployment-specific stage ever sees the plan. #[derive(Debug, Clone, PartialEq)] @@ -102,6 +104,76 @@ pub enum SketchParams { }, } +/// A committed sketch choice: which *category* of query shape it answers — +/// quantile-style, cardinality-style, frequency-style, or heavy-hitter/ +/// top-k-style estimation — together with the concrete [`SketchAlgorithm`] +/// and [`SketchParams`] realizing it. Sits between +/// [`SummaryFamilyType::Sketch`](super::schema::SummaryFamilyType::Sketch) +/// (the `Sketch` family as a whole, sibling to `Sample`/`Wavelet`/ +/// `StatModel`) and the bare algorithm: `Kll` vs. `DDSketch` is a choice +/// *within* `Quantile`, not a choice *of* `SketchKind` — every `Quantile` +/// value already carries which of the two (and its params) was picked. +/// +/// [`SketchKind::new`] is the one place `(SketchAlgorithm, SketchParams)` +/// pairs get classified into a category; construct through it rather than +/// naming a variant directly, so a new algorithm can't drift out of sync +/// with its category. See `implementation::summary_candidates` for the +/// `AggIntent -> [SketchAlgorithm]` candidate list this ultimately groups. +#[derive(Debug, Clone, PartialEq)] +pub enum SketchKind { + /// Approximate rank/percentile queries (e.g. p99 latency). + Quantile(SketchAlgorithm, SketchParams), + /// Approximate distinct-element counting. + Cardinality(SketchAlgorithm, SketchParams), + /// Approximate point/frequency counting (e.g. per-key event counts). + Frequency(SketchAlgorithm, SketchParams), + /// Approximate heavy-hitter / top-k queries. + TopK(SketchAlgorithm, SketchParams), +} + +impl SketchKind { + /// Classify `(algorithm, params)` into its `SketchKind` category. The + /// one place that mapping is made — every other piece of this crate + /// that needs to know an algorithm's category goes through this rather + /// than re-deriving it. + pub fn new(algorithm: SketchAlgorithm, params: SketchParams) -> Self { + match algorithm { + SketchAlgorithm::Kll | SketchAlgorithm::DDSketch => { + SketchKind::Quantile(algorithm, params) + } + SketchAlgorithm::Hll | SketchAlgorithm::Theta | SketchAlgorithm::Kmv => { + SketchKind::Cardinality(algorithm, params) + } + SketchAlgorithm::Cms | SketchAlgorithm::CountSketch => { + SketchKind::Frequency(algorithm, params) + } + SketchAlgorithm::CmsWithHeap | SketchAlgorithm::CountSketchWithHeap => { + SketchKind::TopK(algorithm, params) + } + } + } + + /// The algorithm this kind committed to, regardless of category. + pub fn algorithm(&self) -> &SketchAlgorithm { + match self { + SketchKind::Quantile(a, _) + | SketchKind::Cardinality(a, _) + | SketchKind::Frequency(a, _) + | SketchKind::TopK(a, _) => a, + } + } + + /// The parameters this kind committed to, regardless of category. + pub fn params(&self) -> &SketchParams { + match self { + SketchKind::Quantile(_, p) + | SketchKind::Cardinality(_, p) + | SketchKind::Frequency(_, p) + | SketchKind::TopK(_, p) => p, + } + } +} + // ── Sampling summaries ─────────────────────────────────────────────────────── /// A sampling-based summary family — retains an actual (weighted) subset of diff --git a/docs/developer_docs/ASAP-aware-mapping-developer-guide.md b/docs/developer_docs/ASAP-aware-mapping-developer-guide.md index a8e870d1..bc2193bd 100644 --- a/docs/developer_docs/ASAP-aware-mapping-developer-guide.md +++ b/docs/developer_docs/ASAP-aware-mapping-developer-guide.md @@ -12,7 +12,7 @@ It is written for developers who want to: The focus here is the **current code interfaces and their contracts**. For the higher-level motivation and future search design, see the separate design document, [`docs/design_docs/asap_aware_mapping.md`](../design_docs/asap_aware_mapping.md). -Code samples named `My*` or `Prefer*` (`MyStrategy`, `MyCostModel`, `PreferDDSketch`, …) below are illustrative sketches of a pattern, not code that ships in this crate. Samples that name a real type (`SketchFamilyStrategy`, `SharedSubtreeStrategy`, `bind_with_implementation`, …) are copied verbatim from `replacement.rs`/`bind.rs`/`cost_model.rs`. +Code samples named `My*` or `Prefer*` (`MyStrategy`, `MyCostModel`, `PreferDDSketch`, …) below are illustrative sketches of a pattern, not code that ships in this crate. Samples that name a real type (`SketchAlgorithmStrategy`, `SharedSubtreeStrategy`, `bind_with_implementation`, …) are copied verbatim from `replacement.rs`/`bind.rs`/`cost_model.rs`. --- @@ -21,7 +21,7 @@ Code samples named `My*` or `Prefer*` (`MyStrategy`, `MyCostModel`, `PreferDDSke Two words come up constantly below and are worth pinning down before anything else, since neither is self-explanatory from context alone: - **`implementation`** (the module `implementation.rs`) — every valid way one `AggIntent` could be realized: as an approximate sketch, an exact mergeable accumulator, or a pass-through (no summary at all). `implementation::implementations_for_with` computes this **one node at a time**, exhaustive and ranked; it doesn't walk anything, and it doesn't pick a favorite — picking is left to its callers. -- **`bind` / "binding"** — this crate's own `bind.rs`. It has no "bind me one tree" entry point of its own: `replacement::SketchFamilyStrategy::replacements()` is the only public way to get bound output for a target, and it always returns *every* candidate; a caller that wants a single executable answer takes the first entry itself (see §3). What `bind.rs` provides is the shared low-level primitive, `bind_with_implementation`, that turns one already-decided candidate into a real `SummaryNode`, plus `implement_workload`/`implement_workload_with`, which drive that same take-the-head selection over a whole workload's roots (see §3's closing note on why those two still keep it internally). This is what "the binding path" means everywhere in this guide: the code that commits to one `Implementation` per node because something has to actually execute. (The word "bind" means other things elsewhere in this crate's downstream consumers — see `lib.rs`'s own "Terminology" section if you need the full picture — but within this guide, "bind"/"binding" always means this.) +- **`bind` / "binding"** — this crate's own `bind.rs`. It has no "bind me one tree" entry point of its own: `replacement::SketchAlgorithmStrategy::replacements()` is the only public way to get bound output for a target, and it always returns *every* candidate; a caller that wants a single executable answer takes the first entry itself (see §3). What `bind.rs` provides is the shared low-level primitive, `bind_with_implementation`, that turns one already-decided candidate into a real `SummaryNode`, plus `implement_workload`/`implement_workload_with`, which drive that same take-the-head selection over a whole workload's roots (see §3's closing note on why those two still keep it internally). This is what "the binding path" means everywhere in this guide: the code that commits to one `Implementation` per node because something has to actually execute. (The word "bind" means other things elsewhere in this crate's downstream consumers — see `lib.rs`'s own "Terminology" section if you need the full picture — but within this guide, "bind"/"binding" always means this.) So: `implementation` enumerates **every** way one node could become something; `ReplacementStrategy` (this guide's main subject) wraps that list into one `ReplacementSubDAG` per candidate, keeping all of them; a caller that wants one answer takes the first entry itself — see §3 for exactly how these connect. @@ -176,13 +176,13 @@ The two methods have intentionally different responsibilities. - **`rank_candidates`** — order a set of sketch candidates for one `AggIntent`, best choice first. **No default** — this is the one hook every `CostModel` must implement; candidate selection needs a real answer from somewhere. ```rust - fn rank_candidates(&self, intent: &AggIntent, candidates: &[SketchKind]) -> Vec; + fn rank_candidates(&self, intent: &AggIntent, candidates: &[SketchAlgorithm]) -> Vec; ``` -- **`size_params`** — pick concrete parameters (e.g. sketch capacity) for one already-chosen `SketchKind`, given an accuracy target `(eps, delta)`. This is a separate hook from `rank_candidates` specifically so a deployment can override *just* sizing (e.g. an empirically-tuned table, or discrete capacity rungs a downstream catalog requires) without also forking candidate selection — same "one extension point per decision" shape as everything else in this trait. Default: `implementation::default_size_params`, this crate's built-in per-family sizing formulas. +- **`size_params`** — pick concrete parameters (e.g. sketch capacity) for one already-chosen `SketchAlgorithm`, given an accuracy target `(eps, delta)`. This is a separate hook from `rank_candidates` specifically so a deployment can override *just* sizing (e.g. an empirically-tuned table, or discrete capacity rungs a downstream catalog requires) without also forking candidate selection — same "one extension point per decision" shape as everything else in this trait. Default: `implementation::default_size_params`, this crate's built-in per-family sizing formulas. ```rust - fn size_params(&self, kind: SketchKind, intent: &AggIntent, eps: f64, delta: f64) -> SketchParams; + fn size_params(&self, kind: SketchAlgorithm, intent: &AggIntent, eps: f64, delta: f64) -> SketchParams; ``` - **`realize_extension`** — decide what post-ASAP `Implementation` a deployment-defined `AggIntent::Extension` maps to (a shape core has no built-in opinion on). Default: `Implementation::PassThrough`. @@ -192,7 +192,7 @@ The two methods have intentionally different responsibilities. ```rust fn realize_extension(&self, ext_kind: &str, _payload: &serde_json::Value) -> Implementation { if ext_kind == "frequency" { - Implementation::Sketch { kind: SketchKind::CountSketch, params: /* ... */ } + Implementation::Sketch(SketchKind::Frequency(SketchAlgorithm::CountSketch, /* params */)) } else { Implementation::PassThrough // fall back to the default for anything else } @@ -235,6 +235,35 @@ A custom cost model does not necessarily need to override every hook. The curren --- +### Family, kind, and algorithm + +Three levels sit below "summary" in this crate's type vocabulary, and `Sketch` is the only family with all three: + +| Level | Type | Example | +| --- | --- | --- | +| **family** | `SummaryFamilyType` | `Sketch`, `Sample`, `Wavelet`, `StatModel`, `ExactAggregate` | +| **kind** | `SketchKind` (only inside `Sketch`) | `Quantile`, `Cardinality`, `Frequency`, `TopK` | +| **algorithm** | `SketchAlgorithm` (nested inside a `SketchKind`) | `Kll` / `DDSketch` (both `Quantile`); `Hll` / `Theta` / `Kmv` (all `Cardinality`) | + +A `SketchKind` isn't just a category tag — every value already carries the committed algorithm and its params: + +```rust +pub enum SketchKind { + Quantile(SketchAlgorithm, SketchParams), + Cardinality(SketchAlgorithm, SketchParams), + Frequency(SketchAlgorithm, SketchParams), + TopK(SketchAlgorithm, SketchParams), +} +``` + +`SketchKind::new(algorithm, params)` is the one place an `(algorithm, params)` pair gets classified into its category — construct through it rather than naming a variant directly, so a new algorithm can't drift out of sync with its category. `.algorithm()`/`.params()` pull the committed pair back out regardless of which category variant it's in. + +Where this matters in practice: `CostModel::rank_candidates`/`size_params`, `implementation::implementations_for_with`, and `SketchAlgorithmStrategy`'s candidate enumeration all operate one level down, at **algorithm** — `summary_candidates(intent)` returns a list of `SketchAlgorithm`s (`[Kll, DDSketch]` for a `Quantile` intent), never a bare `SketchKind` with nothing chosen underneath it. `SketchKind` only shows up once an algorithm has actually been picked and sized — on `Implementation::Sketch(SketchKind)` and `SummaryFamilyType::Sketch(SketchKind)`, both single-field wrapping the already-committed kind. + +No other family needs this extra level today — `Sample`/`Wavelet`/`StatModel` are each a flat `(Kind, Params)` pair, same shape `Sketch` used to be before this split. `Sketch` grew a third level because it's the one family with more than one algorithm per purpose (KLL vs. DDSketch both answer `Quantile`). + +--- + ## 3. How the current pieces fit together There is one place this crate decides what an `AggIntent` may become — @@ -246,7 +275,7 @@ only the replacement-strategy path: - **`implementation::implementations_for_with(intent, cost_model)`** enumerates every valid `Implementation` for `intent`, exhaustive and ranked (most-preferred first via `cost_model`). -- **`replacement::SketchFamilyStrategy::replacements()`** (§2) wraps that list +- **`replacement::SketchAlgorithmStrategy::replacements()`** (§2) wraps that list directly: every entry becomes its own bound `ReplacementSubDAG`, none discarded. This is the *only* public way to get bound output for a target — there is no second, single-answer entry point sitting behind it. @@ -258,22 +287,22 @@ only the replacement-strategy path: This is exactly the "alternatives"/"candidates" language in [the design doc](../design_docs/asap_aware_mapping.md): a `ReplacementSubDAG` **is** one candidate; a `TargetSubDAG` with its full `replacements()` list **is** the set of alternatives for one spot in the plan. -**The tradeoff, stated plainly:** because a single-target bind now goes through the same enumeration `SketchFamilyStrategy` uses, it sizes and fully constructs *every* sketch candidate at every sketch-capable node — not just the one a caller keeps — before that caller selects the head. That's strictly more work per bind than a version that only ever computed the preferred candidate, in exchange for there being exactly one place in this crate that decides what an `AggIntent` may become and exactly one place bound output comes from. Recursion into a node's child goes back through the same enumerate-then-select step fresh, so choosing (or forcing) a candidate for one target never leaks into that target's own nested aggregates. +**The tradeoff, stated plainly:** because a single-target bind now goes through the same enumeration `SketchAlgorithmStrategy` uses, it sizes and fully constructs *every* sketch candidate at every sketch-capable node — not just the one a caller keeps — before that caller selects the head. That's strictly more work per bind than a version that only ever computed the preferred candidate, in exchange for there being exactly one place in this crate that decides what an `AggIntent` may become and exactly one place bound output comes from. Recursion into a node's child goes back through the same enumerate-then-select step fresh, so choosing (or forcing) a candidate for one target never leaks into that target's own nested aggregates. What this is *not*: the design doc's future Cascades/Volcano-style search engine — generate candidates via `ReplacementStrategy` across a *whole plan*, evaluate/select via `CostModel` across whole candidate plans — is a separate, not-yet-built piece of work. What exists today is a **single-node** stand-in for that selection (`cost_model.rank_candidates`, applied one node at a time), not the real thing. -**One exception:** `bind::implement_workload`/`implement_workload_with` still keep the take-the-head step internally, because workload-wide CSE sharing memoizes on `Rc` pointer identity — two workload roots that collapsed onto the same `Rc` must resolve to the *same* canonical decision to be shareable at all, so there's no meaningful "N candidates" answer to memoize against. That's the one place inside this crate a single-answer selection still lives; every other caller goes through `SketchFamilyStrategy::replacements()` directly. +**One exception:** `bind::implement_workload`/`implement_workload_with` still keep the take-the-head step internally, because workload-wide CSE sharing memoizes on `Rc` pointer identity — two workload roots that collapsed onto the same `Rc` must resolve to the *same* canonical decision to be shareable at all, so there's no meaningful "N candidates" answer to memoize against. That's the one place inside this crate a single-answer selection still lives; every other caller goes through `SketchAlgorithmStrategy::replacements()` directly. ### The shared low-level primitive: `bind_with_implementation` -Underneath `SketchFamilyStrategy::replacements()` sits one more function, `bind::bind_with_implementation(expr, implementation, cost_model)` — *given* an already-decided `Implementation` for `expr`'s top intent, bind it into a `SummaryNode` (or fall back to a logical passthrough). It doesn't re-decide how a chosen `Implementation` becomes a `SummaryNode`; `replacements()` just calls it once per candidate: +Underneath `SketchAlgorithmStrategy::replacements()` sits one more function, `bind::bind_with_implementation(expr, implementation, cost_model)` — *given* an already-decided `Implementation` for `expr`'s top intent, bind it into a `SummaryNode` (or fall back to a logical passthrough). It doesn't re-decide how a chosen `Implementation` becomes a `SummaryNode`; `replacements()` just calls it once per candidate: ```text implementations_for_with(intent, cost_model) every valid Implementation, ranked | v - SketchFamilyStrategy::replacements() + SketchAlgorithmStrategy::replacements() keeps every candidate | v @@ -309,7 +338,7 @@ The important rule is: > Strategies should reuse existing decision and binding logic where possible instead of reimplementing it. -`SketchFamilyStrategy` follows this literally: it doesn't reimplement any part of binding — it hands `implementations_for_with`'s own list straight to `bind_with_implementation`, once per candidate. +`SketchAlgorithmStrategy` follows this literally: it doesn't reimplement any part of binding — it hands `implementations_for_with`'s own list straight to `bind_with_implementation`, once per candidate. --- @@ -352,7 +381,7 @@ There are four decisions to make. `matches` should contain the minimum structural and semantic checks needed to determine whether the strategy applies. -For example, `SketchFamilyStrategy` only matches the aggregate shape that the existing binder can actually bind: +For example, `SketchAlgorithmStrategy` only matches the aggregate shape that the existing binder can actually bind: - the node is an `Aggregate`, - it has one aggregation intent, @@ -499,7 +528,7 @@ If another module already knows how to determine whether something is legal or h Do not create a second implementation of the same semantics inside the strategy. -The existing `SketchFamilyStrategy` is the model to follow: it reuses `implementation.rs`'s existing candidate list and the existing binder. +The existing `SketchAlgorithmStrategy` is the model to follow: it reuses `implementation.rs`'s existing candidate list and the existing binder. --- @@ -523,22 +552,22 @@ If your transformation requires context not currently represented in `TargetSubD --- -## 6. Example: current `SketchFamilyStrategy` +## 6. Example: current `SketchAlgorithmStrategy` -`SketchFamilyStrategy` is the reference implementation for a strategy that produces bound summaries. +`SketchAlgorithmStrategy` is the reference implementation for a strategy that produces bound summaries. Construction: ```rust let strategy = - SketchFamilyStrategy::default_cost_model(); + SketchAlgorithmStrategy::default_cost_model(); ``` or with a custom cost model: ```rust let model = MyCostModel; // illustrative -let strategy = SketchFamilyStrategy::new(&model); +let strategy = SketchAlgorithmStrategy::new(&model); ``` The strategy matches bindable aggregate nodes. @@ -570,7 +599,7 @@ For an approximate quantile, the current candidate list includes both KLL and DD ### How each candidate actually gets bound: `bind_with_implementation` -`SketchFamilyStrategy`'s whole `replacements()` body is one loop: +`SketchAlgorithmStrategy`'s whole `replacements()` body is one loop: ```rust fn replacements(&self, target: &TargetSubDAG<'_>) -> Vec { @@ -664,13 +693,13 @@ impl CostModel for PreferDDSketch { fn rank_candidates( &self, _intent: &AggIntent, - candidates: &[SketchKind], - ) -> Vec { + candidates: &[SketchAlgorithm], + ) -> Vec { let mut ranked = candidates.to_vec(); if let Some(pos) = ranked.iter().position( - |k| *k == SketchKind::DDSketch + |k| *k == SketchAlgorithm::DDSketch ) { let dd = ranked.remove(pos); @@ -688,13 +717,13 @@ Then inject it into code that accepts a `&dyn CostModel`: let model = PreferDDSketch; let strategy = - SketchFamilyStrategy::new(&model); + SketchAlgorithmStrategy::new(&model); let replacements = strategy.replacements(&target); ``` -Important: changing `rank_candidates` changes the preferred ordering, but `SketchFamilyStrategy` still enumerates every valid sketch candidate. +Important: changing `rank_candidates` changes the preferred ordering, but `SketchAlgorithmStrategy` still enumerates every valid sketch candidate. A custom cost model should not change which alternatives are semantically legal. @@ -706,7 +735,7 @@ Use this as a practical guide. ### `rank_candidates` -Use when you want to change the preference among valid sketch families. +Use when you want to change the preference among valid sketch algorithms. Example: @@ -721,8 +750,8 @@ Signature: fn rank_candidates( &self, intent: &AggIntent, - candidates: &[SketchKind], -) -> Vec; + candidates: &[SketchAlgorithm], +) -> Vec; ``` The returned vector should rank candidates from most to least preferred. @@ -733,14 +762,14 @@ It should rank candidates that were supplied to it rather than invent unrelated ### `size_params` -Use when the sketch family is already known and you want to choose its parameters from an accuracy target. +Use when the sketch algorithm is already known and you want to choose its parameters from an accuracy target. Signature: ```rust fn size_params( &self, - kind: SketchKind, + kind: SketchAlgorithm, intent: &AggIntent, eps: f64, delta: f64, @@ -756,7 +785,7 @@ Typical uses include: Conceptually: ```text -SketchKind + AggIntent + accuracy target +SketchAlgorithm + AggIntent + accuracy target | v SketchParams @@ -912,13 +941,13 @@ That turns a cost decision into a legality decision and prevents later global pl --- -## 12. Adding a new sketch family +## 12. Adding a new sketch algorithm -A new sketch family generally touches more than `ReplacementStrategy`. +A new sketch algorithm generally touches more than `ReplacementStrategy`. -The strategy should not maintain its own private list of sketch kinds. +The strategy should not maintain its own private list of sketch algorithms. -`SketchFamilyStrategy` obtains sketch alternatives through `implementation.rs`'s existing interface: +`SketchAlgorithmStrategy` obtains sketch alternatives through `implementation.rs`'s existing interface: ```rust summary_candidates(intent) @@ -926,23 +955,24 @@ summary_candidates(intent) and binds them through the normal binder. -Therefore, when adding a new built-in sketch family, the intended flow is: +Therefore, when adding a new built-in sketch algorithm, the intended flow is: ```text -1. Teach `implementation.rs` that the sketch is a valid candidate - for the relevant AggIntent. +1. Teach `implementation.rs` that the algorithm is a valid candidate + for the relevant AggIntent, and that `SketchKind::new` classifies + it into the right category. 2. Teach the cost model how to rank and size it. -3. Ensure the binder can realize the sketch family. +3. Ensure the binder can realize the algorithm. -4. SketchFamilyStrategy will then enumerate it through the +4. SketchAlgorithmStrategy will then enumerate it through the existing candidate/binding path. ``` This keeps one source of truth for sketch applicability. -Do not special-case the new sketch inside `SketchFamilyStrategy` unless the strategy itself needs fundamentally new behavior. +Do not special-case the new sketch inside `SketchAlgorithmStrategy` unless the strategy itself needs fundamentally new behavior. --- @@ -953,7 +983,7 @@ The basic calling pattern is: ```rust let target = TargetSubDAG::new(&root); let strategy = - SketchFamilyStrategy::default_cost_model(); + SketchAlgorithmStrategy::default_cost_model(); if strategy.matches(&target) { let candidates = @@ -1116,13 +1146,13 @@ For ranking: let ranked = model.rank_candidates( &intent, - &[SketchKind::Kll, - SketchKind::DDSketch], + &[SketchAlgorithm::Kll, + SketchAlgorithm::DDSketch], ); assert_eq!( ranked[0], - SketchKind::DDSketch + SketchAlgorithm::DDSketch ); ``` @@ -1132,7 +1162,7 @@ For example: ```rust let strategy = - SketchFamilyStrategy::new(&model); + SketchAlgorithmStrategy::new(&model); let replacements = strategy.replacements(&target); @@ -1175,7 +1205,7 @@ fn replacements(...) -> Vec { ### Mistake: maintaining a second sketch-applicability table -If `implementation.rs` already defines which sketch families satisfy an `AggIntent`, reuse that source. +If `implementation.rs` already defines which sketch algorithms satisfy an `AggIntent`, reuse that source. Otherwise the binder and replacement strategy can silently disagree. @@ -1243,7 +1273,7 @@ When adding a new cost model: - [ ] Use extension hooks for extension-defined implementations/readouts. - [ ] Use CSE hooks for recompute-vs.-sharing costs. - [ ] Test the hook directly. -- [ ] Test integration through a consumer such as `SketchFamilyStrategy`. +- [ ] Test integration through a consumer such as `SketchAlgorithmStrategy`. - [ ] Verify that changing cost preferences does not silently remove valid replacement candidates. --- @@ -1258,7 +1288,7 @@ Use this table to find the right place for a change. | Add a new replacement for an existing target shape | `ReplacementStrategy::replacements` | | Change when a strategy applies | `ReplacementStrategy::matches` | | Add a new built-in sketch candidate | `implementation.rs`'s summary-candidate mapping | -| Prefer one sketch family over another | `CostModel::rank_candidates` | +| Prefer one sketch algorithm over another | `CostModel::rank_candidates` | | Change sketch sizing for an accuracy target | `CostModel::size_params` | | Add extension-defined implementation behavior | `CostModel::realize_extension` | | Add extension-defined readout behavior | `CostModel::readout_extension` | @@ -1266,9 +1296,9 @@ Use this table to find the right place for a change. | Change shared-maintenance cost | `CostModel::cse_shared_maintenance_cost` | | Change current share/recompute choice | `CostModel::cse_share_decision` | | Decide whether an available implementation satisfies a required one | `impl Matcher` | -| Produce a normal (ranked-first) bound summary for one target | `SketchFamilyStrategy::replacements(...).into_iter().next()` | +| Produce a normal (ranked-first) bound summary for one target | `SketchAlgorithmStrategy::replacements(...).into_iter().next()` | | Bind a whole workload's roots, sharing across CSE-collapsed roots | reuse `bind::implement_workload`/`implement_workload_with` | | Bind a specific, already-chosen `Implementation` | reuse `bind::bind_with_implementation` | -| Enumerate valid sketch kinds | reuse `implementation::summary_candidates` | +| Enumerate valid sketch algorithms | reuse `implementation::summary_candidates` | | Build a target with no workload context | `TargetSubDAG::new` | | Build a target with known sharing context | `TargetSubDAG::with_consumer_count` | diff --git a/docs/user-guide/user-guide.md b/docs/user-guide/user-guide.md index 747e62c2..c4177e0a 100644 --- a/docs/user-guide/user-guide.md +++ b/docs/user-guide/user-guide.md @@ -86,16 +86,16 @@ allowed, `Epsilon(e)` / `EpsilonDelta{epsilon, delta}` otherwise. Feed the `QueryExpr` to `asap-aware-mapping`. This crate depends only on `asap-types`, never on a front end, so it's agnostic to which language produced the tree. There is no "bind me one tree" -entry point: `SketchFamilyStrategy::replacements()` always returns every valid candidate for a +entry point: `SketchAlgorithmStrategy::replacements()` always returns every valid candidate for a target, ranked, and you take the one you want. ```rust -use asap_aware_mapping::{Replacement, ReplacementStrategy, ReplacementSubDAG, SketchFamilyStrategy, TargetSubDAG}; +use asap_aware_mapping::{Replacement, ReplacementStrategy, ReplacementSubDAG, SketchAlgorithmStrategy, TargetSubDAG}; use std::rc::Rc; let root = Rc::new(pre_asap); let target = TargetSubDAG::new(&root); -let candidates = SketchFamilyStrategy::default_cost_model().replacements(&target); +let candidates = SketchAlgorithmStrategy::default_cost_model().replacements(&target); // Take the cost-model-preferred candidate — the common case. let Some(ReplacementSubDAG { replacement: Replacement::Summary(post_asap), .. }) = @@ -110,10 +110,10 @@ else { ``` `implementations_for_with(&AggIntent, &dyn CostModel) -> Vec` is the lower-level -enumeration `SketchFamilyStrategy` wraps, if you only need the per-node decision (sketch, exact +enumeration `SketchAlgorithmStrategy` wraps, if you only need the per-node decision (sketch, exact accumulator, or pass-through) without binding it into a `SummaryNode`. -`SketchFamilyStrategy::new(&dyn CostModel)` (vs. `default_cost_model()`) is the extension point for +`SketchAlgorithmStrategy::new(&dyn CostModel)` (vs. `default_cost_model()`) is the extension point for a deployment that wants its own candidate ranking or parameter sizing instead of this crate's built-in static preference order (`DefaultCostModel` — what `default_cost_model()` uses). See the `CostModel` trait doc in `crates/asap-aware-mapping/src/cost_model.rs` for its overridable From e7480979eff010d5f4b4f7e87eecb749b651c056 Mon Sep 17 00:00:00 2001 From: zz_y Date: Mon, 24 Aug 2026 07:15:46 -0600 Subject: [PATCH 2/2] docs(asap-aware-mapping): explain the into_iter().next() take-first idiom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reader asked what the diagram's "a caller wanting one answer: replacements(...).into_iter().next()" line actually does — spell it out inline instead of assuming it's self-evident. --- docs/developer_docs/ASAP-aware-mapping-developer-guide.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer_docs/ASAP-aware-mapping-developer-guide.md b/docs/developer_docs/ASAP-aware-mapping-developer-guide.md index bc2193bd..2d5b20bd 100644 --- a/docs/developer_docs/ASAP-aware-mapping-developer-guide.md +++ b/docs/developer_docs/ASAP-aware-mapping-developer-guide.md @@ -315,6 +315,8 @@ Underneath `SketchAlgorithmStrategy::replacements()` sits one more function, `bi a caller wanting one answer: replacements(...).into_iter().next() ``` +(`replacements(...)` returns a `Vec` ranked most-preferred-first; `.into_iter().next()` takes just that first entry and drops the rest — the same "keep candidate[0]" step `implement_tree_with` used to do internally, now written out explicitly on the calling side instead of hidden behind a second entry point.) + ### Replacement-strategy path ```text