Repository navigation
[Refactor] Crate organization by stage: per-crate functionality, cross-stage code into asap-accuracy / asap-cost / asap-types, enforced crate boundaries, public vs private #1054
Description
Example
Run stage_pipeline on one query, quantile_over_time(0.9, latency[5m]), evaluated every minute with max_error 0.01. On main (cef05d2), the crate that should only select a plan (asap-plan-selection, Stage 3) runs every other stage itself:
| Step | Stage it belongs to | Where it runs today |
|---|---|---|
| hash-cons the roots | Stage 0 | plan-selection/src/search/pipeline.rs:197 → LogicalMemo::from_roots |
| per-computation candidates | Stage 1 Pass 1 | pipeline.rs:198 → generate_per_computation_candidates_with_memo; route.rs:872 → try_populate_pass1 |
| whole-result δ-sized candidates | Stage 1 Pass 1 | plan-selection/src/search/candidates/accuracy_configurations.rs:8 |
| pane candidates and shared summary states | Stage 1 Pass 2 | route.rs:889 → extend_local_window_composition; route.rs:906 → expand_concrete_summary_state_groups_before |
| physical candidates and materializations | Stage 2 | plan-selection/src/search/candidates/{inventory,configurations}.rs |
| realize each plan the search proposes | Stage 2 | search/refinement/planner.rs:1050 → physical.rs:82 → optimize_physical_asap_chosen |
| resize sketches for the failure probability and recompile the DAG | Stage 1 sizing + Stage 2 | plan-selection/src/lib.rs:2243 sized_for_failure_probability |
| check accuracy | Stage 3, but using Pass 1 code | lib.rs:275-284 imports 8 guarantee functions from pass1::per_computation_candidates |
The other crates have the same problem:
asap-typesholds algorithms used by one stage only.asap-physical-asap-optimization(Stage 2) depends on the Stage 1 crate for one field and one re-export.- Almost every module of
asap-logical-asap-optimizationispub.
What should happen instead:
- Each stage crate does only its stage.
- Stage crates never depend on each other.
- Shared models (accuracy, cost) and shared types live in non-stage crates.
- Each crate makes public only what other crates use.
Concrete maintenance problem
Current crate graph ([dependencies], not dev-dependencies)
flowchart TD
planner["asap-planner<br/>(facade)"]
fsql["frontend-sql"]; fpromql["frontend-promql"]; fmql["frontend-metricsql"]; fcommon["frontend-common"]
ps["asap-plan-selection<br/>(Stage 3, plus S1/S2/facade code)"]
phys["asap-physical-asap-optimization<br/>(Stage 2)"]
logical["asap-logical-asap-optimization<br/>(S0 hash-cons, Pass 1, Pass 2, accuracy)"]
exec["asap-executor"]
types["asap-types<br/>(IR, workload, plus single-stage algorithms)"]
planner --> fsql & fpromql & fmql
planner --> ps
planner --> logical
planner -- "✗ #1016" --> exec
fsql & fpromql & fmql --> fcommon
fcommon --> types
ps -- "✗ X1, X3: calls Stage 2" --> phys
ps -- "✗ X2, X4: runs Pass 1/2, accuracy" --> logical
phys -- "✗ one field + one re-export" --> logical
logical --> types
phys --> types
ps --> types
exec --> types
classDef stage fill:#e8f0fe,stroke:#4a6fa5
classDef bad fill:#fdecea,stroke:#c0392b
class fsql,fpromql,fmql,fcommon,logical,phys stage
class ps bad
Edges marked ✗ are the ones this issue removes or redirects. Not drawn: devtools and integration-tests (they depend on almost everything); dev-dependencies (executor → logical, plan-selection); the optional logical → frontend-promql under test-support; external crates (asap_sketchlib, highs, parsers).
The graph has no cycles. The problem is which edges exist and what each crate holds:
| Edge | Why it exists |
|---|---|
| plan-selection → logical | Stage 3 runs Pass 1 and Pass 2 and uses Pass 1's accuracy functions (table above) |
| plan-selection → physical | Stage 3 realizes each proposed plan through Stage 2 (refinement/planner.rs:1050), and calls summary_materialization_eligibility while pricing (costing/direct_profile.rs:196) |
| physical → logical | only DeploymentCapabilities (one field, query_time_results_retained, physical/src/lib.rs:308) and pass1::realization::ranked_column, which merely re-exports asap_types::ir::schema::topk::ranked_column (realization.rs:172) |
| planner → executor | the executor's supported-summary list and the compile checks (installable.rs:380-390, :505-600); see #1016 |
What each crate holds today (production lines)
| Crate | Lines | Content, and what does not belong |
|---|---|---|
asap-types |
~15.5k | IR (operators, schema, scalars, guarantees, timing) and workload types. Single-stage algorithms: • ir/cse.rs (Stage 0 hash-consing)• ir/canonicalize.rs (frontends only)• physical/summary_window.rs plan_pane_phase / validate_pane_coverage and timing.rs split_shared_by_timing (Stage 2 only)• workload/resources/* (Stage 3 only)Deployment items: materialized_view.rs, SupportedSummary, agg_has_exact_per_entity_kernel |
asap-frontend-*, frontend-common |
~10k | Stage 0: query text → QueryRoot |
asap-logical-asap-optimization |
~15.4k | LogicalMemo hash-consing, Pass 1, Pass 2 (memo rules plus the whole-workload path), accuracy/.Of pass1/per_computation_candidates.rs (3,879 lines), only about 600 generate candidates (498-1110). About 2,000 lines are the shared accuracy model:• guarantee propagation (1615-3031) • guarantee helpers that Stage 3 calls (1328-1516) • sized (3661-3711)The rest is whole-workload composition and substitution. Pass 1 depends on Pass 2 ( pcc:613-625, :1690).DeploymentCapabilities is defined here but read by Stages 1, 2 and 3 |
asap-physical-asap-optimization |
~1.2k | Stage 2, plus the whole-workload optimize_physical_asap |
asap-plan-selection |
~23.4k | About 60% is Stage 3 (checks, pricing, cost models, MILP, report). The rest is: • Stage 2 catalog construction ( search/candidates/{inventory,configurations,physical,reconstruction}.rs, ~1.3k lines)• Pass 1 sizing and pruning ( query_pruning.rs, accuracy_configurations.rs, lib.rs:2243-2364)• facade orchestration ( search/pipeline.rs; search/route.rs except :954-1194)• the public API ( OptimizationPass, optimize, PlanningModels in search/mod.rs)• accuracy glue ( accuracy.rs, lib.rs:2651-2936) |
asap-planner |
~1.1k | Frontend entry, InstallablePlan. The selected plan's DAG is exported four times: Stage 2 (physical/src/lib.rs:664), Stage 3 after resizing (plan-selection/src/lib.rs:2260), and installable_plan_of before and after pane expansion (installable.rs:606, :622) |
Data structures and functions used across stages
| Item | Defined in | Used by |
|---|---|---|
IR: OperatorNode, QueryRoot, schema, AccuracyTarget, AccuracyGuarantee, Materialization, ExecutionTiming; workload types |
types | all stages |
| memo and stage-boundary types (#1047) | logical, plan-selection | two or more stages each |
accuracy model: accuracy/*, the guarantee propagation and sizing in per_computation_candidates.rs, AccuracyModel (plan-selection/src/accuracy.rs) |
logical, plan-selection | Pass 1, Pass 2, Stage 3 |
bounds derived from DataWorkload: rows, group count G, source rates (plan-selection/src/lib.rs:1852-2242, :2937-3211) |
plan-selection | pricing and accuracy sizing |
DeploymentCapabilities |
logical | Pass 1, Stage 2, Stage 3, planner |
CandidateGenerationTimeout |
logical | all stages |
PhysicalASAPDAG, compile_physical_asap_* |
types | Stage 2, Stage 3 (again), planner (twice), executor |
Visibility
asap-logical-asap-optimization:pub mod accuracy / pass1 / pass2and every submodule are public. About 40pubitems ofasap_aware_cseare used outside the crate only by devtools.asap-plan-selection:PlanSelectorand its methods,CalibrationFit,cost::analyticalandcost::operator_statistics(~20 types), and six cost constants arepubwith no user outside the crate.pub use search as pass;(lib.rs:297) gives one module two public paths.
asap-types: single-stage functions are public crate-wide.
Before and after structure
Decided 2026-10-29.
Target crate graph
flowchart TD
planner["asap-planner (facade)<br/>runs: frontends → S0 → P1 → per round: P2 → S2 → S3 → build plan → InstallablePlan<br/>splits deployment inputs; injects the Stage 2 realizer into Stage 3"]
fe["asap-frontend-*<br/>(Stage 0 lowering)"]
logical["asap-logical-asap-optimization<br/>(S0 hash-cons, Pass 1 incl. all sizing, Pass 2)"]
phys["asap-physical-asap-optimization<br/>(Stage 2, realizer)"]
ps["asap-plan-selection<br/>(Stage 3 only)"]
acc["asap-accuracy (new)<br/>guarantees, estimators, sizing, AccuracyModel"]
cost["asap-cost (new)<br/>resource usage, cost models 1/2/3, CostInputs"]
types["asap-types<br/>IR, workload, workload-derived bounds,<br/>stage-boundary types (asap_types::stages)"]
planner --> fe & logical & phys & ps
fe --> types
logical --> acc
logical -.->|early pruning| cost
phys -.-> acc
phys -.->|early pruning| cost
ps --> acc
ps --> cost
logical & phys & ps --> types
acc --> types
cost --> types
classDef stage fill:#e8f0fe,stroke:#4a6fa5
classDef shared fill:#e9f7ef,stroke:#2e8b57
class fe,logical,phys,ps stage
class acc,cost shared
Blue boxes are stage crates; there is no edge between any two of them. Green boxes are shared models; any stage may depend on them, and dashed edges are allowed but not needed yet. planner also depends on asap-accuracy, asap-cost and asap-types to assemble deployment inputs (not drawn).
Rules:
- Stage crates never depend on each other. The stage crates are
frontend-*,logical,physicalandplan-selection. A stage reads the previous stage's output type fromasap-typesand calls no other stage. Any stage may useasap-accuracyandasap-cost. asap-accuracyandasap-costdepend only onasap-types. Neither depends on the other: the workload-derived bounds both need live inasap-types.- The facade is the only crate that depends on every stage. devtools and integration-tests are unrestricted.
- CI checks the graph. A test reads
cargo metadataand fails on any[dependencies]edge outside the allowed list.
Per-crate functionality after the change
| Crate | Owns | Receives from |
|---|---|---|
asap-types |
IR, workload facts, workload-derived bounds (rows, group count G, source rates), the stage-boundary types of #1047 in asap_types::stages (DagOfLogicalMemoGroups, PerComputationLogicalMemoCandidates, LogicalASAPMemoCandidates, PhysicalASAPMemoCandidates, PrunedCandidate), CandidateGenerationTimeout |
plan-selection (bounds), logical (memo types, timeout). Gives away its single-stage algorithms (below) |
asap-accuracy (new) |
Guarantee composition and propagation; estimators (target → sketch parameters); accuracy budget split; δ union bound; size_for_all_readers; merged-error checks; AccuracyModel, AccuracyEvidenceProvider, OperandDomain, satisfies |
logical accuracy/* and the accuracy parts of per_computation_candidates.rs; plan-selection accuracy.rs and lib.rs:2651-2936 |
asap-cost (new) |
(1) Resource-usage estimation: analytical model, operator statistics, sketch byte models, measured operation costs, calibration. (2) Cost models: cost model 1/2/3, ResourceUnitCost, Pareto, and CostInputs (was PlanSelectionSettings). It gives the usage for a given timing and materialization, and lower/upper bounds over all materializations, so earlier stages can prune by cost |
plan-selection cost/*, lib.rs pricing (3212-4591) and byte models (4873-5184); types workload/resources/* and EXACT_ACCUMULATOR_STATE_BYTES |
asap-frontend-* |
Stage 0 lowering | types canonicalize, resolve_group_keys_promql |
asap-logical-asap-optimization |
Stage 0 hash-consing; Pass 1, including all sizing (budget shares, whole-result δ, G); Pass 2 memo rules, including pane widths and shared summary-state sizes |
types cse.rs; plan-selection accuracy_configurations.rs, resize_for_failure_probability, Pass 1 pruning |
asap-physical-asap-optimization |
Stage 2: physical candidates, allowed timing and materializations (recording may_keep_across_evaluations), pane phase and coverage, building the selected plan; the realizer used by Stage 3 |
plan-selection search/candidates/{inventory,configurations,physical,reconstruction}.rs, per_entity_aggregate_violation (guess: executor legality); types summary_window.rs, split_shared_by_timing |
asap-plan-selection |
Stage 3 only: checks (accuracy, latency, raw-data retention), MILP search, search-specific cost machinery (lower bounds, proposals, direct profiles, MILP price options), Selection |
— |
asap-planner |
Orchestration, including pane-width rounds (#1048); public API (OptimizationPass, optimize, check_contract); deployment-input assembly and dispatch; InstallablePlan |
plan-selection search/pipeline.rs, the round loop of route.rs, search/mod.rs |
asap-devtools |
stage_pipeline, viewer data, fuzzing, and the reference pricer: the whole-workload optimize_physical_asap + select_plan, kept to check the MILP's choice against enumeration (--price-enumerated, planner_fuzz; #1002, #1004) |
— |
Deployment items in asap-types (materialized_view.rs, SupportedSummary, the executor kernel list) and the planner → executor edge are decided in #1016.
Resolving the four cross-stage calls
| # | Today | After |
|---|---|---|
| X1 | Stage 3 calls Stage 2 for each proposed plan (refinement/planner.rs:1050): the MILP proposes with local legality and cost lower bounds; an evaluator realizes the plan through Stage 2, checks it and prices it exactly, and turns the result into a cut. The catalog does not carry combination-level legality (timing copies, pane phase), combined accuracy, or consumer-dependent cost |
Short term: the trait AssignmentRealizer lives in asap_types::stages. The Stage 2 crate implements it, and the facade passes that implementation to Stage 3. There is no crate edge, and physicalization stays lazy. Longer term (under consideration): Stage 2 precomputes the combination-level work, so realizing a plan is only assembling it from the catalog plus IR validation, and the realizer goes away. First measure how often and why Stage 2 rejects an assignment today |
| X2 | Stage 3 resizes sketches for δ and recompiles the DAG (lib.rs:2243-2364) |
All sizing is in Stage 1 (below); Stage 3 only checks |
| X3 | Pricing calls summary_materialization_eligibility (direct_profile.rs:196) |
Stage 2 records may_keep_across_evaluations on each configuration |
| X4 | Stage 3 imports 8 Pass 1 guarantee functions and the HLL/UnivMon estimators | They move to asap-accuracy |
Sizing: every size is decided in Stage 1, Stage 3 only checks
Sizes are decided in four places today:
- Pass 1 per budget share (
groups.rs:204-215→compose_alone→sized). add_scoped_candidatesin plan-selection.- Pass 2 shared sizes (
size_for_all_readers,shared_size, state groups). - Stage 3 δ resizing (
lib.rs:2243).
After the change:
| Decision | Stage | Function (asap-accuracy unless noted) |
|---|---|---|
Whole-window build size: ε, budget share n, δ split over G |
Pass 1 (absorbs places 2 and 4) | estimators, budget split, δ union bound |
Pane width w and count n = W / w, chain sharing |
Pass 2 window rule; the facade gives each round's width domain (#1048) | — (structural rule in logical) |
| Sketch size inside panes = the whole-window build's size | Pass 2 (inherited, never resized) | merged_error: Exact and AsIfBuiltOverUnion keep the guarantee at equal parameters; Approximate gives none |
| Shared build or chain size = the strictest reader | Pass 2 state groups | size_for_all_readers |
| Pane phase and alignment | Stage 2 | plan_pane_phase, validate_pane_coverage |
| Composed accuracy meets the target | Stage 3 (check only) | satisfies, guarantee composition |
Memory of n panes = n × sketch bytes |
Stage 3 pricing | asap-cost byte models |
Docs to fix: planner-layering.md ("Summary nodes are created unsized") and per_computation_candidates.rs:22 should say that Pass 1 creates sized candidates, one per accuracy-budget split. Smaller sketches per pane are out of scope; they need a sketchlib bound that depends on the number of merges.
Pruning
#1047's rule becomes: a stage removes only what its own knowledge proves invalid, or what is provably dominated under the shared cost model.
- Dominated means the pruned candidate's cost lower bound is ≥ the kept candidate's upper bound, for the same output, a guarantee at least as strong, and the same sharing.
- This is the memo design's "keep the cheapest per sharing signature".
Visibility rules
- Public: a crate's entry functions, its boundary and IR types, and what the facade and devtools use.
pub(crate): used by other modules of the same crate.- Private: used only in its own module.
#[cfg(test)]or atest-supportfeature: used only by tests.- No
pub modthat leaks internals, and no second public path to the same item.
| Crate | Change |
|---|---|
| logical | pass1, pass2, accuracy modules become pub(crate), with named pub use for the entry points and output types. asap_aware_cse items become pub(crate) (devtools labels move to the memo's origin). computation_candidates_for_intent, ClassicHllConfidence and DEFAULT_DELTA become private. Remove the realization::ranked_column re-export |
| plan-selection | PlanSelector (+ methods), CalibrationFit, cost::analytical, cost::operator_statistics and the six cost constants become pub(crate) (most move to asap-cost). Keep one path (pass) instead of pub use search as pass. confirmed_weights and with_confirmed_weights go under test-support |
| physical | optimize_physical_asap_assigned goes under test-support; physical_candidate_count and materializes_any go under #[cfg(test)] |
| types | Single-stage functions move to their stage. These become pub(crate): structural_hash, is_shareable_when_equal, SummaryCoverage::{derive, definition_id}, optimistic_floor, approximate_layer_count, expand_pane_chains, whole_window_build, compile_physical_asap_dag_with_node_ids, *_before, flatten_before, view_window |
Dead and test-only public code found by this review is listed in #1046.
Why the proposed structure is minimally complex
There are two new crates, each shared by several stages:
asap-accuracy: Pass 1, Pass 2, Stage 3.asap-cost: Stage 3 now; earlier stages for cost-based pruning later.
Everything else is moving code to the crate whose stage uses it, and narrowing visibility. Removing the edges between stage crates turns the "no calls across stages" rule (#1047) from a review convention into a compile error.
Existing unit and end-to-end protection
- Planner snapshots in
devtools/tests/corpus_snapshots/must stay unchanged in every PR. cargo test --workspace.- The new
cargo metadatagraph test.
Compatibility or migration impact
- Import paths change with no compatibility aliases (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 decision 5). Externally, ASAPQuery-backend uses only
asap_planner(InstallablePlan,PlanningWorkload,StoredOutputBuild,PlanError), checked in itsorigin/main. PlanSelectionSettingsbecomesCostInputs, including thestage_pipeline --plan-selection-settingsfile.
Suggested order (one concern per PR; move PRs only move code):
- Create
asap-accuracy; move the accuracy model (removes X4). - Create
asap-cost; move resource-usage estimation and the cost models. - Move the single-stage algorithms out of
asap-types, and the workload-derived bounds into it. - Move the boundary types into
asap_types::stages([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3–P5). - Move the Stage 2 catalog into
physical([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P5); fix X3. Drop physical → logical. - Move all sizing into Pass 1 (removes X2), then the realizer trait (removes X1).
- Move orchestration and the public API to the planner (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 step 6, [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P7). Drop plan-selection → logical / physical.
- Visibility pass per crate, then the CI graph test.
Related
- 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: Stage 1 crate reorganization; its decisions 3–4 are extended here.
- [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047: stage-boundary types; [Refactor] Pane-width rounds without cross-stage calls: rerun Pass 2 per round, or pass an incremental Pass 2 state #1048: pane-width rounds.
- [Refactor] Rename memo-named caches and the memo types by stage #1050: memo and cache renames.
- Remove dead and test-only code: row-reference oracle, old_docs, test-only demand model, batch bridge, memo snapshot #1046: dead code.
- Deployment interface review: InstallablePlan, materialized views, DataStore and the executor #1016: deployment items and the executor dependency.
- Stage 1 Pass 2 review: ASAP-aware CSE rules over memo groups and summary-state groups #1013: Pass 2 window rules.
- memo-milp: size summaries once (final config per reader set and summary count; Stage 3 never resizes) #929: sizing once per reader set.
- Tracking: stage-by-stage review of ASAPPlanner — functionality, concepts, abstractions, interfaces #1010: stage-by-stage review.
Activity
refactor: move single-stage algorithms out of asap-types (#1054) (#1060)
refactor: create asap-cost with resource-usage estimation and cost mo…
zzylol commentedon Oct 29, 2026
Progress 2026-10-29:
asap-cost: done in refactor: create asap-cost with resource-usage estimation and cost models (#1054) #1061.PlanSelectionSettingsis renamedCostInputs;asap-costdepends only onasap-types.asap-types: done in refactor: move single-stage algorithms out of asap-types (#1054) #1060.canonicalizemoved to frontend-common, the pane layout and coverage code to physical, and CSE to logical.split_shared_by_timingstays inasap-typesbecause it reads private parts oftiming.rs. Theworkload/resourcesmove was done in refactor: create asap-cost with resource-usage estimation and cost models (#1054) #1061.asap-accuracy, after [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3asap_types::stages, with [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3–P5docs: every summary size is decided in Stage 1 (#1054) (#1062)
zzylol commentedon Oct 29, 2026
Decided 2026-10-29: the realizer is option D. A trait
AssignmentRealizerlives inasap_types::stages; the Stage 2 crate implements it, and the facade passes that implementation to Stage 3. The search algorithm is unchanged. The MILP algorithm itself may change later (an efficiency discussion is pending). Option B, where Stage 2 precomputes combination-level work so that realizing is only assembly, stays a candidate. Data point: in a profile of the 6 memo-example workloads, Stage 2 rejected no assignments in 5 of them; ratio-of-quantiles had 46 rejections, allno_accuracy_guarantee.refactor: narrow asap-types visibility to what other crates use (#1054)…
refactor: create asap-accuracy with the accuracy model (#1054, part 1) (
refactor: Stage 2 records whether a pane chain may be kept; pricing r…
refactor: split plan-selection lib.rs into modules by concern (#1054) (…
refactor: move data-workload bounds into asap-types (#1054) (#1083)
refactor: move guarantee propagation and sizing into asap-accuracy (#…
refactor: Stage 1 → Stage 2 boundary types in asap_types::stages (#1047…
refactor: narrow physical, cost, accuracy and types visibility (#1054 …