Skip to content

feat(asap-aware-mapping): AvgToSumOverCountStrategy semantic rewrite (#253) - #260

Merged
zzylol merged 2 commits into
mainfrom
feat/semantic-rewrite-avg-253
Aug 25, 2026
Merged

zzylol merged 2 commits into
mainfrom
feat/semantic-rewrite-avg-253

Conversation

@zzylol

@zzylol zzylol commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Closes #253. Part of #33. Stacked on #259 (issue #251) — do not merge before that one.

What

A ReplacementStrategyAvgToSumOverCountStrategy — implementing the
"semantic-equivalent rewriting" degree of freedom from
docs/asap_aware_mapping.md: rewrites Aggregate{ measures: [Avg{col}], .. }
into Aggregate{ measures: [Sum{col}, Count], .. } |> Project{ sum/count re-divided under the original avg column name }.

Why

boundary::implementation_for_with dispatches Avg straight to
Implementation::PassThrough — a bare avg node has no summary
realization at all, so it can never be a SharedSubtreeStrategy target.
Sum and Count are both ordinary mergeable accumulators
SharedSubtreeStrategy (and a future sketch-family search) already know
how to reuse across a workload; this rewrite reshapes avg into those
pieces without changing what the query computes.

Shape

  • matches: a single Avg{col} measure, no HAVING (the same
    bindable shape bind::implement_tree_with requires — mirrors
    SketchFamilyStrategy's own bindable_intent check), further
    restricted to an ordinary by(...) reduction. without(...) and
    Reduction::PerEntity are excluded, for real correctness reasons, not
    style: PerEntity is single-measure by construction
    (aggregate_output_schema's own debug_assert, which this rewrite's
    second measure would violate), and without(...) leaves the aggregate's
    own output schema open (closed: false) while the wrapping Project
    this rewrite always produces forces closed: true — a schema drift
    this strategy declines to introduce.
  • replacements: exactly one Replacement::Rewrite candidate — no
    ranking, no "is it worth it" heuristic. Per the issue's own framing:
    earlier drafts needed a manual before/after-CSE cost comparison to
    decide whether rewriting helps; with ReplacementStrategy's
    exhaustive-candidate shape in place that's unnecessary — both the
    original Avg and the rewritten Sum/Count form are just two
    candidates a future cost-based search (issue asap-aware-mapping: Cascades/Volcano-style candidate-plan search engine over ReplacementStrategy #252) can compare, not
    something this strategy pre-decides.
  • The Project's sum / count division is explicitly Cast to
    Float64 so the rewritten avg column matches
    AggIntent::Avg::output_column's own (Float64, nullable: false)
    shape regardless of the summed column's own type — an uncast
    Int64 / Int64 division would otherwise silently retype the column,
    which a test in this PR pins down.

Before / after planner output

Example query:

SELECT job, avg(cpu) FROM metrics GROUP BY job

Beforeavg has no summary realization at all (implementations_for_with dispatches it to PassThrough), so the only candidate keeps the whole aggregate logical — nothing to share or sketch:

replacements(avg_agg) = [
    Summary(KeepPreAsap(Aggregate {
        reduction: by([job]),
        measures: [Avg { col: None }],
        child: Scan(metrics),
    })),
]

AfterAvgToSumOverCountStrategy adds a second candidate: avg reshaped into sum/count under the same grouping, re-divided back by a wrapping Project — two ordinary mergeable accumulators a future sketch-family search / CSE can now actually reuse, where a bare avg node never could:

replacements(avg_agg) = [
    Summary(..),  // unchanged, still offered
    Rewrite(
        Project {
            cols: [Column(0), Cast(Column(1) / Column(2), Float64) AS "avg"],
            child: Aggregate {
                reduction: by([job]),
                measures: [Sum { col: None }, Count { accuracy: Exact }],
                child: Scan(metrics),
            },
        },
    ),
    // rationale: "avg has no summary realization at all (replacement::implementations_for_with
    // dispatches it to PassThrough) ... rewriting it into sum/count ... computes the same result
    // from two ordinary mergeable accumulators SharedSubtreeStrategy (and a future sketch-family
    // search) can actually reuse across the workload"
]

Tests

Positive: an ungrouped and a grouped Avg aggregate both match and
rewrite correctly. Negative: multi-measure, HAVING-bearing, non-Avg
intents, without(...) grouping, Reduction::PerEntity, and non-Aggregate
nodes all correctly don't match. Schema round-tripping:
original.output_schema() == rewritten.output_schema() holds exactly for
an ungrouped aggregate (including an explicit-output-name-override
variant); for a grouped aggregate, column-level equality is asserted
directly, with an explicit test documenting the one known gap (Project
always resets unique_keys, a pre-existing property of every
Project-wrapped rewrite in this IR, not specific to this rewrite —
unique_keys also isn't consumed by anything yet per Schema's own doc
comment). A dedicated test with an Int64-typed summed column exercises
the Cast directly.

lib.rs

Added only pub mod rewrite; — kept minimal since sibling PRs #252/#254/#256
touch the same file from the same base branch in parallel.

Checks

cargo build --workspace --all-targets, cargo test --workspace,
cargo fmt --all -- --check, and
cargo clippy --workspace --all-targets --all-features -- -D warnings all
pass clean.

🤖 Generated with Claude Code

@zzylol
zzylol force-pushed the feat/replacement-strategy-251 branch from ca60c0c to 66c772d Compare August 23, 2026 19:59
@zzylol
zzylol force-pushed the feat/semantic-rewrite-avg-253 branch from cdf4ca8 to 8c019f1 Compare August 24, 2026 14:11
zzylol added a commit that referenced this pull request Aug 24, 2026
…licability.rs

"Site" was an informal synonym introduced when this module's docs were
written — every occurrence names the exact same thing the type system
already calls TargetSubDAG (or, once discovered, a MemoGroup's own
`target`). Replaced every free-standing "site"/"a site's..." with
TargetSubDAG (or "the TargetSubDAG" as a phrase) throughout the module
doc and the OptimizationKind/collect_locations/test doc comments, so
the prose names the real type instead of a parallel, undefined term.
Left two references untouched: `crate::search::discover_sites` (a
real, unrenamed function name in search.rs) and the quoted section
title "Where `for site in plan.bindable_sites()` comes from" (a
verbatim quote of search.rs's own doc heading) — both are accurate
references to search.rs's own content, which this PR doesn't touch.

Also fixed a handful of stale `boundary::`-module doc links found
while in here (the module was renamed to `implementation` well before
this branch's fork point — same staleness class as PR #263/#262/#261/
#260, just not load-bearing for compilation since these were doc-only
intra-doc links).

Verified: cargo build --workspace --all-targets, cargo test
--workspace (0 failures), cargo fmt --all -- --check, cargo clippy
--workspace --all-targets --all-features -- -D warnings — all clean.
…urrent tip

This branch had been forked at the very first draft of #251
(boundary::implementation_for_with, before the boundary.rs ->
implementation.rs rename, the "make binding literally a selector over
ReplacementStrategy" refactor, and this session's deletion of
bind::implement_tree/implement_tree_with) and never rebased — its own
frozen copy of replacement.rs/lib.rs would not compile against the
current tip of feat/replacement-strategy-251.

Rebuilt fresh: reset this branch onto the current #259 tip, then
reapplied only the genuinely new content this PR adds on top —
replacement.rs/bind.rs/implementation.rs now come from #259 itself,
unmodified.

- rewrite.rs (AvgToSumOverCountStrategy, issue #253, part of #33) had
  almost no stale-API surface to begin with: it's a pure pre-ASAP ->
  pre-ASAP QueryExpr rewrite (avg -> sum/count) that only touches
  crate::replacement's stable, unchanged vocabulary
  (Replacement/ReplacementStrategy/ReplacementSubDAG/TargetSubDAG) and
  never called implement_tree_with or touched SketchKind/Implementation
  construction. Reapplied verbatim except for three stale prose spots:
  two `boundary::implementation_for_with` doc/rationale references
  updated to `implementation::implementations_for_with` (the module
  rename), and one `docs/asap_aware_mapping.md` path reference
  corrected to `docs/design_docs/asap_aware_mapping.md` (the design
  doc's actual location on this tip). None of these were load-bearing
  for compilation.
- lib.rs: added `pub mod rewrite;`, a `## Status` doc bullet for the
  new module (mirroring the existing bullets' style; the original PR's
  own lib.rs diff never added one), and `pub use
  rewrite::AvgToSumOverCountStrategy` (also missing from the original
  PR's diff) so the strategy is actually reachable from outside the
  crate the same way its SketchFamilyStrategy/SharedSubtreeStrategy
  siblings are.

Verified: cargo build --workspace --all-targets, cargo test
--workspace (0 failures, including all 12 rewrite:: tests), cargo fmt
--all -- --check, cargo clippy --workspace --all-targets --all-features
-- -D warnings — all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the feat/semantic-rewrite-avg-253 branch from 8c019f1 to 7c90615 Compare August 25, 2026 00:17
@zzylol
zzylol changed the base branch from feat/replacement-strategy-251 to main August 25, 2026 00:17
@zzylol
zzylol merged commit 282de62 into main Aug 25, 2026
3 checks passed
@zzylol
zzylol deleted the feat/semantic-rewrite-avg-253 branch September 7, 2026 15:51
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.

pre-asap: semantic-equivalent rewriting (avg -> sum/count) as a ReplacementStrategy

2 participants