Skip to content

feat(sketch-db): Phase 3b — cross-schema result combiner + engine registry wiring - #22

Merged
zzylol merged 1 commit into
mainfrom
sketchdb/phase3b-query-dispatch
Apr 17, 2026
Merged

zzylol merged 1 commit into
mainfrom
sketchdb/phase3b-query-dispatch

Conversation

@zzylol

@zzylol zzylol commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands the §7 combiner primitive plus the minimal plumbing the query path needs to reach a live SchemaRegistry. Does not yet re-route the existing dispatch through the combiner — that is Phase 3b-2, a pure-plumbing follow-up PR.

  • New src/engines/timeline_dispatch.rs with combine_statistic() + CombinedResult { Full | Partial } + SegmentValue. 12 unit tests.
  • SimpleEngine gains schema_registry: Arc<SchemaRegistry> (default empty), with_schema_registry() builder, timeline_for_query() thin delegate. All ~35 existing call-sites of SimpleEngine::new* keep compiling unchanged.
  • main.rs defers Arc::new(engine) until after the precompute engine is built, then plumbs precompute_ingest_state.schemas.clone() into the engine so both components share the same Arc<SchemaRegistry> the HTTP swap handler (Phase 2b) is driving.

Why split 3b into two PRs

The engine's query dispatch (build_query_execution_context_promql + the range pipeline) is ~5k LoC with many interacting tests. Wiring the combiner into it in the same PR as landing the combiner itself makes the PR review surface too large. Splitting keeps each PR local and reviewable.

§7.3 combinability table

Statistic Combinable at scalar level?
Count, Sum Yes — sum
Min, Max Yes — pointwise
Cardinality No — distinct HLL parameterisations can't be OR-merged via scalars
Increase, Rate No — need raw counters, not deltas
Quantile, Topk No — need underlying sketch merge

Non-combinable results come back as Partial { covered, missing } so the user (and Phase 3b-2's caller) can see exactly which segments failed.

Test plan

  • 12 new combiner unit tests: all statistics × empty / single / multi / with-unresolved.
  • 559 lib tests pass (up from 547).
  • cargo clippy --workspace --all-targets --tests -- -D warnings clean.
  • cargo fmt -- --check clean.

🤖 Generated with Claude Code

…istry wiring

Lands the §7 combiner primitive used by the query path to stitch
per-segment scalars across a schema change, plus the minimal
SimpleEngine plumbing needed to reach a live SchemaRegistry. Does
**not** yet re-route the existing dispatch through the combiner —
that is Phase 3b-2, a pure-plumbing follow-up PR.

## New: `src/engines/timeline_dispatch.rs`

* `CombinedResult { Full(f64) | Partial { covered, missing } }` —
  the result type that surfaces the schema-change failure mode
  explicitly instead of silently returning "the wrong answer" when
  a statistic can't span a schema boundary.
* `SegmentValue { segment, value }` — per-segment scalar input.
* `combine_statistic(stat, &segments, &unresolved) -> CombinedResult`
  implementing §7.3 combinability:
  - Count / Sum: additive.
  - Min / Max: pointwise.
  - Cardinality / Increase / Rate / Quantile / Topk: non-combinable
    at the scalar level — caller must merge underlying sketches or
    fall back to the exact DB.
* 12 unit tests covering each statistic, empty-input, single-segment,
  and unresolved-with-covered scenarios.

## SimpleEngine wiring

* Adds `schema_registry: Arc<SchemaRegistry>` field, defaulted to
  `SchemaRegistry::empty()` so the ~35 existing `SimpleEngine::new*`
  call-sites keep compiling unchanged.
* `with_schema_registry(Arc<SchemaRegistry>) -> Self` builder
  alongside `with_controller_client`.
* `timeline_for_query(metric, t1_ms, t2_ms) -> Vec<TimelineSegment>`
  thin delegate so the engine's own query path (and Phase 3b-2 tests)
  don't need to reach into the store module to build a timeline.

## main.rs plumbing

Defers the `Arc::new(engine)` wrap until after the precompute engine
is constructed, so the engine can share `precompute_ingest_state.schemas`
— the same `Arc<SchemaRegistry>` the ingest path is already
reconciling and the HTTP swap handler is driving event-driven (Phase
2b). When precompute isn't enabled the engine keeps its default
empty registry and `timeline_for_query` returns no segments,
matching pre-Phase-3 behaviour.

559 lib tests pass (up from 547: +12 combiner tests); clippy/fmt
clean. No changes to existing engine tests or call-sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit c67f16e into main Apr 17, 2026
@zzylol
zzylol deleted the sketchdb/phase3b-query-dispatch branch April 17, 2026 20:54
zzylol added a commit that referenced this pull request Apr 20, 2026
…able stats (#48)

Task #34 gap #1 of 3. The §7 schema-timeline primitive
(`SchemaRegistry::timeline_for_metric`) and the cross-schema
combiner (`engines::timeline_dispatch::combine_statistic`)
landed in PRs #20 / #22 / #25, but `SimpleEngine::handle_query_promql`
was still resolving a single `agg_id` via `resolve_agg_info_promql`
and running the full query against it. A query whose time range
spans a reconfigure boundary (old `agg_id` retired, new
`agg_id` created) saw a data cliff for the pre-boundary slice.

This PR wires the dispatcher:

* New `SimpleEngine::try_handle_query_promql_via_timeline`:
  1. Parse + pattern-match the query, extract metric name.
  2. Build a probe `QueryExecutionContext` to read the
     resolved `[t1, t2]` + `Statistic`.
  3. Call `timeline_for_query(metric, t1, t2)`. Bail out
     with `None` (fall-through to default single-agg path)
     if fewer than two segments, or if the statistic is
     non-combinable (quantile / topk / cardinality / rate
     / increase — those follow in PR B2 with a Partial
     HTTP response surface).
  4. Per segment: reuse `build_query_execution_context_promql_for_agg_id`
     from PR #37 (the extracted forced-agg-id entry point),
     clip the store plan's `[start, end]` to the segment's
     bounds, execute, collect results.
  5. Group by label-tuple and fold per-group per-segment
     scalars through `combine_statistic`. Emit the combined
     scalar as an `InstantVectorElement`. Purged segments
     or segments whose `agg_id` is no longer in the config
     go into `unresolved` so the combiner sees them.

* `handle_query_promql` now tries the timeline path first;
  returns immediately on `Some`, falls through to the
  existing single-agg path on `None`. Zero behavior change
  when the timeline has 0–1 segments for the query's metric
  (the common case today).

## Scope

Combinable stats only: Count / Sum / Min / Max. Non-combinable
stats still take the single-agg path — PR B2 will surface
`CombinedResult::Partial` on the HTTP response so users see
`{covered, missing: [segments]}` explicitly instead of a
silent data cliff.

## Validation

- `cargo test -p query_engine_rust --lib` — 728 pass (baseline
  unchanged; the dispatcher stays dormant when tests only
  register one schema per metric).
- `cargo clippy --all-targets -- -D warnings` — clean
- `cargo fmt --all -- --check` — clean

## Follow-ups (explicit non-scope here)

- **Integration test** seeding two agg_ids + cross-boundary
  Sum query. Requires the full `PrecomputeEngine` setup
  harness the existing e2e tests use; deferred as a
  dedicated PR so this one stays a focused dispatcher
  patch.
- **PR B2**: Partial response surface for non-combinable
  stats on the HTTP adapter.

Co-authored-by: Claude Opus 4.7 (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