Skip to content

feat: physical planner — plan() builds PhysicalNode tree with Placement + Exchange (Layer 5) - #117

Merged
zzylol merged 13 commits into
mainfrom
pr/agg-intent-physical
Apr 4, 2026
Merged

zzylol merged 13 commits into
mainfrom
pr/agg-intent-physical

Conversation

@zzylol

@zzylol zzylol commented Apr 4, 2026 •

Copy link
Copy Markdown
Contributor

Files changed

controller/src/algebra/physical.rs (+471 lines)

New function: plan(expr: &QueryExpr, config: &PhysicalPlannerConfig) → PhysicalNode

Walks the optimized QueryExpr tree bottom-up and produces a PhysicalNode tree where each node has:

  • op: PhysicalOp — concrete operator (OtelSketchBuild, SketchMerge, TopK, DbQuery, Exchange, etc.)
  • placement: Placement — which pipeline stage runs it (AgentCollector, BackendCollector, QueryEngine, Database)
  • cost: PhysicalCost — estimated bandwidth, memory, CPU
  • children: Vec<PhysicalNode> — child nodes

New function: decide_sketch_placement(resolved, budgets) → Placement

Assigns sketch operations to Agent by default. Defers to BackendCollector when agent memory budget is exceeded, and to QueryEngine when both are exceeded.

New function: insert_exchange_if_needed(node)

When a parent node's placement differs from a child's, wraps the child in an Exchange node with the appropriate format:

  • Agent → Backend: ExchangeFormat::Otlp
  • Agent → QueryEngine: ExchangeFormat::Otlp
  • Backend → QueryEngine: ExchangeFormat::SketchBinary
  • Agent → Database: ExchangeFormat::RawSamples

New struct: PhysicalPlannerConfig

Holds StageResourceBudgets for budget-based deferral decisions.

New methods on PhysicalNode:

  • node_count() — total nodes in tree
  • placements() — distinct placements in tree
  • exchange_count() — number of Exchange boundaries

Placement rules implemented in plan_node():

QueryExpr node Placement
Source AgentCollector
Filter Same as child
SketchAgg, WindowedAgg AgentCollector (or deferred by budget)
Partition, Merge, Dedup BackendCollector
TopK, HistogramQuantile, BinaryOp, PromQLSubquery QueryEngine
Aggregate (non-sketch exact) Database
Sort, Limit, Project, Window, WindowFunc Inherit child
Join, JoinSketch, SetOp QueryEngine

controller/docs/query-to-sketch-translation.md (updated)

Layer 5 sections for all four concrete examples rewritten to show actual plan() output as PhysicalNode trees with Placement and Exchange annotations:

Example 4 (PromQL quantile): single-stage, all AgentCollector, 0 Exchanges

OtelSketchBuild { DDSketch, OtelTumblingFlush(5m) }  [AgentCollector]
  └── Filter                                         [AgentCollector]
        └── OtlpScan                                 [AgentCollector]

Example 5 (PromQL topk): 3-stage, 2 Exchanges

TopK                                [QueryEngine]
  └── Exchange(SketchBinary)        [QueryEngine]
        └── HashAggregate           [BackendCollector]
              └── Exchange(Otlp)    [BackendCollector]
                    └── OtelSketchBuild(CountSketch)  [AgentCollector]

Example 6 (SQL AVG): Agent→Database passthrough

DbQuery { GROUP BY }     [Database]
  └── Exchange(RawSamples)  [Database]
        └── OtlpScan      [AgentCollector]

Example 7 (SQL TUMBLE COUNT DISTINCT): 3-stage with HLL

TopK                                [QueryEngine]
  └── Exchange(SketchBinary)        [QueryEngine]
        └── HashAggregate           [BackendCollector]
              └── Exchange(Otlp)    [BackendCollector]
                    └── OtelSketchBuild(HLL)  [AgentCollector]

Tests

