Skip to content

docs: QuerySpec definition + batched-queries DAG-reuse example - #2

Closed
zzylol wants to merge 2 commits into
mainfrom
docs/workload-plan-and-queryspec
Closed

zzylol wants to merge 2 commits into
mainfrom
docs/workload-plan-and-queryspec

Conversation

@zzylol

@zzylol zzylol commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Expand the QuerySpec placeholder in core::workload into a full struct (id, query+language, time_range, accuracy/latency/dollars SLAs, optional deployment_model hint) plus supporting QueryLanguage and AccuracyTarget enums.
  • Add a second end-to-end example tracing three related PromQL queries through L3 (workload-level CSE producing fan-in), L4 (MergeKllSketches collapsing q1+q2 onto one KLL state, q3 stays exact), and L5 (colored multi-root DAG emission with edge-side memory dropping from 3× to 1× of scan/window).
  • Introduce WorkloadPlan { bindings, roots } as the workload-level multi-root container so QueryExpr stays single-root and cross-query fan-in lives one level above per-query IR. Within-query CTE fan-in keeps using the existing LetBinding/Ref mechanism unchanged.

Test plan

  • Render docs/design.md and confirm both new sub-sections read cleanly
  • Cross-check QuerySpec fields against existing references throughout the doc (HTTP POST /plan, OpAMP capability-miss callback, replan cycles)
  • Confirm the batched example's IR shapes are consistent with §6 design rule 5 (DAG fan-in) and §6 core::cost (workload_cost / ReusedComponent)

🤖 Generated with Claude Code

Expand the QuerySpec placeholder in core::workload into a full struct
(id, query+language, time_range, accuracy/latency/dollars SLAs, optional
deployment_model routing hint), plus supporting QueryLanguage and
AccuracyTarget enums.

Add a second end-to-end example walking three related PromQL queries
through L3 (workload-level CSE producing fan-in), L4 (MergeKllSketches
collapsing two quantile sketches onto one KLL state), and L5 (colored
multi-root DAG emission). Introduce WorkloadPlan { bindings, roots } as
the workload-level multi-root container so QueryExpr stays single-root
and cross-query fan-in lives one level above per-query IR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol marked this pull request as draft May 5, 2026 21:35
Add `shape: QueryShape` (OneShot / Streaming / Periodic{every}) for how
a query is evaluated, and `data: DataShape` (Batch / AppendOnlyStream /
Mutable / Mixed) for the data feeding it. Document the planner's
acceptance matrix across the cross-product (e.g. Streaming over Batch
is rejected at L1; Mutable disables sketch binding pending
retraction-aware operators).

Note that per-leaf data shape rides on Source at L3; the QuerySpec
field is a workload-level summary, with Mixed deferring to per-leaf
detail during L4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 27, 2026
…#2, #3)

Locks behavior introduced by the recent fixes, +5 tests (125→130):

- promql_conformance: nested unary negation propagates rejection
  (`a - -b`, `sum(-x)`), not just top-level; and count→Cardinality threads
  the AccuracyTarget (Exact stays exact, Epsilon carried) — pins review #2.
- sql_lowering: a qualified WHERE on the *duplicated* join column
  (`WHERE hosts.service = ...`) binds to the qualified position (4), not the
  first `service` (1) — extends the #7 disambiguation past the join key.
- error.rs: `UnsupportedFeature` Display is language-neutral (no "PromQL") —
  pins the #3 fix without depending on DataFusion plan shapes.
- schema.rs: `Column.table` deserializes to `None` when the key is absent
  (the `#[serde(default)]` backward-compat contract), and a qualified column
  round-trips.

Full workspace green (130 tests), clippy -D warnings + fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 27, 2026
#2, scalar)

The two scalar IRs were byte-identical 13-variant enums differing only in the
column-reference type, with duplicated `conjuncts`/`disjuncts`/`columns_referenced`
— maintained twice. Collapse them into a single `Expr<C>`:

- `type L2Expr = Expr<ColumnRef>` (name-based, front-end-emitted)
- `type L3Expr = Expr<ColumnId>`  (positional, resolved)

