feat(core): RuntimeStatsExec — passthrough tap for row counts + quantile sketches#2094
Open
avantgardnerio wants to merge 5 commits into
Open
feat(core): RuntimeStatsExec — passthrough tap for row counts + quantile sketches#2094avantgardnerio wants to merge 5 commits into
avantgardnerio wants to merge 5 commits into
Conversation
… counts and quantile sketches Passthrough physical operator that observes input batches as they stream past and accumulates two families of runtime state, without changing the data: * per-partition row count (`AtomicUsize`, always) * per-partition quantile sketch over the first `ORDER BY` expression's `Float64` values (`Mutex<TDigest>`, only when `order_by = Some`) Both accessors are readable at any point, so callers get mid-stream snapshots when the sample is accurate enough or post-drain snapshots when they want the fully-observed state. Ships in isolation: nothing wires it into a plan yet, and the executor doesn't yet ship the accumulated state back to the scheduler. Those pieces arrive with the range-repartition operators (the first consumer). Includes: * `datafusion-functions-aggregate-common` dep for `TDigest` * proto `RuntimeStatsExecNode` (tag 6 on `BallistaPhysicalPlanNode`) and `QuantileSketchState` (T-Digest wire format via `to_scalar_state`) * `sketch_to_proto` / `sketch_from_proto` helpers * codec encode/decode registration * nine tests: three wire round-trip, three streaming (populated sketch, NULL-key skip, row-count-only), and three plan-node serde roundtrips Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-common dep Adds the transitive entry to ballista-core in python/Cargo.lock so `cargo check --locked` succeeds in the python workspace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
@phillipleblanc FYI |
4 tasks
avantgardnerio
added a commit
to avantgardnerio/arrow-ballista
that referenced
this pull request
Jul 20, 2026
apache#2094's RuntimeStatsExecNode also took tag 6 (both PRs were cut off origin/main independently). Whoever landed second would have had to rebase; picking 7 now lets both merge without a follow-up shuffle. No wire compatibility concerns — neither variant has shipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
phillipleblanc
left a comment
Contributor
There was a problem hiding this comment.
I have a few comments, but this is looking good!
TDigest has no NULL slot, so a nullable routing column silently excludes NULLs from the sketch while `row_count` still counts them — skewing the sketch away from what actually flowed through. Rejecting nullable routing at construction closes the gap. KLL (the planned swap) will lift this by positioning NULLs per each key's `SortOptions::nulls_first`. Also: - Extend module TODO to name KLL's NULL positioning story. - Rewrite the `ingest` comment about NULLs — it's a TDigest limitation, not a semantic decision (NULLs are still forwarded and counted). - Delete `execute_skips_null_routing_keys_from_sketch` — unreachable under the new construction check; replaced by a rejection test in serde tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Thanks for your feedback @phillipleblanc . As usual it was very helpful 😄 |
Matches the algebraic-method surface on BufferExec — adds explicit `required_input_distribution`, `required_input_ordering`, and `benefits_from_input_partitioning` alongside the three behaviour-overriding methods already there. All six now spell out the passthrough contract so a reader doesn't have to consult trait defaults to know how the planner will treat this node. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
phillipleblanc
approved these changes
Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
RuntimeStatsExec: a passthrough physical operator that observes input batches as they stream past and accumulates runtime state, without touching the data. Two accessor families:AtomicUsize::fetch_addon the hot path (no cross-partition contention, no lock overhead). Always tracked.Mutex<TDigest>(writes never contend across partitions). Only allocated when the caller passesSome(order_by)at construction.Both accessors are readable at any point, so callers get a mid-stream snapshot when the sample is accurate enough or a post-drain snapshot when they want the fully-observed state (typical after a blocking downstream like
SortExec).Why this exists — supporting the forthcoming range-repartition operators
RuntimeStatsExecis the shared substrate for two upcoming ops (OrderedRangeRepartitionExecandUnorderedRangeRepartitionExec) that will land the distributed parallel-window path. Two modes matched to two use sites:Sketching mode (
Some(order_by)) → drivesUnorderedRangeRepartitionExec.Placed above the scan,
RuntimeStatsExecstreams a T-Digest over the routing column while the data is scanned. Each executor's sketch is merged (TDigest::merge_digests) to choose global cut points at a scheduler barrier, thenUnorderedRangeRepartitionExecuses those cuts to route rows to sub-partitions by value range — no upfront sort required.Row-count-only mode (
None) → drivesOrderedRangeRepartitionExec.Placed above a
SortExec, it exposes exact runtime row counts per sub-partition. DataFusion'spartition_statistics()only returns plan-time estimates, which are too coarse for the ordered stage's bin-packer. The tap fixes that at operator level without touching DF upstream.The full parallel-window path (Stage 1 sketch → cut selection → Stage 2 slice) is described in the
parallel-window-kll-adaptivedesign doc on the source branch and will land alongside the range-repartition operators.Scope of this PR
Just the operator, its proto/serde plumbing, and tests. Nothing consumes the accumulated state yet — the executor still ships
SuccessfulTaskunchanged, and no rule insertsRuntimeStatsExecinto any plan. Those pieces land with the range-repartition operators.Included:
datafusion-functions-aggregate-common = \"54\"workspace dep (forTDigest).RuntimeStatsExecNodeonBallistaPhysicalPlanNode.oneof(tag 6), andQuantileSketchStatefor the wire T-Digest (fixed 6-elementVec<ScalarValue>perTDigest::to_scalar_state).sketch_to_proto/sketch_from_protohelpers with a shape guard against corrupted wire input (would otherwise panic infrom_scalar_state).BallistaPhysicalExtensionCodec.TDigest(Float64-only, single-column) for a generic-over-OrdKLL sketch that can cover the fullVec<PhysicalSortExpr>.Tests (9)
execute_populates_sketch_and_row_count,execute_skips_null_routing_keys_from_sketch,execute_row_count_only_no_sketch.order_byrejection.Test plan
cargo test -p ballista-core(all 123 tests + 2 doctests + 1 compile-fail pass).cargo clippy --all-targetsclean.cargo fmt --all.🤖 Generated with Claude Code