8 new tests in physical.rs:

  • plan_simple_sketch_at_agent — SketchAgg placed at Agent
  • plan_windowed_agg_has_window — WindowedAgg resolves physical window
  • plan_topk_at_query_engine — TopK placed at QueryEngine
  • plan_topk_inserts_exchange — Exchange inserted between Agent and QueryEngine
  • plan_partition_at_backend — Partition placed at Backend
  • plan_aggregate_at_database — non-sketch Aggregate placed at Database
  • plan_full_pipeline_has_multiple_stages — verifies 3 stages + ≥2 exchanges
  • plan_budget_deferral — tiny agent budget defers sketch to Backend

323 total tests pass.

🤖 Generated with Claude Code

zzylol and others added 6 commits April 2, 2026 16:43
Refactors the sketch algebra to cleanly separate the logical plan
(Layer 3) from physical implementation details (Layer 5).

Core changes:
- Rename SketchAggOp → AggIntent with implementation-independent variants:
  DDSketch{quantiles,epsilon} → Quantile{quantiles,accuracy}
  HLL{registers}              → Cardinality{accuracy}
  CountMin/CountSketch{w,d}   → Frequency{accuracy}
  ExactMinMax{min,max}        → Extrema{min,max}
  Hydra{inner,partition_keys} → PerPartition{inner,keys}
- Add WindowSpec{kind,time_col} and WindowKind enum
  (Tumbling, Sliding, Unbounded, Landmark, Session)
- Add QueryExpr::WindowedAgg{agg,window,col,input} — bundles
  window and sketch agg because the window defines sketch lifecycle
- Keep SketchAggOp as backward-compat type alias for AggIntent
- Update directory, allocator, optimizer, stage_split, parsers
- Update docs/query-to-sketch-translation.md with 5-layer design

The logical plan now expresses WHAT to compute (quantile at φ=0.99
with ≤1% error) not HOW (DDSketch vs KLL vs PromSketch EHKLL).
Physical implementation selection moves to the physical planner.

279 tests pass, no business logic changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cal.rs

Phase 1 — Layer 3 cleanup:
- Remove SketchAggOp type alias and all backward-compat helpers
  (default_ddsketch, default_hll, default_count_min, default_count_sketch)
- All callers now use implementation-independent names:
  default_quantile, default_cardinality, default_frequency
- Zero physical sketch names (DDSketch, HLL, CountMin) remain in Layer 3

Phase 2 — Layer 5 formalization:
- New algebra/physical.rs with PhysicalAggOp struct and resolve() function
- PhysicalAggOp bundles: intent + sketch_type + sketch_params + memory estimate
- resolve() is the single Layer 3→5 boundary: AggIntent → physical implementation
- stage_split.rs and allocator.rs now call physical::resolve() instead of
  inline directory::sketch_type_for_op/sketch_params_for_op

Phase 3 — Layer documentation:
- promql.rs header documents Layers 1→3 lowering
- algebra/mod.rs documents the full 5-layer module layout
- docs/query-to-sketch-translation.md updated with 5-layer architecture

