Skip to content

Commit 94a6d35

Browse files
zzylolclaude
andcommitted
test: port the legacy-search callers outside logical-optimizer to Stage 1
The sketch_coverage devtool and the frontend, integration and executor tests that drove ASAPStrategies / search_workload / retain_exact now take their candidates from Stage 1 (Pass 1 inventory and composition) or the stage pipeline. Tests that only pinned legacy search behaviour are deleted, among them integration-tests/tests/cse.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a35b447 commit 94a6d35

15 files changed

Lines changed: 658 additions & 1295 deletions

File tree

‎crates/devtools/src/bin/sketch_coverage.rs‎

Lines changed: 63 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,24 @@
22
//
33
// Lowers every query in every corpus we have (mirrors `variant_coverage`'s
44
// corpus list exactly, so the two reports are directly comparable) with an
5-
// *approximate* `AccuracyTarget`, runs `asap_logical_optimizer::explain_replacements`
6-
// over each corpus as one workload, and reports the MVP demo's query-coverage
7-
// metric: of the queries that lowered successfully, what fraction got
8-
//
9-
// - a `SketchApproximation` candidate (a genuine sketch alternative was
10-
// found for at least one aggregate in the query — the KLL-vs-DDSketch
11-
// kind of degree of freedom), and/or
12-
// - a `CommonSubexpressionReuse` candidate (the query shares a sub-DAG,
13-
// inside itself or with another query in the same corpus, that a
14-
// build-once-and-share candidate was found for).
5+
// *approximate* `AccuracyTarget`, runs Stage 1's Pass 1
6+
// (`enumerate_local_logical_candidates`) over each query, and reports which
7+
// sketch alternatives Pass 1 offers for it, plus the fraction of lowered
8+
// queries with at least one.
159
//
1610
// `--epsilon <f64>` (default 0.01) sets the `AccuracyTarget` every query in
17-
// every corpus lowers with. Without an approximate target,
18-
// `ASAPStrategies` never has a genuine sketch alternative to
19-
// report.
20-
//
21-
// This is a workload-level count (`explain_replacements` runs
22-
// `search_workload` once per corpus, over every query in it together), not
23-
// just a per-query re-run of the single-target path — so cross-query CSE
24-
// reuse inside one corpus shows up here the same way it would in the
25-
// dag-viewer's Union mode.
11+
// every corpus lowers with. Without an approximate target, Pass 1 offers no
12+
// sketch alternative.
2613

2714
use asap_devtools::lower_promql_with_data_ingestion_interval;
2815
use asap_frontend_sql::{lower_sql_dialect, SqlCatalog};
29-
use asap_logical_optimizer::{explain_replacements, ExplanationKind};
30-
use asap_types::ir::schema::{DataType, Field, Schema};
31-
use asap_types::ir::OperatorNode;
16+
use asap_logical_optimizer::pass1::logical_candidates::enumerate_local_logical_candidates;
17+
use asap_logical_optimizer::Realization;
18+
use asap_types::ir::schema::{DataType, Field, GroupingStrategy, Schema};
19+
use asap_types::ir::{OperatorNode, QueryRoot};
3220
use asap_types::types::AccuracyTarget;
3321
use asap_types::workload::SqlDialect;
34-
use std::collections::BTreeSet;
22+
use std::collections::{BTreeMap, BTreeSet};
3523
use std::rc::Rc;
3624

3725
/// Line-based `#`/`--` comment stripping, then split on `;` — the shape every
@@ -123,9 +111,10 @@ struct CorpusCoverage {
123111
name: &'static str,
124112
lowered: usize,
125113
failed: usize,
126-
sketch_covered: usize,
127-
cse_covered: usize,
128-
either_covered: usize,
114+
/// Lowered queries Pass 1 rejected.
115+
rejected: usize,
116+
/// Per lowered query Pass 1 accepted, the sketch alternatives it offers.
117+
sketches: Vec<(String, BTreeSet<String>)>,
129118
}
130119

131120
fn pct(n: usize, total: usize) -> String {
@@ -136,86 +125,69 @@ fn pct(n: usize, total: usize) -> String {
136125
}
137126
}
138127