The helpers are now one generic `impl<C> Expr<C>` (`columns_referenced` returns
`Vec<&C>`, unifying the L2 `&ColumnRef` and L3 owned-`ColumnId` variants — the
sole caller is the L2 Binder). Because the aliases preserve variant
construction/pattern syntax (`L2Expr::Compare { .. }` etc.), every front end,
the converter's `resolve_expr` map, and all tests compile unchanged.

This is the clean, low-risk half of the QueryExpr<C> parameterization the review
recommended starting with. No behavior change. Full suite green (133), clippy +
fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Jul 22, 2026
…yer diagrams

- Filled in the "implementation refers to [TODO]" placeholder in L4.
- Moved Bind (name resolution) into L2's own bullets and canonicalize
  (cross-language shape unification) into L3's own bullets, instead of
  a separate transition section — Bind is why L2 stays "per-language"
  even though both front ends share one Rust type; canonicalize is
  where cross-language convergence actually happens (issue #34).
- Added a Mermaid diagram to each L1-L5 section showing its actual
  data flow, verified against the real code paths.
- Fixed the Glossary's "Bind" lookup table to point at sections that
  still exist, and marked Implementation/implement_tree/Match/Bind #2
  as currently undocumented in this file (their old transition-section
  homes were removed) rather than leaving dangling links.
@milindsrivastava1997

Copy link
Copy Markdown
Collaborator

Closing as stale.

zzylol added a commit that referenced this pull request Aug 24, 2026
#251, part of #33)

## Why

`asap-aware-mapping`'s output boundary is `PlanSpace` — every candidate
`TargetSubDAG` replacement plus its cost, meant for a downstream
DAG-visualization consumer. It does not own "commit to one final,
physically-materialized, shared post-ASAP DAG for a whole workload" —
that's a downstream deployment's job (picking *which* sketch and *where*
to place it are a joint decision only a deployment can see the full
picture for; this crate pre-deciding one in isolation, with no real
consumer of that single materialized answer inside this crate, is out
of scope). `bind.rs`'s `implement_workload`/`implement_workload_with`
(keep first/cost-preferred candidate per node, memoized by `Rc` identity
across roots so pre-ASAP CSE sharing survives into a single physical
binding) was exactly that out-of-scope "commit to one answer" step, so
it's deleted.

## What

1. Deleted `bind.rs` entirely (no replacement file, no rename). Removed
   `pub mod bind;` and its re-exports from `lib.rs`. `ImplementError`
   moved into `replacement.rs` (`pub`, since it's part of what a
   `ReplacementStrategy` implementor's construction path can realistically
   fail with) — still needed by `realize_child`/`keep_pre_asap`/the
   child-recursion path. `bind.rs`'s own test suite (16 tests exercising
   `select_and_bind`'s/`construct_summary_agg`'s schema-derivation
   behavior across various `AggIntent` shapes) moved into
   `replacement.rs`'s existing test module, adapted to call
   `realize_child` directly — this crate's own production logic, not
   `bind.rs`'s deleted orchestration. The one test specific to
   `implement_workload_with`'s CSE-decline memoization behavior has no
   equivalent left to test and was dropped (that comparison is already
   covered by `cost_sorted_orders_shared_subtree_candidates_by_cse_share_decision`
   and `cost_model.rs`'s own `cse_share_decision` tests).

   Rewrote `lib.rs`'s `## Status` section: two capabilities —
   `replacement::ReplacementStrategy` (per-target, exhaustive) and
   `replacement::search_workload`/`PlanSpace::cost_sorted` (workload-wide,
   every candidate + cost) — plus an explicit statement that picking/
   materializing one final answer is a downstream deployment's job.
   Rewrote the `## Terminology` table: dropped the `implement_workload`
   row; the `Bind #2` (downstream placement) row now also covers "which
   candidate to commit to", since that's where the deleted step's
   behavior would live if a deployment still wants it.

2. Renamed the `pub(crate)` single-target rank-and-take-first helper
   `select_and_bind` → `realize_child` (word "bind" is retired from this
   crate's vocabulary — see `lib.rs`'s Terminology section). Also renamed
   its sibling `bind_one` (the representative-node helper `cse_preference`
   uses for a `CostModel::cse_share_decision` comparison) → `realize_one`
   for the same reason. Updated every call site and doc comment; behavior
   unchanged (rank via `SketchFamilyStrategy`, take first, `Rewrite` is
   unreachable, `None` falls back to `keep_pre_asap`).

3. Renamed "site" → "target"/`TargetSubDAG` throughout the merged
   search-engine content in `replacement.rs`: `discover_sites` →
   `discover_targets`, `discover_new_descendant_sites` →
   `discover_new_descendant_targets`, local variables/doc prose
   (`MemoGroup`'s "one distinct site", the "Where 'for site in
   plan.bindable_sites()' comes from" section heading, `PlanSpace`/
   dedup/termination prose). The quoted historical pseudocode block
   itself (`for site in plan.bindable_sites()`) stays verbatim, framed
   explicitly as a quote. (This item was landed independently by a
   concurrent session as commit c562f3c while this work was in
   progress on the same shared branch — reconciled here via
   `git reset --hard origin/...` + manual reapply per the shared-branch
   protocol; this commit only carries items 1/2/4/5 on top of it.)

4. Rewrote `crates/integration-tests/tests/cse.rs`: it called the now-
   deleted `implement_workload` and asserted two bound `SummaryNode`s were
   `Rc::ptr_eq` — a workload-level "one materialized answer" claim that no
   longer has an API to make. Rewrote its three tests to call
   `replacement::search_workload` and assert on the resulting `PlanSpace`
   instead: the shared subtree collapses onto one `MemoGroup`/one shared
   `Rc<QueryExpr>`, mirroring `replacement.rs`'s own
   `shared_aggregate_across_two_roots_gets_both_strategies_candidates`
   test through the crate's public API.

5. Fixed every other `asap_aware_mapping::bind::*` import (devtools'
   `show_post_asap_ir`, `frontend-promql`'s corpus test,
   `integration-tests`' `l4_binding`/`l4_binding_sql`) to
   `asap_aware_mapping::replacement::*`, and updated stale doc references
   in `asap-types::pre_asap::cse`/`mod.rs` that pointed at
   `asap_aware_mapping::implement_workload`/`bind::implement_workload_with`.

## Verification

$ cargo build --workspace --all-targets && cargo test --workspace \
    && cargo fmt --all -- --check \
    && cargo clippy --workspace --all-targets --all-features -- -D warnings
(all clean; asap-aware-mapping: 68 passed, 0 failed; cse.rs: 3 passed)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Aug 24, 2026
Restructure the flat 19-section developer guide into the three-part
structure the doc owner asked for: Part 1 - Code Architecture, Part 2
- Interfaces and Definitions, Part 3 - How to Add X, Y, Z (each ending
in how to verify). Content is moved, not rewritten:

Part 1 (Mental model first, per doc-owner follow-up, then a new
whole-PR architecture diagram, then "How the current pieces fit
together"):
- old #1 Mental model -> Part 1 #1
- new: whole-PR architecture diagram (TargetSubDAG's two entry points
  through ReplacementStrategy, PlanSpace/cost_sorted, explanation.rs,
  to a downstream consumer) -> Part 1 #2
- old #3 How the current pieces fit together -> Part 1 #3

Part 2:
- old Terminology's "Implementation" definition merged into the
  Glossary as one more entry (### Implementation), next to
  ReplacementStrategy
- old #2 Glossary -> Part 2 #1 (plus the merged Implementation entry
  and old #10 Matcher, retitled to match glossary-entry style)
- old #10 Matcher (implementation.rs) -> ### Matcher inside the
  Glossary; implementation.rs no longer exists, so the stale title
  is fixed
- old #19's definitional content (ReplacementExplanation/
  ExplanationKind shapes, node_hash, why there's no ExplanationRule
  trait, location-text ownership) -> Part 2 #2

Part 3:
- old #4, #5, #6, #7, #13, #14 -> Part 3 #1, Adding a new
  ReplacementStrategy (ending in Testing a new strategy)
- old #8, #9, #15 -> Part 3 #2, Adding or customizing a CostModel
  (ending in Testing a new cost model)
- old #12 -> Part 3 #3, Adding a new sketch algorithm, with its
  stale implementation.rs/binder references fixed to replacement.rs/
  construct_summary vocabulary, plus a new "Verifying a new sketch
  algorithm" close grounded in the existing coverage-matrix tests
- old #11, #16, #17, #18 -> Part 3 #4-#7 (capstone + closing
  reference material); #18's extension-map table's implementation.rs
  row fixed to replacement.rs
- old #19's "Using it"/"Adding a new kind" content -> Part 3 #8,
  Using and extending explanation.rs

cargo build --workspace --all-targets is clean (docs-only change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Oct 14, 2026
… directly

The identical-expression rule tried all 2^g subsets of the sharing groups
and dropped those that repeat a candidate. With nested groups most
subsets repeat (14 groups: 16 384 subsets for 130 candidates). Sharing a
set of groups also shares the groups it leaves as one node; that closure
maps each subset to its candidate, and the distinct candidates are its
closed sets. They are now enumerated directly (Next Closure: one closure
per step and group), then put in the same binary-counter order, so the
output is unchanged. Twenty nested groups give their 20 candidates
quickly; a brute-force test checks the same candidates on small cases.

Sharing groups are computed once per candidate, and a Pass 2 candidate
names its shared groups by kind and number (Scan #1, TimeRange #2), so
repeated kinds stay distinguishable. The viewer fixtures are regenerated.
zzylol added a commit that referenced this pull request Oct 14, 2026
… directly

The identical-expression rule tried all 2^g subsets of the sharing groups
and dropped those that repeat a candidate. With nested groups most
subsets repeat (14 groups: 16 384 subsets for 130 candidates). Sharing a
set of groups also shares the groups it leaves as one node; that closure
maps each subset to its candidate, and the distinct candidates are its
closed sets. They are now enumerated directly (Next Closure: one closure
per step and group), then put in the same binary-counter order, so the
output is unchanged. Twenty nested groups give their 20 candidates
quickly; a brute-force test checks the same candidates on small cases.

Sharing groups are computed once per candidate, and a Pass 2 candidate
names its shared groups by kind and number (Scan #1, TimeRange #2), so
repeated kinds stay distinguishable. The viewer fixtures are regenerated.
zzylol added a commit that referenced this pull request Oct 14, 2026
…-expression top-k heaps, counter-sample weights, per-second plan-selection cost (#692)

fix(logical-asap-optimization): weight Top-K heap sketches by the ranked value

Pass 1's Top-K sketch update weighted every heap by the sample value. Over
SQL rows that column is the one named `value`, or else the only numeric
column: an Int64 key made the heap candidates fail to compose, and a key
named `value` would have weighted the heap by the key.

The update now reads the column the top-k ranks (the aggregate's measure
over grouped rows, the sample value over per-series rows), the same column
exact top-k sorts by. `ranked_column` moves from Stage 2 into the shared
realization helpers so both stages use it.
feat(logical-asap-optimization): Pass 2 identical-expression rule as Stage 3 candidates

Stage 1 Pass 2 now applies the identical-expression rule of Algorithm 2.
For each composed Pass 1 candidate it finds the sharing groups (two or
more nodes with identical input and computation semantics that are legal
to share) and adds one candidate per non-empty subset of the groups,
keeping the independent candidate. A subset that leaves out a group that
the other shared groups already merge gives the same candidate as the
larger subset, so it is listed once. Nothing is capped. The variants are
ordinary candidates for Stage 2 and Stage 3.

PromQL rows that carry the full series identity now declare
(identity, timestamp) as a unique key, which makes scans and range
selectors legal to share.

Plan selection treats totals that differ only by floating-point rounding
as a tie and keeps the earlier candidate. A shared NotMaterialized input
is charged once per consumer, so it ties with the independent plan.

Example 1 gets 9 logical candidates (3 Pass 1 x (1 + 2 Pass 2)) and
Example 3a gets 972. stage_pipeline's --max-combinations still counts
Pass 1 combinations; its document lists every Stage 1 candidate. The
Example 1 and scalar-queries viewer fixtures are regenerated.
refactor(logical-asap-optimization): move the keyed-additive heap rule into the realization helpers

Pass 1's whole-expression heap reuses the legacy search's keyed-additive
rule, so the rule, its result types and schema_column_ref move from
replacement.rs into realization.rs, which both use. Pure move.
feat(logical-asap-optimization): whole-expression top-k heaps and counter-sample weights

Pass 1 gains a whole-expression candidate kind. A top-k over a per-item
sum or count that is itself a target (one consumer, no filter or HAVING,
realizable by the keyed-additive rule) has its Count-Min and CountSketch
heap candidates replaced by whole-expression ones: one heap over the
inner aggregate's input, keyed by the ranked item and weighted by the
summed value. It absorbs the inner target, which is then not computed.
The outer top-k's partition keys are moved onto the input the heap reads.
Example 1's Q2 gets exactly three candidates: exact, Count-Min + heap and
CountSketch + heap. Enumeration skips choices that differ only in an
absorbed target, and PerComputationCandidates::combinations counts the
same set.

Count-Min needs non-negative weights. Pass 1 does not read the data
workload: when a heap's weights are raw samples of metric M, or plain sums
of them, it records the open requirement WeightDomain::SamplesOfMetric{M}.
Rate, other arithmetic and other functions end it, and no type is inferred
from a `_total` name. DataWorkload gains metric_types (declared Prometheus
types). Plan selection admits Count-Min over such weights only when M is
declared a counter; an unconfirmed requirement counts as unmet. The
planner pipeline's selected plan carries the confirmed requirement as the
proof NonNegativeWeightProof::CounterSamples, since the executor reads no
data workload.

Example 1 declares http_requests_total a counter, so its Count-Min heap
is now rejected only because a top-k output has no guarantee; without the
declaration it is rejected on its weights. Raw-sample precompute now
checks the whole-expression heaps. The Example 1 viewer fixture is
regenerated.
feat(plan-selection): price candidates per second of wall time

Plan selection now prices a candidate per second, so queries with
different recurrences add up, and takes each query's recurrence:

- A NotMaterialized node is charged once per consumer at the sum of their
  rates (a shared NotMaterialized input still costs what reading it
  separately does).
- A MatAtQuery node is computed once at the combined rate of the queries
  reaching it; queries at the same interval run together.
- A MatAtIngestion node is priced over one second of arriving rows, once
  per second.
- One-off, scheduled and unknown queries are spread over horizon_s.

ResourceCalibration gains horizon_s (default 3600 s) and
cost_per_retained_byte_second, the price of state kept between
computations. Every materialized node's kept state is charged per byte per
second: twice its output at ingestion time (the window being built and
the completed one), once at query time. Memory held only during a query
gets no per-second charge. select_plan, run_stage1_to_stage3 and
run_stage1_to_stage3_from_pass1 take the recurrences; the planner
pipeline and stage_pipeline read them from the workload. The cost model
is described in the stage3_plan_selection module docs. The viewer
fixtures are regenerated.
fix(plan-selection): charge memory only as kept state per second

Plan selection no longer charges a computation for the memory it holds
while it runs (#682's per-computation peak-memory term, including the
growing exact state of a query-time aggregate). Memory is charged only as
state a materialized node keeps between computations: kept bytes times
cost_per_retained_byte_second. The kept state is the node's output, or
the values an exact aggregate keeps per group when that is more, so a kept
exact quantile or distinct count is still sized by what it keeps. Peak
capacity belongs to cost model 2 (#683). Every term is now a per-second
cost; the per-computation memory coefficient is ignored.

The tests that compared exact state against sketches now keep the state
at query time, where its size is charged.
feat(plan-selection): bind the sign of confirmed counter samples

An exact sum or mean of relative-error values keeps the relative bound
only over non-negative values. When the folded values are raw samples of
one metric, or plain sums of them (an exact sum accumulator, a renaming,
or a heap estimate whose weights carry the SamplesOfMetric requirement),
Stage 1 now names the unknown sign by that metric,
values_non_negative[M], and gates the bound on it. Plan selection binds it
to 1 when the data workload declares M a counter; a gauge, an undeclared
metric, rate, other arithmetic or a name such as _total leave it unbound,
so the target stays unmet. Other folds keep the bare values_non_negative,
which nothing binds.
feat(devtools): limit Stage 1 candidates, Pass 2's included, while enumerating

stage_pipeline's --max-combinations (default 10^4) now counts Stage 1
candidates as they are enumerated, Pass 2's identical-expression
candidates included, and stops with an error as soon as the count passes
the limit; it never truncates. Exit status 3 (the viewer server's 422)
is unchanged. The library stays uncapped: Stage 1 is exposed as a lazy
iterator (stage1_logical_asap_optimization) and Stage 2 and Stage 3 run
over the candidates a caller enumerated (run_stage2_and_stage3);
run_stage1_to_stage3_from_pass1 enumerates them all. planning_stages
takes the limit as an Option.
perf(logical-asap-optimization): enumerate distinct Pass 2 candidates directly

The identical-expression rule tried all 2^g subsets of the sharing groups
and dropped those that repeat a candidate. With nested groups most
subsets repeat (14 groups: 16 384 subsets for 130 candidates). Sharing a
set of groups also shares the groups it leaves as one node; that closure
maps each subset to its candidate, and the distinct candidates are its
closed sets. They are now enumerated directly (Next Closure: one closure
per step and group), then put in the same binary-counter order, so the
output is unchanged. Twenty nested groups give their 20 candidates
quickly; a brute-force test checks the same candidates on small cases.

Sharing groups are computed once per candidate, and a Pass 2 candidate
names its shared groups by kind and number (Scan #1, TimeRange #2), so
repeated kinds stay distinguishable. The viewer fixtures are regenerated.
fix(plan-selection): every consumer gets the selected plan with confirmed weights

The counter-sample requirement Stage 3 confirms was made a proof only in
the planner pass, so run_stage1_to_stage3's selected_logical and the
selected candidate's roots still carried the open requirement, which the
executor rejects. Stage 3 now puts the plan on its output:
Selection::plan is the selected candidate's roots with confirmed weights,
and Stage1ToStage3Run::selected_logical returns the selected logical
roots the same way. The pass reads Selection::plan.
fix(logical-asap-optimization): an open weight requirement does not certify Count-Min

estimate_has_bound treated any weight domain but UnknownOrSigned as
proven, so a Count-Min estimate over weights with the open
SamplesOfMetric requirement got its bound as if the weights were proven
non-negative. Only NonNegative now counts. Such an estimate instead gets
its local bound gated on the metric's sign, values_non_negative[M], which
plan selection binds to 1 when M is declared a counter and leaves unbound
otherwise.
test: Example 1's shared-input totals tie with the independent one up to rounding

Shows that the tie tolerance is needed: the shared-input candidates'
totals differ from the independent candidate's in the last bits, and
plan selection reports them as tied and keeps the independent one. Also
drops stale wording in the acceptance helpers.
test(logical-asap-optimization): Pass 2 candidate labels and roots agree

Applying a candidate's label (the groups it shares) gives the same roots
the direct enumeration returned, sharing exactly those groups.
fix(plan-selection): the sign gate opens only at 1

The sign of one metric's samples gates a relative bound as a factor; a
binder supplying 0 would have read as a perfect bound. Plan selection now
binds a sign statistic only to 1 (proven) and leaves any other value
unbound.
docs: update the Example 1, 2 and 3 selections this PR changes

The Example 3 acceptance spec and the stage viewer guide now give 3a's
972 candidates and its all-KLL selection, 3b's KLL selection tied with
DDSketch, the per-second totals, and the shared-input plans that tie with
the independent one.
@milindsrivastava1997
milindsrivastava1997 deleted the docs/workload-plan-and-queryspec branch November 4, 2026 16:53
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.

2 participants