283 tests pass (279 + 4 new physical.rs tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both parsers now emit Layer 2 relational operators (Aggregate + AggFunc),
and a shared lowering pass (algebra/lower.rs) converts to Layer 3
sketch algebra (SketchAgg + AggIntent).

Layer compliance:
- Layer 1-2 (parsers): zero AggIntent in code — only Aggregate + AggFunc
- Layer 2→3 (lower.rs): shared lower_to_sketch_algebra() for both languages
- Layer 3 (expr.rs): AggIntent, zero physical sketch names in code
- Layer 4 (optimizer.rs): matches AggIntent, zero SketchType references
- Layer 5 (physical.rs): resolve() maps AggIntent → SketchType + SketchParams

Changes:
- promql.rs: walk_call_to_op returns AggFunc (not AggIntent);
  build_qe_sketched → build_qe_aggregate emitting Aggregate + Window nodes
- lower.rs: new shared lowering pass (18 tests) — walks all 25 QueryExpr
  variants, converts single-agg Aggregate → SketchAgg where sketchable
- mod.rs: parse_query_expr calls lower_to_sketch_algebra after parsing

301 tests pass (283 + 18 new lowering tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, L4 is deployment-aware

- Layers 1-3: query-language-independent and workload-independent
  (same AggIntent whether from PromQL, SQL, DataFusion, or ElasticDSL)
- Layer 4: deployment-constraint-aware optimizer (memory budgets,
  network topology, available backends)
- Layer 5: deployment-specific physical plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. WindowedAgg emission: lower.rs fuses Window + Aggregate → WindowedAgg
   when the inner Aggregate is sketchable (2 new tests)

2. SQL TUMBLE/HOP/time_bucket: SQL parser detects TUMBLE(ts, INTERVAL),
   HOP(ts, slide, size), and time_bucket('5 minutes', ts) in GROUP BY
   and emits Window nodes (4 new tests)

3. PhysicalPlan tree: physical.rs expanded with PhysicalOp enum
   (OtelSketchBuild, PromSketchBuild, SketchMerge, SketchEval, Exchange),
   Placement enum, PhysicalNode tree, PhysicalCost, resolve_window()
   (4 new tests)

4. Optimizer deployment constraints: DeploymentConstraints struct with
   agent/backend memory budgets, promsketch_available, agent_supports_sliding,
   bandwidth. QueryOptimizer::with_constraints() builder. DefaultCostModel
   penalises sketches exceeding agent budget.

5. Doc examples: all 4 concrete examples rewritten to show all 5 layers
   (Layer 1 AST → Layer 2 Aggregate+AggFunc → Layer 3 AggIntent →
   Layer 4 optimizer → Layer 5 physical resolve + config emit)

311 tests pass (301 + 10 new).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Fix label_matchers YAML format: controller now emits structured
   [{key: "env", value: "prod"}] matching the Go LabelMatcher struct,
   not flat ["env=prod"] strings.

2. Add bootstrap config endpoints for collectors:
   - GET /api/v1/collector-config/agent → default agent YAML
   - GET /api/v1/collector-config/backend → default backend YAML
   Collectors can start with:
     ./sketchcol --config "http://controller:8080/api/v1/collector-config/agent"

3. Add sketchcol unified collector builder config with ALL sketch
   processors (DDSketch, HLL, KLL, CountSketch, CountMinSketch +
   merge variants) in a single binary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
zzylol and others added 4 commits April 4, 2026 11:06
…ectStore

Replace OTel/PromSketch-specific names with deployment-agnostic terms:
- edge processors (sketch build at data source)
- backend sketchDB (merge + approximate query)
- backend original DB (exact computation)
- object store (raw data backup)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…owering

Section 3 now clearly separates:
- 3.1 Layer 1→2: language AST → language logical plan (Aggregate + AggFunc)
  Both PromQL and SQL produce the same relational operators, no sketch names.
- 3.2 Layer 2→3: shared lowering algorithm converts AggFunc → AggIntent
  Includes the full mapping table and Window→WindowedAgg fusion rule.

Removes all physical sketch names (DDSketch, HLL, CountMin) from the
parsing section — those belong only to Layer 5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PromQL parser now puts partition keys from `topk by (dims)` into
the inner Aggregate's `keys` field instead of wrapping with a separate
Partition node. This fixes two bugs:

1. Count inside topk was not lowered to Frequency — the lowering pass
   saw Count-without-GROUP-BY and skipped it. Now it sees
   Count-with-GROUP-BY → Frequency.

2. Double Partition wrapping — topk added Partition, and
   apply_qe_partition added another. Now the lowering pass creates
   the single Partition from the Aggregate's keys.

Example 5 now produces:
  TopK { 10, by: ["service"],
    Partition { ["service"],
      WindowedAgg { Frequency, Tumbling(1m), Filter(Source) } } }

Doc updated: fixed PromQL syntax (topk by (service) (10, ...)),
added Layer 1 + Layer 2 sections, corrected Layer 3 output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nt, inserts Exchange

Replaces the placeholder physical.rs with a working physical planner:

physical::plan(expr, config) → PhysicalNode tree

The planner walks the optimized QueryExpr bottom-up and:
1. Assigns each node to a Placement (AgentCollector, BackendCollector,
   QueryEngine, Database) based on the operator type and memory budgets
2. Resolves AggIntent → concrete SketchType + SketchParams via resolve()
3. Resolves WindowSpec → PhysicalWindow via resolve_window()
4. Inserts Exchange nodes at stage boundaries (Agent→Backend, Backend→QueryEngine)
5. Defers sketches to Backend/QueryEngine when agent budget is exceeded

Placement rules:
- Source, Filter, SketchAgg, WindowedAgg → AgentCollector
- Partition, Merge, Dedup → BackendCollector
- TopK, HistogramQuantile, BinaryOp, PromQLSubquery → QueryEngine
- Aggregate (non-sketch exact) → Database
- Sort, Limit, Project → inherit child placement

PhysicalNode provides: node_count(), placements(), exchange_count()

8 new tests verify placement, exchange insertion, budget deferral,
and full multi-stage pipeline (Agent→Backend→QueryEngine).

323 total tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zzylol zzylol changed the title feat: AggIntent + physical.rs — separate logical sketch intent from physical implementation feat: real physical planner — stage placement + Exchange insertion (Layer 5) Apr 4, 2026
zzylol and others added 2 commits April 4, 2026 11:44
Main had the placeholder physical.rs (from PR #116 merge). This branch
has the real physical planner with plan(), Placement, Exchange insertion.
Keep ours.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rees

All four examples now show the actual PhysicalNode tree output from
physical::plan(), including:
- Placement annotations ([AgentCollector], [BackendCollector], etc.)
- Exchange nodes at stage boundaries
- Concrete sketch types and window implementations

Example 4: single-stage (all Agent, no Exchange)
Example 5: 3-stage (Agent→Backend→QueryEngine, 2 Exchanges)
Example 6: Agent→Database (raw passthrough for non-mergeable AVG)
Example 7: 3-stage with HLL (Agent→Backend→QueryEngine)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zzylol zzylol changed the title feat: real physical planner — stage placement + Exchange insertion (Layer 5) feat: physical planner — plan() builds PhysicalNode tree with Placement + Exchange (Layer 5) Apr 4, 2026
Completes Layer 5: main.rs now calls physical_plan_to_staged() instead
of split_expr_by_stage().

New in physical.rs:
- PhysicalNode::to_staged_plan() — walks the tree and extracts a flat
  StagedPlan (AgentSubPlan, BackendSubPlan, PrecomputeSubPlan, DbSubPlan)
  from the PhysicalNode tree. Bridges the physical planner to existing
  config generators.
- physical_plan_to_staged(expr, budgets) → (StagedPlan, PhysicalNode) —
  single entry point for main.rs: runs plan() then to_staged_plan().

Changed in main.rs:
- Replaced `use planner::stage_split::split_expr_by_stage` with
  `use algebra::physical::physical_plan_to_staged`
- handle_plan now calls physical_plan_to_staged(&opt_qe, &budgets)

The data flow is now:
  parse → lower → optimize → physical::plan() → PhysicalNode tree
    → to_staged_plan() → StagedPlan → config generators → YAML

3 new tests: staged_plan_simple_sketch, staged_plan_topk_multi_stage,
staged_plan_exact_agg_at_db. 326 total pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit a16f219 into main Apr 4, 2026
@zzylol
zzylol deleted the pr/agg-intent-physical branch April 4, 2026 17:39
zzylol added a commit that referenced this pull request Apr 4, 2026
Rebased on latest main. Dropped stale controller changes (already on
main via PRs #96, #116, #117, #119).

Only benchmark tooling remains:
- replay.py: replay DEBS dataset through OTel collector
- run.py: orchestrate benchmark runs
- scrape.py: collect Prometheus metrics during run
- analyze.py: parse benchmark results
- compare.py: compare sketch results to ground truth
- summarize.py: generate summary tables
- ground_truth/: ground truth computation for accuracy checking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Apr 4, 2026
Rebased on latest main. Dropped stale controller changes (already on
main via PRs #96, #116, #117, #119).

Only benchmark tooling remains:
- replay.py: replay DEBS dataset through OTel collector
- run.py: orchestrate benchmark runs
- scrape.py: collect Prometheus metrics during run
- analyze.py: parse benchmark results
- compare.py: compare sketch results to ground truth
- summarize.py: generate summary tables
- ground_truth/: ground truth computation for accuracy checking

Co-authored-by: zz_y <zeyingz@umd.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SieDeta pushed a commit that referenced this pull request Apr 17, 2026
…nt + Exchange (Layer 5) (#117)

* feat: 5-layer architecture — AggIntent, WindowSpec, WindowedAgg

Refactors the sketch algebra to cleanly separate the logical plan
(Layer 3) from physical implementation details (Layer 5).

Core changes:
- Rename SketchAggOp → AggIntent with implementation-independent variants:
  DDSketch{quantiles,epsilon} → Quantile{quantiles,accuracy}
  HLL{registers}              → Cardinality{accuracy}
  CountMin/CountSketch{w,d}   → Frequency{accuracy}
  ExactMinMax{min,max}        → Extrema{min,max}
  Hydra{inner,partition_keys} → PerPartition{inner,keys}
- Add WindowSpec{kind,time_col} and WindowKind enum
  (Tumbling, Sliding, Unbounded, Landmark, Session)
- Add QueryExpr::WindowedAgg{agg,window,col,input} — bundles
  window and sketch agg because the window defines sketch lifecycle
- Keep SketchAggOp as backward-compat type alias for AggIntent
- Update directory, allocator, optimizer, stage_split, parsers
- Update docs/query-to-sketch-translation.md with 5-layer design

The logical plan now expresses WHAT to compute (quantile at φ=0.99
with ≤1% error) not HOW (DDSketch vs KLL vs PromSketch EHKLL).
Physical implementation selection moves to the physical planner.

279 tests pass, no business logic changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: complete 5-layer implementation — remove SketchAggOp, add physical.rs

Phase 1 — Layer 3 cleanup:
- Remove SketchAggOp type alias and all backward-compat helpers
  (default_ddsketch, default_hll, default_count_min, default_count_sketch)
- All callers now use implementation-independent names:
  default_quantile, default_cardinality, default_frequency
- Zero physical sketch names (DDSketch, HLL, CountMin) remain in Layer 3

Phase 2 — Layer 5 formalization:
- New algebra/physical.rs with PhysicalAggOp struct and resolve() function
- PhysicalAggOp bundles: intent + sketch_type + sketch_params + memory estimate
- resolve() is the single Layer 3→5 boundary: AggIntent → physical implementation
- stage_split.rs and allocator.rs now call physical::resolve() instead of
  inline directory::sketch_type_for_op/sketch_params_for_op

Phase 3 — Layer documentation:
- promql.rs header documents Layers 1→3 lowering
- algebra/mod.rs documents the full 5-layer module layout
- docs/query-to-sketch-translation.md updated with 5-layer architecture

283 tests pass (279 + 4 new physical.rs tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: both PromQL and SQL now follow 5-layer architecture

Both parsers now emit Layer 2 relational operators (Aggregate + AggFunc),
and a shared lowering pass (algebra/lower.rs) converts to Layer 3
sketch algebra (SketchAgg + AggIntent).

Layer compliance:
- Layer 1-2 (parsers): zero AggIntent in code — only Aggregate + AggFunc
- Layer 2→3 (lower.rs): shared lower_to_sketch_algebra() for both languages
- Layer 3 (expr.rs): AggIntent, zero physical sketch names in code
- Layer 4 (optimizer.rs): matches AggIntent, zero SketchType references
- Layer 5 (physical.rs): resolve() maps AggIntent → SketchType + SketchParams

Changes:
- promql.rs: walk_call_to_op returns AggFunc (not AggIntent);
  build_qe_sketched → build_qe_aggregate emitting Aggregate + Window nodes
- lower.rs: new shared lowering pass (18 tests) — walks all 25 QueryExpr
  variants, converts single-agg Aggregate → SketchAgg where sketchable
- mod.rs: parse_query_expr calls lower_to_sketch_algebra after parsing

301 tests pass (283 + 18 new lowering tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix layer descriptions — L1-3 are language/workload-independent, L4 is deployment-aware

- Layers 1-3: query-language-independent and workload-independent
  (same AggIntent whether from PromQL, SQL, DataFusion, or ElasticDSL)
- Layer 4: deployment-constraint-aware optimizer (memory budgets,
  network topology, available backends)
- Layer 5: deployment-specific physical plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: complete 5-layer implementation — all gaps fixed

1. WindowedAgg emission: lower.rs fuses Window + Aggregate → WindowedAgg
   when the inner Aggregate is sketchable (2 new tests)

2. SQL TUMBLE/HOP/time_bucket: SQL parser detects TUMBLE(ts, INTERVAL),
   HOP(ts, slide, size), and time_bucket('5 minutes', ts) in GROUP BY
   and emits Window nodes (4 new tests)

3. PhysicalPlan tree: physical.rs expanded with PhysicalOp enum
   (OtelSketchBuild, PromSketchBuild, SketchMerge, SketchEval, Exchange),
   Placement enum, PhysicalNode tree, PhysicalCost, resolve_window()
   (4 new tests)

4. Optimizer deployment constraints: DeploymentConstraints struct with
   agent/backend memory budgets, promsketch_available, agent_supports_sliding,
   bandwidth. QueryOptimizer::with_constraints() builder. DefaultCostModel
   penalises sketches exceeding agent budget.

5. Doc examples: all 4 concrete examples rewritten to show all 5 layers
   (Layer 1 AST → Layer 2 Aggregate+AggFunc → Layer 3 AggIntent →
   Layer 4 optimizer → Layer 5 physical resolve + config emit)

311 tests pass (301 + 10 new).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: fix end-to-end OTel integration gaps

1. Fix label_matchers YAML format: controller now emits structured
   [{key: "env", value: "prod"}] matching the Go LabelMatcher struct,
   not flat ["env=prod"] strings.

2. Add bootstrap config endpoints for collectors:
   - GET /api/v1/collector-config/agent → default agent YAML
   - GET /api/v1/collector-config/backend → default backend YAML
   Collectors can start with:
     ./sketchcol --config "http://controller:8080/api/v1/collector-config/agent"

3. Add sketchcol unified collector builder config with ALL sketch
   processors (DDSketch, HLL, KLL, CountSketch, CountMinSketch +
   merge variants) in a single binary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update physical plan terminology — edge/sketchDB/originalDB/objectStore

Replace OTel/PromSketch-specific names with deployment-agnostic terms:
- edge processors (sketch build at data source)
- backend sketchDB (merge + approximate query)
- backend original DB (exact computation)
- object store (raw data backup)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: rewrite §3 as two clear steps — Layer 1→2 parsing + Layer 2→3 lowering

Section 3 now clearly separates:
- 3.1 Layer 1→2: language AST → language logical plan (Aggregate + AggFunc)
  Both PromQL and SQL produce the same relational operators, no sketch names.
- 3.2 Layer 2→3: shared lowering algorithm converts AggFunc → AggIntent
  Includes the full mapping table and Window→WindowedAgg fusion rule.

Removes all physical sketch names (DDSketch, HLL, CountMin) from the
parsing section — those belong only to Layer 5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: propagate topk partition keys into inner Aggregate GROUP BY

The PromQL parser now puts partition keys from `topk by (dims)` into
the inner Aggregate's `keys` field instead of wrapping with a separate
Partition node. This fixes two bugs:

1. Count inside topk was not lowered to Frequency — the lowering pass
   saw Count-without-GROUP-BY and skipped it. Now it sees
   Count-with-GROUP-BY → Frequency.

2. Double Partition wrapping — topk added Partition, and
   apply_qe_partition added another. Now the lowering pass creates
   the single Partition from the Aggregate's keys.

Example 5 now produces:
  TopK { 10, by: ["service"],
    Partition { ["service"],
      WindowedAgg { Frequency, Tumbling(1m), Filter(Source) } } }

Doc updated: fixed PromQL syntax (topk by (service) (10, ...)),
added Layer 1 + Layer 2 sections, corrected Layer 3 output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: real physical planner — plan() walks QueryExpr, assigns Placement, inserts Exchange

Replaces the placeholder physical.rs with a working physical planner:

physical::plan(expr, config) → PhysicalNode tree

The planner walks the optimized QueryExpr bottom-up and:
1. Assigns each node to a Placement (AgentCollector, BackendCollector,
   QueryEngine, Database) based on the operator type and memory budgets
2. Resolves AggIntent → concrete SketchType + SketchParams via resolve()
3. Resolves WindowSpec → PhysicalWindow via resolve_window()
4. Inserts Exchange nodes at stage boundaries (Agent→Backend, Backend→QueryEngine)
5. Defers sketches to Backend/QueryEngine when agent budget is exceeded

Placement rules:
- Source, Filter, SketchAgg, WindowedAgg → AgentCollector
- Partition, Merge, Dedup → BackendCollector
- TopK, HistogramQuantile, BinaryOp, PromQLSubquery → QueryEngine
- Aggregate (non-sketch exact) → Database
- Sort, Limit, Project → inherit child placement

PhysicalNode provides: node_count(), placements(), exchange_count()

8 new tests verify placement, exchange insertion, budget deferral,
and full multi-stage pipeline (Agent→Backend→QueryEngine).

323 total tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update Layer 5 examples to show physical::plan() PhysicalNode trees

All four examples now show the actual PhysicalNode tree output from
physical::plan(), including:
- Placement annotations ([AgentCollector], [BackendCollector], etc.)
- Exchange nodes at stage boundaries
- Concrete sketch types and window implementations

Example 4: single-stage (all Agent, no Exchange)
Example 5: 3-stage (Agent→Backend→QueryEngine, 2 Exchanges)
Example 6: Agent→Database (raw passthrough for non-mergeable AVG)
Example 7: 3-stage with HLL (Agent→Backend→QueryEngine)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: wire physical planner into main.rs — replaces split_expr_by_stage

Completes Layer 5: main.rs now calls physical_plan_to_staged() instead
of split_expr_by_stage().

New in physical.rs:
- PhysicalNode::to_staged_plan() — walks the tree and extracts a flat
  StagedPlan (AgentSubPlan, BackendSubPlan, PrecomputeSubPlan, DbSubPlan)
  from the PhysicalNode tree. Bridges the physical planner to existing
  config generators.
- physical_plan_to_staged(expr, budgets) → (StagedPlan, PhysicalNode) —
  single entry point for main.rs: runs plan() then to_staged_plan().

Changed in main.rs:
- Replaced `use planner::stage_split::split_expr_by_stage` with
  `use algebra::physical::physical_plan_to_staged`
- handle_plan now calls physical_plan_to_staged(&opt_qe, &budgets)

The data flow is now:
  parse → lower → optimize → physical::plan() → PhysicalNode tree
    → to_staged_plan() → StagedPlan → config generators → YAML

3 new tests: staged_plan_simple_sketch, staged_plan_topk_multi_stage,
staged_plan_exact_agg_at_db. 326 total pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SieDeta pushed a commit that referenced this pull request Apr 17, 2026
Rebased on latest main. Dropped stale controller changes (already on
main via PRs #96, #116, #117, #119).

Only benchmark tooling remains:
- replay.py: replay DEBS dataset through OTel collector
- run.py: orchestrate benchmark runs
- scrape.py: collect Prometheus metrics during run
- analyze.py: parse benchmark results
- compare.py: compare sketch results to ground truth
- summarize.py: generate summary tables
- ground_truth/: ground truth computation for accuracy checking

Co-authored-by: zz_y <zeyingz@umd.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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