Skip to content

Commit 2faa0de

Browse files
zzylolclaude
andcommitted
test(integration): derive the design doc's filtered worked example
Pass 1 now builds a filtered SummaryAgg, so the #573 §4.2.2 worked example runs whole: the FILTER range and the renamed value set move to the selection, and the expression condition stays in the definition. The quantile state is read from the Stage 1 logical candidates, which build it whatever plan the cost model selects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
1 parent 1eae73e commit 2faa0de

1 file changed

Lines changed: 63 additions & 28 deletions

File tree

‎crates/integration-tests/tests/summary_coverage_e2e.rs‎

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
//! Query string → planned summary state → `coverage()`, for the examples of
2-
//! the ASAP primitive schema design doc (#573 §4.2.2). Each query is lowered,
3-
//! searched and selected with the default cost model, and the quantile's KLL
4-
//! `SummaryAgg` in the selected DAG is checked.
2+
//! the ASAP primitive schema design doc (#573 §4.2.2). Each query is lowered
3+
//! and its Stage 1 logical candidates are enumerated; the quantile's KLL
4+
//! `SummaryAgg` is checked. Candidates, not the selected plan: the plan the
5+
//! cost models select for these small queries may build no summary.
56
7+
use std::collections::BTreeMap;
68
use std::ops::Bound;
79
use std::rc::Rc;
810

911
use asap_frontend_sql::{lower_sql, SqlCatalog};
1012
use asap_integration_tests::fixtures::lower_promql;
11-
use asap_logical_optimizer::search_workload;
12-
use asap_plan_selection::candidate_selection::global_selection;
13-
use asap_plan_selection::DefaultCostModel;
13+
use asap_logical_optimizer::pass1::logical_candidates::{
14+
compose_logical_candidate, enumerate_choices, enumerate_local_logical_candidates,
15+
};
1416
use asap_types::ir::properties::summary_coverage::{ColumnIdentity, Constraint, SelectionBox};
1517
use asap_types::ir::scalar::ScalarValue;
1618
use asap_types::ir::schema::{DataType, Field, FieldDataType, Schema, SketchAlgorithm};
17-
use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode, Predicate, ScalarExpr};
19+
use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode, Predicate, QueryRoot, ScalarExpr};
1820
use asap_types::types::AccuracyTarget;
1921

2022
fn accuracy() -> AccuracyTarget {
@@ -24,25 +26,25 @@ fn accuracy() -> AccuracyTarget {
2426
}
2527
}
2628

27-
fn plan(pre: Rc<OperatorNode>) -> Rc<OperatorNode> {
28-
let space = search_workload(vec![("query", pre)]);
29-
global_selection(&space, &DefaultCostModel)
30-
.assemble_selected_dag(&space.roots[0].1)
31-
.expect("materialization failed")
32-
.expect("root must be discovered")
33-
}
34-
35-
/// The single KLL `SummaryAgg` of the selected DAG.
36-
fn kll_state(root: &Rc<OperatorNode>) -> Rc<OperatorNode> {
37-
let states: Vec<_> = OperatorNode::reachable(root)
38-
.into_iter()
39-
.filter(|node| {
29+
/// The KLL `SummaryAgg` of the first logical candidate that builds one.
30+
fn kll_state(root: Rc<OperatorNode>) -> Rc<OperatorNode> {
31+
let inventory =
32+
enumerate_local_logical_candidates(vec![(0, QueryRoot::Operator(root))], &BTreeMap::new())
33+
.unwrap();
34+
for choice in enumerate_choices(&inventory, usize::MAX) {
35+
let roots = compose_logical_candidate(&inventory, &choice).unwrap();
36+
let QueryRoot::Operator(root) = &roots[0].1 else {
37+
panic!("operator root")
38+
};
39+
let state = OperatorNode::reachable(root).into_iter().find(|node| {
4040
matches!(node.asap(), Some(ASAPOp::SummaryAgg { family: FieldDataType::Sketch(kind, _), .. })
4141
if kind.algorithm() == &SketchAlgorithm::Kll)
42-
})
43-
.collect();
44-
assert_eq!(states.len(), 1, "expected one KLL state in {root:#?}");
45-
states[0].clone()
42+
});
43+
if let Some(state) = state {
44+
return state;
45+
}
46+
}
47+
panic!("no candidate builds a KLL state");
4648
}
4749

4850
async fn sql_state(sql: &str) -> Rc<OperatorNode> {
@@ -57,13 +59,11 @@ async fn sql_state(sql: &str) -> Rc<OperatorNode> {
5759
let pre = lower_sql(sql, &catalog, accuracy())
5860
.await
5961
.unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}"));
60-
kll_state(&plan(pre))
62+
kll_state(pre)
6163
}
6264

6365
fn promql_state(query: &str) -> Rc<OperatorNode> {
64-
kll_state(&plan(
65-
lower_promql(query, accuracy()).expect("lowering failed"),
66-
))
66+
kll_state(lower_promql(query, accuracy()).expect("lowering failed"))
6767
}
6868

6969
fn column(table: Option<&str>, name: &str) -> ColumnIdentity {
@@ -157,6 +157,41 @@ async fn sql_renamed_column_moves_and_expression_stays() {
157157
);
158158
}
159159

160+
/// The worked example of #573 §4.2.2: the `FILTER` range and the renamed
161+
/// value set move to the selection, the expression condition stays.
162+
#[tokio::test]
163+
async fn sql_worked_example() {
164+
let state = sql_state(
165+
"SELECT job, approx_percentile_cont(latency, 0.99) FILTER (WHERE latency < 100) \
166+
FROM (SELECT job, region AS r, latency FROM t \
167+
WHERE region = 'us' AND latency * 2 > 10) \
168+
GROUP BY job",
169+
)
170+
.await;
171+
assert_eq!(
172+
state.coverage().unwrap().selection,
173+
vec![SelectionBox {
174+
columns: [
175+
(
176+
column(None, "latency"),
177+
Constraint::Interval {
178+
lower: Bound::Unbounded,
179+
upper: Bound::Excluded(ScalarValue::Float64(100.0)),
180+
},
181+
),
182+
(column(None, "r"), Constraint::In(vec![utf8("us")])),
183+
]
184+
.into(),
185+
relative_time: None,
186+
}]
187+
);
188+
let definition = &state.coverage().unwrap().definition;
189+
assert!(matches!(
190+
definition.asap(),
191+
Some(ASAPOp::SummaryAgg { filter: None, .. })
192+
));
193+
}
194+
160195
#[test]
161196
fn promql_offset_window_is_relative_time() {
162197
let state = promql_state("quantile_over_time(0.99, m[1m] offset 2m)");

0 commit comments

Comments
 (0)