Repository navigation
Reorganize by stage: one memo-based Pass 2, Stage 1 finishes the memo, deployment inputs split by stage, asap-planner as the only entry point #958
Description
Problem, by example
crates/logical-asap-optimization/src (Stage 1) is hard to navigate, and part of it is not Stage 1. All facts below are from main and the #952 branch head 7faad601.
1. Pass 2 is implemented twice.
| Path | Files | Callers |
|---|---|---|
| (A) whole-workload: rewrite one chosen computation assignment, enumerate every non-empty subset of sharing choices | asap_aware_cse.rs (4633 lines: identical_expression_pass2_candidates, summary_capability_pass2_candidates, window_composition_pass2_candidates, rollup_pass2_candidates, topk_reuse_pass2_candidates), workload_candidates.rs (Pass2Candidate) |
non-test: plan-selection/src/search/query_pruning.rs, search/pipeline.rs (type only), devtools candidate_labels.rs / planning_stages.rs, and the memo files memo/window_candidates.rs, memo/rules/{rollup,scan_labels,state_configurations,topk}.rs; test-only: plan-selection/src/lib.rs, costing/{direct_profile,lower_bounds,direct_price_options_tests,range_price_options_tests}.rs, physical-asap-optimization/src/lib.rs, memo/state_groups.rs and integration tests |
| (B) memo: each rule adds implementation candidates to memo groups | memo/, memo/rules/* |
plan-selection/src/search/candidates/inventory.rs and the memo search |
On main, plan-selection reaches (A) outside tests in only one place: prune_query_candidates (search/query_pruning.rs), called from route.rs, checks each query's own candidates alone before the joint search through stage1_candidates_of_choice. Every Stage 1 → 3 entry point (run_stage1_to_stage3, _with, _from_pass1) runs the memo search (run_with_initial_memo → candidate_connection_search). Pass2Candidate survives only as the type of LogicalCandidate::pass2, which the memo run leaves empty, and in devtools labels. The other plan-selection references (lib.rs, direct_profile.rs, lower_bounds.rs, direct_price_options_tests.rs, range_price_options_tests.rs), and physical-asap-optimization/src/lib.rs, are all in #[cfg(test)] code. So deleting (A) means giving per-query pruning a memo-based check.
(B) also calls (A) as a library, in five non-test memo files. memo/rules/rollup.rs builds a two-root workload and calls rollup_candidate_for and rollup_pass2_candidates. memo/window_candidates.rs calls pane_candidates and window_composition_pass2_candidates, so local pane candidates enumerate every subset of a candidate's pane builds (capped at MAX_PANE_CANDIDATES = 20). memo/rules/scan_labels.rs calls with_union_scan_labels, memo/rules/state_configurations.rs calls with_pane_chain and with_resized_build, and memo/rules/topk.rs calls topk_reuse_candidates. The full mapping is in the step 3 comment below. (A)'s subset enumeration is capped at MAX_REUSE_CANDIDATES = 20, about a million subsets. The memo is the intended search structure.
2. Stage 3 finishes Stage 1's output. memo/state_groups.rs ("opt-in concrete summary-state groups for the candidate-connection planner"), memo/state_configurations.rs, memo/window_candidates.rs and memo/rules/state_configurations.rs expand concrete sizes and pane grids into state groups. For example, readers needing KLL k ≥ 345 and k ≥ 200 get concrete KLL(345) and KLL(200) groups. Only plan-selection calls expand_concrete_summary_state_groups. So the memo that Stage 1 hands over is incomplete, and Stage 3 completes it with Stage 1 functions.
3. One deployment input serves three stages from the Stage 1 crate. deployment_capabilities.rs defines DeploymentCapabilities. Each field is read by a different stage:
supported_summaries: Stage 1, Pass 1;query_time_results_retained: Stage 2;raw_data_retained: Stage 3.
Stage 2 and Stage 3 import it from the Stage 1 crate.
4. The Stage 3 crate orchestrates the planner and defines its public API. asap-plan-selection holds four different things:
| Content | Where | Belongs to |
|---|---|---|
| Stage 3 itself: search, cost models, accuracy check | search/ (milp, refinement, costing, candidates), cost/, accuracy.rs |
plan-selection |
The pluggable-pass API that ASAPQuery-backend and ASAPCollector read: OptimizationPass, optimize(), check_contract |
search/mod.rs |
the facade |
Stage 1 → 3 orchestration: Stage1ToStage3Pass, run_stage1_to_stage3 |
search/pipeline.rs |
the facade |
The bundle of deployment inputs: PlanningModels, including DeploymentCapabilities and the executor compile check |
search/mod.rs |
assembled by the facade; each stage receives only its own part |
asap-planner, the facade, only runs the frontends and builds the installable plan (InstallablePlan, PlannerExecutorCompileCheck). Callers that already hold IR skip it and call asap_plan_selection::optimize directly.
5. There is no stage structure inside the crate.
- Pass 1 is one 6154-line file (
per_computation_candidates.rs), plusrealization.rsandfunction_rules.rs.function_rules.rsis an accuracy composition rule. - Pass 2 is spread over
asap_aware_cse.rs,workload_candidates.rs,memo/rules/*and a top-levelfiner_aggregate.rs. - Two files are named
state_configurations.rs(memo/andmemo/rules/), and rollup exists both inmemo/rules/rollup.rsand inasap_aware_cse.rs.
The layout approved in #572 (pass1/, pass2/, analytical accuracy/) was never applied to this crate.
Decisions (2026-10-27)
- One Pass 2. Map every capability of (A) to (B): delete what (B) already implements, port what it does not, then delete (A) (
workload_candidates.rs, the*_pass2_candidatessubset enumerations and their plan-selection callers). The mapping table goes in the PR body, so a capability is deleted only when it has a memo implementation or an issue. - Stage 1 finishes the memo. Expanding concrete sizes and pane grids is Pass 2 output: the coupling sites and their thresholds, as the docs: propose workload-wide planning, summary sharing, and materialization #509 design doc describes. Stage 1 runs it, and Stage 3 receives a complete memo and only selects. The expansion API stops being called from plan-selection.
- Deployment inputs split by stage. Each stage crate owns the field it reads: Stage 1 the supported summaries, Stage 2 query-time result retention, Stage 3 raw-data retention (
RawRetentionin Retained raw data as concrete state groups per lookback: offsets, @ and a retention limit #955). TheDeploymentCapabilitiesbundle, which parses--deployment-capabilitiesand dispatches the fields, moves to theasap-plannerfacade, which already depends on every stage.asap-typesstays shared types only. asap-planneris the only entry point;asap-plan-selectionis only Stage 3.asap-plannerowns:- the frontends;
OptimizationPassandoptimize()with the output-contract check;- Stage 1 → 2 → 3 orchestration;
- assembling the deployment inputs and dispatching them to the stages;
- the installable plan and the executor compile check.
asap-plan-selectiontakes the complete memo from Stage 1 (decision 2) and only the inputs Stage 3 reads: cost settings, accuracy model, evidence, raw-data retention, and the compile-check trait. It returns the selection and no longer calls Stage 1.- It may still call Stage 2 per candidate. Lazy physicalization keeps the search scalable.
- No compatibility layer for the moved API. The PR that moves
OptimizationPass/optimize()changes their import paths. It comes with a follow-up PR in ASAPQuery-backend and in ASAPCollector that updates their imports. No temporary re-export is left inasap-plan-selection.
Target layout
crates/logical-asap-optimization/src/
lib.rs
accuracy/ analytical rules (as today) + function_rules.rs
pass1/ per_computation_candidates.rs split by concern
(targets, candidates, composition/guarantees), realization.rs
pass2/
memo/ groups.rs, candidates.rs
rules/
shared_producer.rs identical expression, summary capability, top-k reuse, panes, ranks (#954)
finer_aggregate.rs rollup and compensating filters (#950)
shared_scan.rs replaces scan_labels.rs (#951)
structural.rs summary_inputs, summary_results (factor private nodes into groups)
sites.rs concrete sizes and pane grids (decision 2)
maintained_population.rs is deleted by the open population-rule PR (#681 tracks the capability). deployment_capabilities.rs leaves the crate (decision 3).
Plan (one concern per PR)
The first PR waits for #952, the #951 PR and the #954 PR to merge, because they edit these files. #572's rule holds: a move PR moves code and changes nothing else.
- Move only:
pass1/,pass2/,accuracy/function_rules.rs,pass2/rules/finer_aggregate.rs. Rename the duplicatestate_configurations.rsfiles by role. - Split deployment inputs by stage; the bundle goes to
asap-planner(decision 3). - Map (A) → (B) in a table; port what is missing into memo rules, each with a failing-first test.
- Delete (A) and switch its plan-selection callers to the memo (decision 1).
- Stage 1 runs the state expansion; plan-selection takes the expanded memo (decision 2).
- Move orchestration and the public API to
asap-planner(decision 4). This includespipeline.rs,OptimizationPass/optimize()and thePlanningModelsassembly, and switchesstage_pipeline, devtools and tests that callasap_plan_selection::optimizeto the facade. It comes with the follow-up PRs in ASAPQuery-backend and ASAPCollector (decision 5). It depends on steps 2, 4 and 5, after which plan-selection no longer calls Stage 1. - Split
per_computation_candidates.rsinsidepass1/(move only).
Acceptance for every PR:
- the same selected plans and costs on the existing integration suites;
- for move PRs, a diff that moves code and changes nothing else.
Related
#572 (crate layout by stage), #954 (shared-producer rule), #950 (finer-aggregate reuse), #951 (shared scan), #955 (raw horizon), #681 (maintained population), the design doc of #509.
Activity
[-]Reorganize Stage 1 by pass: one memo-based Pass 2, Stage 1 finishes the memo, deployment inputs split by stage[/-][+]Reorganize by stage: one memo-based Pass 2, Stage 1 finishes the memo, deployment inputs split by stage, asap-planner as the only entry point[/+]zzylol commentedon Oct 27, 2026
Step 3: mapping of the whole-workload Pass 2 to memo rules
All facts are from
origin/mainat6a22ec49(after #959), read from the code. (A) isasap_aware_cse.rs(4541 lines) plusworkload_candidates.rs(556 lines). (B) ismemo/andmemo/rules/*.Summary
rollup.rs:memo/window_candidates.rsmemo/rules/rollup.rsmemo/rules/scan_labels.rsmemo/rules/state_configurations.rsmemo/rules/topk.rsmemo/window_candidates.rsreuses (A)'s subset enumeration. For each Pass 1 candidate it callswindow_composition_pass2_candidates, which lists every non-empty subset of that candidate's pane candidates (2^n, capped atMAX_PANE_CANDIDATES = 20). This is the one place where the memo inherits (A)'s exponential enumeration and its cap.prune_query_candidates→alone_violation→stage1_candidates_of_choice(plan-selection/src/search/query_pruning.rs);LogicalCandidate::pass2(search/pipeline.rs), which production always sets toNone(pipeline.rs:281,route.rs:936,route.rs:949);candidate_labels.rs/planning_stages.rs, which labelpass2and so only label test-built candidates.plan-selection/src/lib.rs,costing/direct_profile.rs,costing/lower_bounds.rs,physical-asap-optimization/src/lib.rsand the (A) uses inmemo/state_groups.rsall sit in#[cfg(test)]modules.1. Inventory of (A)
Classes:
pass2/rules/;workload_candidates::stage1_candidates_of_choice(+ privatechoice_candidates,pass2_candidates,Pass2List,Rules)query_pruning.rsalone_violation. test:integration-tests/src/lib.rsexplicit_assignment_plansPass2Candidatepipeline.rs(LogicalCandidate::pass2, alwaysNonein production), devtoolscandidate_labels.rspass2_label,planning_stages.rsre-export. test:reuse_rules.rs. Docs:develop_docs/asap-aware-mapping-*.md,extend-asap-aware-mapping.mdStage1Candidateidentical_expression_pass2_candidates,IdenticalExpressionPass2Candidatesharing_candidates,SharingCandidate,with_shared_candidatescandidate_labels.rs. test:direct_profile.rs,planner_layering_example1–3,reuse_rules,rollup_controlled,planner/tests/summary_sharing.rs,devtools/tests/stage_pipeline.rssummary_capability_pass2_candidates,capability_sharing_candidates,SummaryCapabilityPass2Candidate,CapabilitySharingCandidate,MAX_CAPABILITY_SHARING_MEMBERSplanner_layering_example2.rswindow_composition_pass2_candidates,WindowCompositionPass2Candidate,MAX_PANE_CANDIDATESmemo/window_candidates.rslocal_pane_candidates_delta. test:memo/state_groups.rs, plan-selectionlib.rs,direct_profile.rs,lower_bounds.rs,direct_price_options_tests.rs,range_price_options_tests.rs,physical-asap-optimization/src/lib.rs, nine integration test files (deployment_capabilities,planner_layering_*,range_evaluation,sql_trailing_window,sql_two_percentiles,tumbling_pane_merge)window_composition_shared_chain_candidates,SharedPaneChainmemo/rules/rollup.rstests,tumbling_pane_merge.rspane_candidates,PaneCandidate(+window_ms)memo/window_candidates.rs, devtoolscandidate_labels.rs. test: same list aswindow_composition_pass2_candidates, plusmemo/rules/rollup.rstestswith_pane_chainmemo/rules/state_configurations.rswith_resized_buildmemo/rules/state_configurations.rs. test:memo/state_groups.rswith_union_scan_labelsmemo/rules/scan_labels.rs. test:devtools/tests/stage_pipeline.rstopk_reuse_candidates,TopKReuseCandidatememo/rules/topk.rs, devtoolscandidate_labels.rs. test:reuse_rules.rstopk_reuse_pass2_candidates,TopKReusePass2Candidaterollup_candidate_for,RollupCandidatememo/rules/rollup.rsrollup_pass2_candidates,RollupPass2Candidatememo/rules/rollup.rs(a two-root workload, one rule, so one subset). test:rollup_controlled.rsrollup_candidatescandidate_labels.rs. test:reuse_rules,rollup_controlled,sketch_rollup_planningrollup_choices,rollup_kind,may_change_rollup_candidate,MAX_REUSE_CANDIDATES2. Capability mapping
identical_expression_pass2_candidates,sharing_candidatesLogicalMemo::from_rootsinterns equal nodes as one group (memo/groups.rs);ir::csegroups.rstestidentical_sub_expressions_of_two_queries_are_one_groupwith_union_scan_labels, inPass2List::identical_expression(widened)memo/rules/scan_labels.rsadd_scan_label_candidates(callswith_union_scan_labels)state_groups.rstestsdisjoint_scan_labels_share_a_union_pane_state_with_typed_readers,general_source_groups_offer_typed_union_scan_readers,scan_label_widening_keeps_different_filters_separatesummary_capability_pass2_candidates(subsets of ≤ 20 members)memo/rules/state_configurations.rsSummaryStates,StateCovering::Size, viashared_producer.rsshared_producer_snapshot.rsshared_size_candidates_are_unchanged;state_groups.rsquantile_readers_share_concrete_state_and_retain_local_candidates,distinct_sizes_gain_compatible_shared_configurations; integrationsql_two_percentilesstate_size_configurations), so partial sharing comes from per-reader choice. No member cappane_candidates+window_composition_pass2_candidatesmemo/window_candidates.rsadd_local_window_composition→local_pane_candidates_delta, which calls the (A) functionswindow_candidates.rstestsincremental_local_widths_match_one_complete_pass,reader_grids_include_subsets_instead_of_only_full_union; integrationtumbling_pane_mergeMAX_PANE_CANDIDATEScap; when it is exceeded,Erris silently skipped (let Ok(..) else continue)push_every_shared_chaininsidewindow_composition_pass2_candidates(nested and gcd grids)state_configurations.rsStateCovering::Panes(pane_configurations_before,with_pane_chain)shared_producer_snapshot.rsshared_pane_candidates_are_unchanged;state_groups.rsdifferent_pane_grids_and_horizons_gain_explicit_newest_pane_readers,identical_local_pane_chains_share_state_without_removing_local_choicesPaneWidthDomainrollup_candidates+rollup_pass2_candidates(subsets of choices, ≤ 20)memo/rules/rollup.rsadd_rollup_candidates(pairs of groups; callsrollup_candidate_for+rollup_pass2_candidateswith one rule);add_window_rollup_candidates;filtered_group.rs(being replaced by #952)state_groups.rsexact_rollup_exposes_shared_finalized_values,sketch_rollup_exposes_shared_finer_state,grouped_window_rollup_exposes_one_finer_chain; integrationproduction_rollup_workloadstopk_reuse_candidates+topk_reuse_pass2_candidates(subsets)memo/rules/topk.rsTopKRankingsviashared_producer.rsstate_groups.rstopk_outputs_share_explicit_exact_ranking;shared_producer_snapshot.rstop-k testspass2_candidatesapplies the rules in sequence to one assignmentstate_groups.rs:102–109: scan labels → summary inputs → top-k → filtered group → rollup → window rollup → summary results → state configurations); combinations arise from per-group choicesstate_groups.rsmultiple_private_states_can_share_only_the_common_producer(B) only:
summary_inputs.rsandsummary_results.rs(factoring private nodes into groups),filtered_group.rs.Integration tests that exercise (A), not (B):
explicit_assignment_plans/explicit_rule_plans:reuse_rules,rollup_controlled,sketch_rollup_planning,promql_to_post_asap;planner_layering_example1–3and the window tests above.Their assertions are about rule semantics: a reused plan answers like the original. Before deletion they need to read the candidate from the memo instead.
3. Callers outside (A) and their replacement
plan-selection/src/search/query_pruning.rsalone_violation(fromprune_query_candidates←route.rs:767)stage1_candidates_of_choiceon a one-query inventory: the composed candidate and its single-query Pass 2 candidates ("the rules a single query can apply, such as window composition"), each through Stage 2 and the checksLogicalMemo::from_roots→try_populate_pass1→add_local_window_composition, plus the memo rules if needed) and check the candidates of the root group's closure for thatchoice. The pruning question is "does any implementation of this choice pass alone", so the memo's local candidates are the right setsearch/pipeline.rsLogicalCandidate::pass2: Option<Pass2Candidate>Noneintegration-tests/src/lib.rs,reuse_rules.rs)candidate_labels.rs(pass2_label,same_pane_chain),planning_stages.rsre-exportpass2;PaneCandidateCandidateOriginmemo/window_candidates.rslocal_pane_candidates_deltapane_candidates,window_composition_pass2_candidates(all subsets)pane_candidatesand the rewrite of one chosen set (in_panes,push_every_shared_chain) intopass2/rules/. Decide whether local candidates need every subset or one per pane candidate (behavior change: own PR, failing-first test)memo/rules/rollup.rsadd_rollup_candidatesrollup_candidate_for,rollup_pass2_candidateswith one rulerollup_pass2_candidatesto the single-rule rewrite it is used as. #952 also edits this file, so this waits for itmemo/rules/scan_labels.rswith_union_scan_labelsmemo/rules/state_configurations.rswith_pane_chain,with_resized_buildmemo/rules/topk.rstopk_reuse_candidatesintegration-tests/src/lib.rsexplicit_assignment_plansand its testsstage1_candidates_of_choiceCandidateOrigin), or delete the tests whose behavior the memo-path tests already coverlib.rs,direct_profile.rs,lower_bounds.rs,direct_price_options_tests.rs,range_price_options_tests.rs,physical-asap-optimization/src/lib.rs,memo/state_groups.rs, nine integration test files,planner/tests/summary_sharing.rs,devtools/tests/stage_pipeline.rsdevelop_docs/asap-aware-mapping-architecture.md,asap-aware-mapping-contracts.md,extend-asap-aware-mapping.md4. Proposed PRs for steps 3–4 (one concern each)
reuse_rules,rollup_controlled,sketch_rollup_planning,planner_layering_example1–3andtumbling_pane_merge: the memo search's selected cost is ≤ the cheapest (A) plan fromexplicit_assignment_plans. Any failure is a missing port and becomes its own failing-first PR.with_pane_chain,with_resized_build,with_union_scan_labels,topk_reuse_candidates,rollup_candidate_for,pane_candidatesand the one-set pane rewrite intopass2/rules/next to their memo rule. Switch the test fixtures' imports. Best done after step 1 (refactor(optimizer): move Stage 1 modules into pass1/ and pass2/ (#958 step 1) #960) and after feat(optimizer): reuse a finer aggregate through a compensating filter and a rollup of values or states #952 merges, since feat(optimizer): reuse a finer aggregate through a compensating filter and a rollup of values or states #952 editsrollup.rs.stage1_candidates_of_choiceinalone_violationwith a one-query memo, using a failing-first test that compares pruned sets on the existing pruning tests. This is the only production use of (A).reuse_rules,rollup_controlled,sketch_rollup_planning,promql_to_post_asapandexplicit_assignment_plans.workload_candidates.rs, the*_pass2_candidatesenumerations,rollup_choices, theMAX_*caps,LogicalCandidate::pass2, devtoolspass2_label, and the docs sections.pass2/rules/<file>).window_candidates.rsstops listing all 2^n subsets per candidate if per-group choice already covers them. This needs a failing-first or equal-cost test, so it is kept out of the deletion.zzylol commentedon Oct 27, 2026
Correction to the step 3 comment above. It said every whole-workload capability has a memo implementation. That came from reading code only. Running both paths on the same workloads (PR #975) shows three cases where the memo search's plan costs more:
Limit(10)nodes instead of one sharedLimit(10) ▸ Sort(#979).Identical expression, union scan labels, local panes, shared pane chains and Example 1 match.
Step 4 (deleting the whole-workload path) waits for #979 and for the shared-sizes case. Each is pinned as an ignored test in
memo_covers_whole_workload_pass2.rs.test(optimizer): check the memo search against the whole-workload Pas…
refactor(optimizer): move Stage 1 modules into pass1/ and pass2/
refactor(optimizer): move Stage 1 modules into pass1/ and pass2/ (#960)
zzylol commentedon Oct 29, 2026
Stage-boundary types and renames decided 2026-10-29: #1047 (Pass 2 ends with shared summary-state groups; the Stage 2 inventory moves out of plan-selection).
zzylol commentedon Oct 29, 2026
Decided 2026-10-29 in #1047: stages and passes never call each other's functions; each reads only the previous step's output type, and the
asap-plannerfacade runs the stages in order (including pane-width rounds and rebuilding the selected DAG). This overrides decision 4's "plan selection may call Stage 2 per candidate". The code moves out ofasap-plan-selectionare tracked here.zzylol commentedon Oct 29, 2026
Crate organization across all crates is now tracked in #1054, decided 2026-10-29:
asap_types::stages.asap-accuracyandasap-cost.cargo metadatagraph check, and visibility rules per crate.#1054 extends this issue's decisions 3–4. This issue's Stage 1-internal layout and steps still apply inside
asap-logical-asap-optimization.refactor: each stage reads only its deployment capability; physical n…