Skip to content

feat(ir): define compatible logical summary merges - #560

Draft
zzylol wants to merge 6 commits into
feat/summary-coverage-contractfrom
stack/528-02b-merge-structure
Draft

zzylol wants to merge 6 commits into
feat/summary-coverage-contractfrom
stack/528-02b-merge-structure

Conversation

@zzylol

@zzylol zzylol commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Rebased on main d4869a7 (DF 54).

Why

First scope in the revised implementation order for #511/#509, above merged #535/#536. Extracts structural summary merge support from #555 before logical graph infrastructure.

Before this PR: constructing ASAPOp::SummaryMerge fails because the operation is reserved.

After this PR: two structurally compatible KLL states can form a typed logical merge with no execution-phase assignment. This validates schema compatibility and conservatively proves disjoint coverage using #567's SummaryCoverage. Merge inputs must be nonempty, carry state with exactly one state field, and have identical family/parameter/grouping schemas. Empty, raw and mismatched states fail.

Schema/result-kind/state identity, coverage derivation and regression tests are included. Runtime execution, materialization and derived timing belong to the later physical scopes. Subtract/delete remain reserved.

Key code interface

SummaryMerge is a logical ASAP operator whose inputs are existing operator nodes producing partial state:

pub enum ASAPOp {
    // Other variants omitted.
    SummaryMerge {
        children: Vec<Rc<OperatorNode>>,
    },
}

It uses the same node construction and validation APIs as other operators:

let merged = OperatorNode::new_shared(Operator::ASAP(
    ASAPOp::SummaryMerge {
        children: vec![pane_a, pane_b],
    },
))?;
merged.validate_structure()?;

Here pane_a and pane_b are Rc<OperatorNode> state producers. new_shared returns Result<Rc<OperatorNode>, SchemaDerivationError> and derives the output schema/result kind. validate_structure checks the complete reachable DAG, including the producers. No timing assignment is required.

The key ASAPOp methods are:

pub fn merged_coverage(&self) -> Result<SummaryCoverage, SchemaDerivationError>;
pub fn validate_inputs(&self) -> Result<(), SchemaDerivationError>;
pub fn output_schema(&self) -> Result<Schema, SchemaDerivationError>;
pub fn output_kind(&self) -> OperatorResultKind;
pub fn produced_state(&self) -> Option<&FieldDataType>;

For SummaryMerge:

  • validate_inputs enforces the compatibility rules below.
  • output_schema validates inputs, then clones the first input's complete schema.
  • output_kind is OperatorResultKind::State.
  • produced_state returns the non-plain field type from the first input. This is a metadata accessor, not a runtime merge or an independent validation step.

Coverage before and after merging

Why Schema equality alone is not enough is explained in #567. This PR uses #567's SummaryCoverage. Coverage is required on summary nodes, SummaryMerge included. Every merge input must have coverage, new derives the output coverage with SummaryCoverage::merge_disjoint, and validate_structure rejects a retained output coverage that differs from that union. The schema is unchanged.

Inputs (same KLL schema (job: Utf8, state: KLL{k=200})) Output coverage Result
[0,1) + [1,2) [0,2) accepted
no time bounds (tabular) + any region of the same population — Coverage(PossibleOverlap)
[0,1) + [2,3) [0,1) ∪ [2,3), gap kept accepted
[0,2) + [1,3) — Coverage(PossibleOverlap)
region=us + region=eu, same time two regions accepted
region=us + tier=premium — Coverage(PossibleOverlap)
us×[0,1) + eu×[1,2) two regions, never {us,eu}×[0,2) accepted
any input with coverage = None — Coverage(UnknownInput)
different update expression or reduction — rejected (summary_update mismatch)
output coverage edited after construction — Coverage(MergeOutputMismatch)

Coverage records only time and population, so the producers carry the rest: every input's OperatorNode::summary_update() (the SummaryAgg input and reduction, or those shared by a nested SummaryMerge) must be present and equal.

crates/types/tests/summary_coverage_examples.rs builds each example from #567's body and docs/design_docs/proposals/asap-primitive-schema.md as a real Scan → SummaryAgg(KLL k=200, by job) → SummaryMerge plan. It covers time, population, joint regions, tabular sources without time, source/update mismatch, required coverage, and the trusted-population case that #570 will close. It asserts the exact merged regions and that the schema is unchanged.

