Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions crates/plan/src/boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,27 @@ fn bind_summary_with(
.into_iter()
.next()
.expect("approximate intent has at least one candidate summary");
let params = match kind {
let params = cost_model.size_params(kind.clone(), intent, eps, delta);
Implementation::Sketch { kind, params }
}

/// `asap-plan`'s built-in `SummaryParams` sizing, keyed off the resolved
/// `(eps, delta)` accuracy budget. [`CostModel::size_params`]'s default
/// body — factored out to a free function so a deployment's own
/// `CostModel` impl can still delegate to it for the candidates it
/// doesn't want to resize itself.
///
/// Each formula inverts the sketch family's standard error bound to the
/// smallest parameter satisfying the target, clamped to the family's sane
/// range. A non-positive ε saturates to the clamp maximum (tightest
/// allowed).
pub fn default_size_params(
kind: SummaryKind,
intent: &AggIntent,
eps: f64,
delta: f64,
) -> SummaryParams {
match kind {
SummaryKind::Kll => SummaryParams::Kll { k: kll_k(eps) },
SummaryKind::Cms => SummaryParams::Cms {
width: cms_width(eps),
Expand Down Expand Up @@ -294,8 +314,7 @@ fn bind_summary_with(
| SummaryKind::MinMax
| SummaryKind::Increase
| SummaryKind::Rate => unreachable!("exact accumulators are not sketch candidates"),
};
Implementation::Sketch { kind, params }
}
}

// ── Parameter sizing ──────────────────────────────────────────────────────────
Expand Down
82 changes: 80 additions & 2 deletions crates/plan/src/cost_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
//! byte.

use asap_ir::intent_algebra::agg_intent::AggIntent;
use asap_sketch::SummaryKind;
use asap_sketch::{SummaryKind, SummaryParams};

/// Ranks the candidate summary families for one [`AggIntent`], best choice
/// first.
Expand All @@ -54,10 +54,32 @@ pub trait CostModel {
/// `implementation_for_with` treats that the same as `candidates`
/// having been empty to begin with.
fn rank_candidates(&self, intent: &AggIntent, candidates: &[SummaryKind]) -> Vec<SummaryKind>;

/// Size [`SummaryParams`] for `kind` (one of the candidates
/// [`rank_candidates`](Self::rank_candidates) put first) under the
/// resolved `(eps, delta)` accuracy budget.
///
/// Splitting sizing out from candidate selection lets a deployment own
/// its own parameter-sizing math (e.g. an empirically-tuned table, or
/// discrete rungs required by a downstream catalog) without forking
/// [`boundary::implementation_for_with`] — the same "one extension
/// point" rationale as `rank_candidates`, one level deeper. Default:
/// [`boundary::default_size_params`], `asap-plan`'s built-in formulas
/// (unchanged) — a deployment that only needs to reorder candidates,
/// not resize them, can leave this method unimplemented.
fn size_params(
&self,
kind: SummaryKind,
intent: &AggIntent,
eps: f64,
delta: f64,
) -> SummaryParams {
crate::boundary::default_size_params(kind, intent, eps, delta)
}
}

/// The default cost model: preserves [`summary_candidates`]'s built-in static
/// order unchanged.
/// order and [`boundary::default_size_params`]'s built-in sizing unchanged.
///
/// [`summary_candidates`]: crate::boundary::summary_candidates
pub struct DefaultCostModel;
Expand Down Expand Up @@ -105,4 +127,60 @@ mod tests {
let ranked = AlwaysPreferLast.rank_candidates(&intent, candidates);
assert_eq!(ranked.first(), candidates.last());
}

/// A deployment that only overrides `rank_candidates` keeps
/// `asap-plan`'s built-in sizing via the trait's default `size_params`
/// body — the split is opt-in per method, not all-or-nothing.
#[test]
fn size_params_default_body_matches_default_size_params() {
let intent = default_cardinality();
assert_eq!(
AlwaysPreferLast.size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
crate::boundary::default_size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
);
}

/// A deployment CAN override `size_params` independently of
/// `rank_candidates` — e.g. to size against a catalog-constrained set
/// of discrete parameter rungs instead of `asap-plan`'s continuous
/// formulas.
struct DiscreteKllRungs;

impl CostModel for DiscreteKllRungs {
fn rank_candidates(&self, _intent: &AggIntent, candidates: &[SummaryKind]) -> Vec<SummaryKind> {
candidates.to_vec()
}

fn size_params(
&self,
kind: SummaryKind,
intent: &AggIntent,
eps: f64,
delta: f64,
) -> SummaryParams {
match kind {
SummaryKind::Kll => {
let k = if eps >= 0.01 { 200 } else { 2048 };
SummaryParams::Kll { k }
}
other => crate::boundary::default_size_params(other, intent, eps, delta),
}
}
}

#[test]
fn custom_cost_model_can_override_sizing_independently_of_ranking() {
use asap_ir::intent_algebra::agg_intent::default_quantile;

let intent = default_quantile(0.99);
assert_eq!(
DiscreteKllRungs.size_params(SummaryKind::Kll, &intent, 0.001, 0.01),
SummaryParams::Kll { k: 2048 },
);
// Untouched kinds still fall through to the default formula.
assert_eq!(
DiscreteKllRungs.size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
crate::boundary::default_size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
);
}
}
Loading