139-
/// `explain_replacements`' `location` is a comma-joined list of breadcrumbs
140-
/// (`collect_locations` in `explanation.rs`), one per path from a workload
141-
/// root to the target — e.g. `root "q3"` or `root "q3" > lhs`. A location
142-
/// "covers" `label` (a bare `root "qN"` breadcrumb) if `label` is exactly one
143-
/// of those comma-separated entries or the prefix of one that goes deeper —
144-
/// i.e. the explanation's target is reachable from that query's root at all.
145-
fn covers(location: &str, label: &str) -> bool {
146-
location
147-
.split(", ")
148-
.any(|loc| loc == label || loc.starts_with(&format!("{label} > ")))
149-
}
150-
151-
fn root_label(id: &str) -> String {
152-
format!("root {id:?}")
153-
}
154-
155-
/// Run `explain_replacements` over one corpus's already-lowered roots as one
156-
/// workload, then attribute each finding back to the query root(s) it's
157-
/// reachable from.
128+
/// Run Pass 1 over each of one corpus's already-lowered queries and collect
129+
/// the sketch alternatives it offers for any of the query's targets. A Hydra
130+
/// alternative is listed as `Hydra(<algorithm>)`.
158131
fn analyze_corpus(
159132
name: &'static str,
160133
roots: Vec<(String, Rc<OperatorNode>)>,
161134
failed: usize,
162135
) -> CorpusCoverage {
163136
let lowered = roots.len();
164-
let labels: Vec<String> = roots.iter().map(|(id, _)| root_label(id)).collect();
165-
let explanations = explain_replacements(roots);
166-
167-
let mut sketch_covered: BTreeSet<usize> = BTreeSet::new();
168-
let mut cse_covered: BTreeSet<usize> = BTreeSet::new();
169-
for explanation in &explanations {
170-
for (i, label) in labels.iter().enumerate() {
171-
if !covers(&explanation.location, label) {
172-
continue;
173-
}
174-
match explanation.kind {
175-
ExplanationKind::SketchApproximation => {
176-
sketch_covered.insert(i);
177-
}
178-
ExplanationKind::CommonSubexpressionReuse => {
179-
cse_covered.insert(i);
137+
let mut rejected = 0;
138+
let mut sketches = Vec::new();
139+
for (id, root) in roots {
140+
let Ok(inventory) = enumerate_local_logical_candidates(
141+
vec![(id.clone(), QueryRoot::Operator(root))],
142+
&BTreeMap::new(),
143+
) else {
144+
rejected += 1;
145+
continue;
146+
};
147+
let offered = inventory
148+
.targets
149+
.iter()
150+
.flat_map(|target| target.alternatives.iter().zip(&target.groupings))
151+
.filter_map(|(alternative, grouping)| match alternative {
152+
Realization::Sketch(kind) if *grouping == GroupingStrategy::default() => {
153+
Some(format!("{:?}", kind.algorithm()))
180154
}
181-
// `#[non_exhaustive]`: a future kind just doesn't count
182-
// toward either bucket here until this tool is taught about it.
183-
_ => {}
184-
}
185-
}
155+
Realization::Sketch(kind) => Some(format!("Hydra({:?})", kind.algorithm())),
156+
_ => None,
157+
})
158+
.collect();
159+
sketches.push((id, offered));
186160
}
187-
let either_covered = sketch_covered.union(&cse_covered).count();
188-
189161
CorpusCoverage {
190162
name,
191163
lowered,
192164
failed,
193-
sketch_covered: sketch_covered.len(),
194-
cse_covered: cse_covered.len(),
195-
either_covered,
165+
rejected,
166+
sketches,
196167
}
197168
}
198169

170+
fn covered(r: &CorpusCoverage) -> usize {
171+
r.sketches.iter().filter(|(_, s)| !s.is_empty()).count()
172+
}
173+
199174
fn report(r: &CorpusCoverage) {
200175
println!("--- {} ---", r.name);
201-
println!("lowered: {}, failed: {}", r.lowered, r.failed);
202176
println!(
203-
"sketch-approximable: {}/{} ({})",
204-
r.sketch_covered,
205-
r.lowered,
206-
pct(r.sketch_covered, r.lowered)
207-
);
208-
println!(
209-
"CSE-shareable: {}/{} ({})",
210-
r.cse_covered,
211-
r.lowered,
212-
pct(r.cse_covered, r.lowered)
177+
"lowered: {}, failed: {}, rejected by Pass 1: {}",
178+
r.lowered, r.failed, r.rejected
213179
);
180+
for (id, offered) in &r.sketches {
181+
if !offered.is_empty() {
182+
let offered: Vec<_> = offered.iter().map(String::as_str).collect();
183+
println!(" {id}: {}", offered.join(", "));
184+
}
185+
}
214186
println!(
215-
"either (coverage): {}/{} ({})",
216-
r.either_covered,
187+
"sketch-approximable: {}/{} ({})",
188+
covered(r),
217189
r.lowered,
218-
pct(r.either_covered, r.lowered)
190+
pct(covered(r), r.lowered)
219191
);
220192
println!();
221193
}
@@ -332,22 +304,16 @@ async fn main() {
332304

333305
let total_lowered: usize = results.iter().map(|r| r.lowered).sum();
334306
let total_failed: usize = results.iter().map(|r| r.failed).sum();
335-
let total_sketch: usize = results.iter().map(|r| r.sketch_covered).sum();
336-
let total_cse: usize = results.iter().map(|r| r.cse_covered).sum();
337-
let total_either: usize = results.iter().map(|r| r.either_covered).sum();
307+
let total_rejected: usize = results.iter().map(|r| r.rejected).sum();
308+
let total_sketch: usize = results.iter().map(covered).sum();
338309

339310
println!("=== global (epsilon = {epsilon}) ===");
340-
println!("total lowered: {total_lowered}, total failed: {total_failed}");
341311
println!(
342-
"sketch-approximable: {total_sketch}/{total_lowered} ({})",
343-
pct(total_sketch, total_lowered)
312+
"total lowered: {total_lowered}, total failed: {total_failed}, \
313+
rejected by Pass 1: {total_rejected}"
344314
);
345315
println!(
346-
"CSE-shareable: {total_cse}/{total_lowered} ({})",
347-
pct(total_cse, total_lowered)
348-
);
349-
println!(
350-
"either (coverage): {total_either}/{total_lowered} ({})",
351-
pct(total_either, total_lowered)
316+
"sketch-approximable: {total_sketch}/{total_lowered} ({})",
317+
pct(total_sketch, total_lowered)
352318
);
353319
}

‎crates/executor/tests/common/mod.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,30 @@ pub fn selected_dag(
6161
};
6262
root.clone()
6363
}
64+
65+
/// Every whole-query candidate Stage 1's Pass 1 lists for `root` (up to
66+
/// 4096), composed; choices that do not compose are skipped.
67+
pub fn stage1_candidates(root: &Rc<OperatorNode>) -> Vec<Rc<OperatorNode>> {
68+
use asap_logical_optimizer::pass1::logical_candidates::{
69+
compose_logical_candidate, enumerate_choices, enumerate_local_logical_candidates,
70+
};
71+
use planner_types::ir::QueryRoot;
72+
let inventory = enumerate_local_logical_candidates(
73+
vec![(0, QueryRoot::Operator(Rc::clone(root)))],
74+
&Default::default(),
75+
)
76+
.unwrap();
77+
enumerate_choices(&inventory, 4096)
78+
.iter()
79+
.filter_map(|choice| {
80+
match compose_logical_candidate(&inventory, choice)
81+
.ok()?
82+
.remove(0)
83+
.1
84+
{
85+
QueryRoot::Operator(node) => Some(node),
86+
QueryRoot::Scalar(_) => None,
87+
}
88+
})
89+
.collect()
90+
}

‎crates/executor/tests/deployment_computation.rs‎

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use asap_executor::{
77
runtime::{Limits, RunContext, Scope},
88
values::{Batch, Value},
99
};
10-
use common::{compile_physical_asap_dag, selected_dag};
10+
use common::{compile_physical_asap_dag, selected_dag, stage1_candidates};
1111
use futures::{executor::block_on, StreamExt};
1212
use planner_types::ir::export::{PhysicalASAPDAG, PhysicalASAPOperatorPayload};
1313
use planner_types::{ir::schema::*, types::AccuracyTarget, workload::*};
@@ -615,30 +615,25 @@ fn population_sums_and_averages_are_compensated() {
615615
#[test]
616616
fn stored_count_min_bare_count_compiles_to_a_evaluation() {
617617
use asap_executor::summary_kernels::CountMinSketchAccumulator;
618-
use asap_logical_optimizer::{Replacement, ReplacementStrategy, TargetSubDAG};
619618
let root = lower_with("count(up)", AccuracyTarget::Epsilon(0.02));
620-
let dag = asap_logical_optimizer::ASAPStrategies::default()
621-
.replacements(&TargetSubDAG::new(&root))
619+
let dag = stage1_candidates(&root)
622620
.into_iter()
623-
.find_map(|candidate| match candidate.replacement {
624-
Replacement::SubDAG(node) => {
625-
let dag = compile_physical_asap_dag(&node).ok()?;
626-
let bare_count = dag.nodes.iter().any(|n| {
627-
matches!(
628-
&n.payload,
629-
PhysicalASAPOperatorPayload::SummaryEstimate {
630-
query: SketchStatistic::PointCount { value: None, .. }
631-
}
632-
)
633-
});
634-
let count_min = dag.nodes.iter().any(|n| {
635-
matches!(&n.payload, PhysicalASAPOperatorPayload::SummaryAgg {
621+
.find_map(|node| {
622+
let dag = compile_physical_asap_dag(&node).ok()?;
623+
let bare_count = dag.nodes.iter().any(|n| {
624+
matches!(
625+
&n.payload,
626+
PhysicalASAPOperatorPayload::SummaryEstimate {
627+
query: SketchStatistic::PointCount { value: None, .. }
628+
}
629+
)
630+
});
631+
let count_min = dag.nodes.iter().any(|n| {
632+
matches!(&n.payload, PhysicalASAPOperatorPayload::SummaryAgg {
636633
family: FieldDataType::Sketch(kind, _), ..
637634
} if kind.algorithm() == &SketchAlgorithm::Cms)
638-
});
639-
(bare_count && count_min).then_some(dag)
640-
}
641-
_ => None,
635+
});
636+
(bare_count && count_min).then_some(dag)
642637
})
643638
.expect("Planner lists a Count-Min candidate for count(up)");
644639
let state = dag

0 commit comments

Comments
 (0)