Grouped merge keeps groups apart: job='api' and job='worker' stay separate states. Checking that the merged coverage contains the window or population a query asks for is left to the composition rule. A later SummaryEstimate is the readout boundary that turns state into a plain value.

Requirements for merging two summaries

This PR enforces these structural requirements:

Requirement Check
At least one input An empty children list is rejected. The API is n-ary; a single compatible state is structurally allowed.
Every input produces state Each child has result_kind == State; raw relations/vectors are rejected.
Exactly one state field The first schema contains exactly one non-Plain field. Identical schemas enforce this on every other input too. Plain grouping fields may accompany it.
Same committed state type Complete schema equality requires the same family, algorithm/kind, parameters and any grouping layout carried by FieldDataType. KLL k=200 and k=300, or KLL and CMS, cannot merge here.
Same grouping/output layout Field positions, plain grouping-field types, names, qualifiers and nullability must match.
Same schema metadata time_index, unique_keys and closed must match as well. Matching only the state algorithm is insufficient.
Structurally valid producers Whole-DAG validate_structure checks each producer's own contracts.

Compatibility is deliberately strict: even differently named but otherwise equivalent schemas need an explicit normalization before this interface accepts them.

These checks establish typed structural compatibility and provably disjoint declared coverage. They do not prove that the inputs cover the intended population/window, that a runtime implements the family's merge operation, or that the merged result meets an accuracy requirement. Those checks belong to the logical composition rule and subsequent physical planning/selection. For example, overlapping frequency panes must not silently double-count observations; matching schemas alone cannot establish correct coverage.

When SummaryMerge can be used

During logical planning: a composition rule can construct SummaryMerge when it needs to combine compatible partial summary states—for example, several tumbling-window KLL panes answering one larger query window, or compatible partition summaries feeding a coarser computation. The planner must establish the intended input coverage and grouping semantics. The node can be constructed and structurally validated before choosing materialization.

During execution: a physical implementation can merge the state contents once its inputs are available and the selected family/runtime supports the operation. Timing follows the materialization choice in #509. For example, a query can merge previously ingested/stored panes, or merge states rebuilt for that query. #560 itself adds no runtime kernel, storage, retention, window generation or execution-phase policy, and does not imply runtime support for every declared state family.

Source at this PR's head: asap.rs and the structural regression in crates/types/tests/summary_merge_structure.rs.

Validation: reproduced the original reserved-operator failure; the focused regression passes after the fix. The complete first-five tip passes 1,961 workspace tests/doctests, formatting, and workspace/all-target/all-feature Clippy with warnings denied.


Revised logical foundation 2/6 · Base: #567 · Next: #537 · Tracker: #528

Revised review order: #567 → #560 → #537 → #539 → #540 → #561.

🤖 Generated with Claude Code

@zzylol
zzylol changed the base branch from main to feat/summary-coverage-contract October 3, 2026 16:07
@zzylol
zzylol force-pushed the stack/528-02b-merge-structure branch 6 times, most recently from 3ab6be7 to 77628c8 Compare October 3, 2026 17:29
@zzylol
zzylol force-pushed the stack/528-02b-merge-structure branch from 77628c8 to 02a6a1b Compare October 3, 2026 17:40
@zzylol
zzylol force-pushed the stack/528-02b-merge-structure branch 2 times, most recently from 60a9e4f to cb00197 Compare October 3, 2026 19:45
@zzylol
zzylol marked this pull request as draft October 3, 2026 20:02
@zzylol
zzylol force-pushed the stack/528-02b-merge-structure branch from cb00197 to 700838b Compare October 3, 2026 20:48
zzylol and others added 5 commits October 5, 2026 04:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…coverage examples

SummaryCoverage no longer repeats input/reduction, so SummaryMerge compares
them through OperatorNode::summary_update. summary_coverage_examples.rs builds
each example in docs/develop_docs/summary-coverage.md as a SummaryAgg ->
SummaryMerge plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… doc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the stack/528-02b-merge-structure branch from 700838b to f2b7b3f Compare October 5, 2026 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant