Skip to content

feat: InstallablePlan describes every stored summary through its SummaryCoverage (#781) - #787

Merged
zzylol merged 2 commits into
mainfrom
feat/general-stored-outputs
Oct 21, 2026
Merged

zzylol merged 2 commits into
mainfrom
feat/general-stored-outputs

Conversation

@zzylol

@zzylol zzylol commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Why

A runtime's summary store should hold every summary the plan stores. InstallablePlan could describe only one kind: a node built directly from one pane of raw samples (StoredOutput.pane). plan_installable refused anything else with StoredOutputWithoutPane. For example, a sliding-window merge of stored panes kept across evaluations failed with Installable(StoredOutputWithoutPane { node_id: 15 }). So a runtime could store only panes, and every other summary the plan stores was refused at install time.

What

  • Each StoredOutput says what the stored summary is, what its build reads, when it is built, and how long it is kept:
    • coverage is the IR's SummaryCoverage: a stable definition_id and the selection (value sets, value ranges, and time relative to the evaluation time).
    • inputs are the raw-sample windows and the stored outputs (by node id) it reads.
    • built_at and retention_ms are as before.
  • plan_installable lists every stored node (MatAtIngestion or MatAtQueryKeptAcrossEvaluations) in dependency order. It refuses only what a runtime cannot build: a stored node that reads a source outside a raw-sample window, a stored node with no summary coverage, and one the executor does not compile with its inputs bound.
  • pane is dropped. It was the single raw-sample window of a pane's inputs, and its time is the time part of the selection. Its consumers are this repo's tests, which are migrated here, and ASAPQuery-backend, which is migrating to the general form.
  • Docs. The installable-plan docs (the installable module doc) say how a runtime builds an output from stored inputs.

How

  • Coverage.

    • A pane's coverage is SummaryCoverage::derive.
    • A merge's selection is the union of its inputs' selections under their shared definition, following the SummaryMerge rules.
    • A summary built from exactly one stored output, with no selection of its own (a rollup to a coarser grouping, or a summary across series built from per-series ones), takes that input's selection. Its definition is its own sub-DAG with the input replaced by the input's definition. It therefore gets a definition of its own, and rollups of equal inputs share it.
    • Anything else keeps its own derived coverage, conservatively.
  • definition_id. A 128-bit FNV-1a hash of the definition's operators, output kinds and schemas, children first. It ignores timing, guarantee and materialization, so it agrees with the equality a merge requires. A summary store can key instances by (definition id, concrete selection).

  • Inputs. Walking down from a stored node, each stored node and each raw-sample TimeRange it reaches is an input. A Scan reached without a TimeRange is refused. Stored outputs are ordered children first, so a pane-only plan keeps its node order.

  • Executability. For each stored output, the executor's own compile runs with its inputs bound as sources, the way a runtime runs it. A failure is refused with the executor's reason.

  • Retention.

    • A pane takes Stage 3's chain retention, as before.
    • Otherwise, an output is kept as far back as its data reaches (its windows' and stored inputs' end_offset_ms + width_ms). An output built from stored outputs adds its readers' longest range-evaluation span.
    • Then each stored input is kept as long as the outputs built from it need it: their retention when they are built in a query run, since a range request may build one for its oldest step, or else as far back as their data reaches.
    • validate refuses an output kept for less than its data reaches.
  • Shapes Stage 3 does not select today. With the built-in models, none of the three shapes in the issue is selected:

    • the rollup is shared within an evaluation (MatAtQuery);
    • the cross-series maximum is not materialized;
    • Stage 2 never offers a SummaryMerge as a materialization candidate.

    The tests force these shapes through UserInput::pass. A test pass runs Stages 1 to 3, re-times the selected plan with the chosen nodes stored, and splits nodes shared across timings as Stage 2 does. It then goes through plan_installable unchanged.

Key code interfaces added / modified

crates/planner/src/installable.rs

/// One stored summary of the installable DAG. The type stays; its fields change.
pub struct StoredOutput {
    /// The node whose output is stored.
    pub node_id: PhysicalASAPNodeId, // unchanged
    /// IngestionRun (once its inputs are complete) or QueryRun (first evaluation that reads it).
    pub built_at: StoredOutputBuild, // unchanged
    /// What the stored state is: its summary coverage.
    pub coverage: StoredCoverage, // new
    /// The raw-sample windows and stored outputs its build reads.
    pub inputs: StoredOutputInputs, // new
    /// How far back from the newest evaluation it stays available (rules above).
    pub retention_ms: u64, // unchanged; now also set for outputs built from stored outputs
    // pub pane: RawSampleWindow, // removed: it is inputs.raw_sample_windows[0] of a pane
}

/// A stored output's SummaryCoverage. New type.
pub struct StoredCoverage {
    /// SummaryCoverage::definition_id: equal for every node that computes the same thing.
    pub definition_id: String, // new
    /// The rows that went in; a merge's is its inputs' union.
    pub selection: Vec<SelectionBox>, // new
}

/// What a stored output's build reads, each bound as a source. New type.
pub struct StoredOutputInputs {
    /// Windows of raw samples it reads directly, in node order.
    pub raw_sample_windows: Vec<RawSampleWindow>, // new
    /// Stored outputs it reads, by node id, each listed before it.
    pub stored_outputs: Vec<PhysicalASAPNodeId>, // new
}

/// Why an installable plan could not be built. The type stays; variants change.
pub enum InstallablePlanError {
    /// Internal bug or bad plan input: timings do not hold.
    Timing(ExecutionDataStateError), // unchanged
    /// Internal bug: a pane chain does not expand.
    PaneChainExpansion(SchemaDerivationError), // unchanged
    /// Bad input: the DAG is not valid.
    InvalidDag(PhysicalASAPDAGValidationError), // unchanged
    /// Bad input: queries do not bind the roots.
    QueriesDoNotMatchRoots { roots: usize }, // unchanged
    /// Unsatisfiable: a standalone scalar query.
    UnsupportedQueryRoot { entry_index: usize }, // unchanged
    /// Bad input: stored outputs or windows do not match the DAG.
    WindowsOrStoredOutputsDoNotMatchDag, // unchanged
    /// Bad input: kept for less than its data reaches back.
    StoredOutputRetentionTooShort { node_id: PhysicalASAPNodeId }, // unchanged; was "than its pane"
    /// Internal bug: retention for an unknown chain.
    PaneRetentionOfUnknownChain { node_id: PhysicalASAPNodeId }, // unchanged
    // StoredOutputWithoutPane { node_id }, // removed
    /// Unsatisfiable: the build reads a source Scan outside a raw-sample window (e.g. a whole SQL table).
    StoredOutputReadsUnwindowedSource { node_id: PhysicalASAPNodeId }, // new
    /// Unsatisfiable: the stored node is not a summary state with a coverage.
    StoredOutputWithoutCoverage { node_id: PhysicalASAPNodeId, reason: String }, // new
    /// Unsatisfiable: the executor does not compile the build with its inputs bound.
    StoredOutputNotExecutable { node_id: PhysicalASAPNodeId, reason: String }, // new
    /// Unsatisfiable: a raw-sample window shape a runtime cannot feed.
    UnsupportedRawSampleWindow { time_range_node_id: PhysicalASAPNodeId, reason: &'static str }, // unchanged
}

crates/types/src/ir/properties/summary_coverage.rs

impl SummaryCoverage {
    /// Stable hex id of `definition`; equal definitions get equal ids on any node.
    pub fn definition_id(&self) -> String; // new
}
/// Now Serialize/Deserialize: columns as [{column, constraint}], bounds as
/// {"included"|"excluded": v} or "unbounded".
pub struct SelectionBox { pub columns: BTreeMap<ColumnIdentity, Constraint>, pub relative_time: Option<(Bound<i64>, Bound<i64>)> } // fields unchanged

crates/types/src/ir/physical_export.rs

impl PhysicalASAPDAG {
    /// Every node as untimed IR by id, the rebuild query_time_reference already used; validates first.
    pub fn operator_nodes(&self) -> Result<HashMap<PhysicalASAPNodeId, Rc<OperatorNode>>, PhysicalASAPDAGValidationError>; // new
}

Usage (the test a_kept_sliding_window_merge_is_listed_and_built):

// A dashboard panel: max_over_time(m[5m]) every minute, answered as the last hour
// at 1-minute steps, over 6 ingesting series; the deployment keeps query-time results.
let workload = exact_workload(&["max_over_time(m[5m])"],
    r#", "range_evaluation": {"step_ms": 60000, "span_ms": 3600000}"#);
// Stage 3 keeps five 1-minute panes; the test pass also keeps their merge.
let plan_json = forced_plan_json(&workload, &capabilities, merges_kept).await;
let plan: InstallablePlan = serde_json::from_str(&plan_json)?;
// stored_outputs, in dependency order:
//   5 panes: inputs.raw_sample_windows = [1-minute window at offset 0..4 min],
//            coverage = { definition_id: D, selection: time (-(i+1) min, -i min] },
//            built_at QueryRun, retention_ms 3,900,000
//   merge:   inputs.stored_outputs = [the 5 panes], no raw windows,
//            coverage = { definition_id: D, selection: time (-5 min, 0] },
//            built_at QueryRun, retention_ms 300,000 + 3,600,000
// The runtime builds each output once its inputs are complete: the executor compiles
// the DAG for the node with each window's rows bound at its TimeRange and each
// stored input's state at its node id. It keys states by (D, concrete selection).
let first = run_data_plane(&plan_json, &samples, EVALUATION_MS, &mut store);  // builds 6
let next = run_data_plane(&plan_json, &samples, EVALUATION_MS + 60_000, &mut store); // builds 2
// Both answer as plan.query_time_reference().

Before this PR

  • A stored output that was not built from one raw pane was refused with StoredOutputWithoutPane, for example the kept merge above.
  • A rollup materialized from finer stored sums was listed with the finer sums' raw pane as if it read raw samples, so a runtime would rebuild it from raw samples.
  • The plan said nothing about what a stored state is, so a runtime had to invent its own identity to share states.

After this PR

  • All three shapes in the issue are listed with their inputs, coverage, build phase and retention, and built from stored inputs. The answers equal the query-time reference.
  • A runtime keys stored states by (definition id, selection made concrete).
  • Pane-only plans list the same nodes in the same order, with the same windows (now inputs.raw_sample_windows[0]), built_at and retention_ms. The new fields are added and pane is gone.

Verification

Tests written first.

  • Refusal on main. plan_installable of the kept-merge plan failed on main with StoredOutputWithoutPane { node_id: 15 }.
  • planner/tests/runtime_install.rs. Each test checks inputs, coverage, phase and retention, then builds every output in the listed order from stored inputs with the executor. The answers equal query_time_reference.
    • a_rollup_from_finer_stored_sums_is_listed_and_built: sum by (job) from stored sum by (job, instance), both at ingestion. The selection is (−60s, 0] for both, the definitions differ, and 2 builds are needed.
    • a_summary_across_series_from_per_series_summaries_is_listed_and_built: max by (job) from stored per-series maxima.
    • a_kept_sliding_window_merge_is_listed_and_built: as in the usage above. A second evaluation one minute later builds only the newest pane and the new merge.
    • panes_of_an_hour_share_one_definition_with_distinct_times: the six 10-minute panes of quantile_over_time(0.99, latency_ms[60m]) share one definition id. Their time selections are (−(i+1)·10m, −i·10m] and their windows are unchanged.
    • The existing pane-plan tests pass unchanged on the generalized runtime harness, which keys states by (definition id, concrete selection): 6 panes built for one hour, 7 for an hour and its offset twin, and 5 then 1 for kept panes. So do per_sample_updater, columnar_batches and integration-tests/runtime_handoff.
  • types/tests/summary_coverage.rs.
    • equal_definitions_get_equal_ids: equal across selections and timings, different for another column, family or source.
    • panes_of_an_hour_share_one_definition_with_distinct_times: the same property, plus their merge.
    • selections_round_trip_through_json.

Commands. cargo test --workspace --locked at the head: exit 0, 150 test binaries, 0 failures. Following the current speed-first policy, fmt and clippy were not run.

Limitations.

  • Conservative coverage. It follows the existing SummaryCoverage rules:
    • An instant selector's TimeRange and its label matchers stay in the definition, with no time in the selection. Such a state's concrete identity also needs the evaluation time; the test harness adds it to the key.
    • A summary built from several stored outputs that is not a merge keeps its own derived coverage, with the inputs' windows inside the definition.
  • Merges are not materialized by Stage 2. Stage 2 still never offers a SummaryMerge as a materialization candidate, and Stage 3 does not select the forced rollup shapes under the built-in prices. This PR makes them installable; making Stage 2 and Stage 3 choose them is separate work.
  • Review. Not reviewed independently.

Closes #781

…age (#781)

- SummaryCoverage::definition_id: a 128-bit FNV-1a hash, in hex, of the
  definition sub-DAG's operators, output kinds and schemas, children
  first, without timing, guarantee or materialization. Equal definitions
  (the equality a merge requires) get equal ids on any node.
- SelectionBox, ColumnIdentity and Constraint serialize: columns as a list
  of {column, constraint}, bounds as {"included"|"excluded": v} or
  "unbounded".
- PhysicalASAPDAG::operator_nodes: every node of a DAG as IR by id, the
  rebuild query_time_reference already used, so a plan file's node
  coverage can be derived.
Tests: equal ids across selections and timings, different ids for other
computations, six 10-minute panes of an hour share one id with distinct
time selections, JSON round trip.
StoredOutput no longer requires a pane of raw samples. Each stored output
now gives:
- coverage: its definition id and selection (SummaryCoverage), a merge's
  being the union of its inputs' selections under their definition, and
  a summary built from one stored output with no selection of its own (a
  rollup to a coarser grouping, or across series) taking that input's
  selection under a definition of its own;
- inputs: the raw-sample windows and the stored outputs (by node id) its
  build reads;
- built_at and retention_ms as before. An output built from stored
  outputs is kept for how far back its data reaches plus its readers'
  longest range-evaluation span; each stored input is kept as long as
  the outputs built from it need it.
plan_installable lists every stored node in dependency order and refuses
only what a runtime cannot build: a stored node that reads a source
outside a raw-sample window, has no summary coverage, or that the
executor does not compile with its inputs bound. The `pane` field is
removed: it is the one raw-sample window of a pane's inputs, and its time
is the coverage's selection.
The module doc says how a runtime builds an output from stored inputs.
Tests: a rollup from finer stored sums, a maximum across series from
stored per-series maxima and a kept sliding-window merge of stored panes,
each listed with its inputs, coverage, phase and retention and built in
order from stored inputs to the query-time reference's answers; existing
pane plans keep their windows, order and retention.
@zzylol zzylol added enhancement New feature or request rollup Fine-to-coarse aggregation and summary reuse schema Schema, type, key, and nullability contracts window Window functions, frames, panes, or temporal semantics labels Oct 20, 2026
@zzylol
zzylol merged commit 4111e62 into main Oct 21, 2026
2 of 3 checks passed
GordonYuanyc added a commit that referenced this pull request Oct 21, 2026
Planner main fails clippy (-D warnings) and one runtime_install test since
#787, #789, #795 and the joint search commit:
- summary_coverage.rs: a Window alias for the relative time window
  (type_complexity).
- workload_candidates.rs: an array instead of vec! (useless_vec).
- query_candidates.rs: OperatorNode::reachable instead of a closure.
- stage1_to_stage3.rs: allow too_many_arguments on
  search_stage2_and_stage3, as lib.rs already does once.
- joint_search_equivalence.rs: struct initializer for the settings, and
  slice::from_ref instead of a clone.
- runtime_install.rs: the merged-pane DDSketch candidate of the p99 avg
  workload is now P3-m1 (was P5-m1); every other assertion of that test
  passes unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol pushed a commit that referenced this pull request Oct 21, 2026
refactor: follow asap_sketchlib's UnivMon, Hydra and HLL renames

UnivMon fields rows/cols/layers/total_weight/layer_heaps, new/clear/
update_weighted/estimate_*, OwnedDataInput, UNIVMON_MAX_LAYERS; Hydra
rows/cols and HydraCounter::CountMinSketch; HLL update_with_hash.

refactor!: HLL, KLL, DDSketch, Count-Min and KMV kernels on native asap_sketchlib types

The portable HllSketch, KllSketch, DdSketch and CountMinSketch are gone
from asap_sketchlib. HLL is a classic HyperLogLogImpl at each precision
4..=18 (register types from impl_hll_registers!); KLL is Kll::with_k;
DDSketch the native DdSketch (get_value_at_quantile, get_count); plain
Count-Min a regular-path CountMinF64 that adds every weight, zero and
negative ones included; KMV the native Kmv. Memory charges come from
Kll::memory_bytes, Kmv::memory_bytes and CountMinSketch::memory_bytes.
The DDSketch ratio tests interpolate PromQL quantiles from the sketch's
order statistics.

refactor!: weighted frequency on asap_sketchlib's f64 Count-Min and Count Sketch

asap_sketchlib drops WeightedFrequency. The Planner kernel keeps its API
over CountMinSketchWithHeap / CountSketchWithHeap (capacity > 0) or a
plain CountMinSketch / CountSketch (capacity 0), all Vector2D<f64> on the
regular path. An identity tuple is one TupleKey, hashed and retained as
bytes. Planner checks what sketchlib does not: finite weights (and
nonnegative for Count-Min), finite Float64 identities, an odd Count
Sketch depth, and a total weight magnitude within f64. Top-k rows are the
heap's keys at their current estimate, descending, ties by key bytes.

refactor!: HydraKLL on asap_sketchlib's Hydra with KLL cells (#644)

HydraKllSketch is gone from asap_sketchlib. The kernel is a one-column
Hydra over HydraCounter::Kll, read by Hydra's median of the cells' KLL
quantiles, and charged the cells' Kll::memory_bytes.

refactor: box the weighted frequency state held as a template

Its heap makes it far larger than the other group-state kinds.

test: fixtures that still exercise native KLL memory and DDSketch quantiles

Kll allocates its item buffer as items arrive, so each stored-pane state
takes 1,100 samples to hold its full buffer (about 11 kB) again; with one
sample the merge fit the 4 MiB limit. DdSketch reads its minimum and
maximum exactly, so the cross-zero ratio fixture interpolates interior
order statistics: [-2, -1, 1.011, 2] instead of [-1, 1.011].

chore!: pin asap_sketchlib to main (de15d66)

asap_sketchlib main, with ProjectASAP/asap_sketchlib#242 merged. UnivMon
builds any row count its 128-bit key hash covers, so the UnivMon shape
tests probe that budget instead of a 20-row limit.

Merge remote-tracking branch 'origin/main' into chore/asap-sketchlib-main
fix: cap UnivMon at the 20 rows its ASAPv1 state encodes

sketchlib main's UnivMon::check_shape accepts any row count within the
128-bit key hash, but its ASAPv1 encoder still refuses more than
MATRIX_MAX_ROWS rows. Keep the executor's builds and the Planner's
hash_independent at 20 rows, as on the old pin, so every UnivMon state
the executor builds can be stored.
fix: count the KLL and weighted frequency structs once in memory charges

Kll::memory_bytes and the heap sketches' memory_bytes already include
their own struct; adding size_of::<Self>() on top counted it twice.
chore: pin asap_sketchlib to release 0.4.0 (eb04e7f)

eb04e7f is the 0.4.0 release commit on top of de15d66; the library code
is the same, only the version, CHANGELOG and README change.
Merge remote-tracking branch 'origin/main' into chore/asap-sketchlib-main

# Conflicts:
#	crates/plan-selection/src/lib.rs
fix: clippy on main's new code and the renumbered P99 pane candidate

Planner main fails clippy (-D warnings) and one runtime_install test since
#787, #789, #795 and the joint search commit:
- summary_coverage.rs: a Window alias for the relative time window
  (type_complexity).
- workload_candidates.rs: an array instead of vec! (useless_vec).
- query_candidates.rs: OperatorNode::reachable instead of a closure.
- stage1_to_stage3.rs: allow too_many_arguments on
  search_stage2_and_stage3, as lib.rs already does once.
- joint_search_equivalence.rs: struct initializer for the settings, and
  slice::from_ref instead of a clone.
- runtime_install.rs: the merged-pane DDSketch candidate of the p99 avg
  workload is now P3-m1 (was P5-m1); every other assertion of that test
  passes unchanged.
@zzylol
zzylol deleted the feat/general-stored-outputs branch November 3, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rollup Fine-to-coarse aggregation and summary reuse schema Schema, type, key, and nullability contracts window Window functions, frames, panes, or temporal semantics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InstallablePlan stores only raw-sample panes; describe every stored summary

1 participant