Skip to content

feat(sql): lower median / approx_median to the φ=0.5 quantile (#111) - #117

Merged
zzylol merged 1 commit into
mainfrom
feat/111-median
Jul 9, 2026
Merged

zzylol merged 1 commit into
mainfrom
feat/111-median

Conversation

@zzylol

@zzylol zzylol commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Part of #111 (the median item; subquery predicates, DISTINCT ON and array_agg remain open there).

Change

median(c) and approx_median(c) reached the aggregate map in crates/frontend-sql/src/sql/mod.rs and fell through to unsupported aggregate. Both are the φ=0.5 quantile, which the intent algebra already models:

"median" | "approx_median" => (AggFunc::Quantile(0.5), reducer_col(&name, &agg_fn.args)?),

Three properties fall out of the existing machinery rather than being special-cased:

  • Column binding. reducer_col resolves the argument, so median(latency) and median(bytes) produce distinct intents — and median(a * b) is rejected instead of silently lowering with col: None. Both inherited from L3: AggIntent::Quantile / Cardinality / TopK drop their input column — distinct aggregates compare equal #115.
  • Accuracy. approx_median is not forced to approximate. The sketch-vs-exact choice is the AccuracyTarget’s (plan::boundary), matching how approx_distinct and approx_percentile_cont already behave. Under Epsilon(0.01) it binds a quantile sketch; under Exact it passes through.
  • Canonical form. median(c) and approx_percentile_cont(c, 0.5) lower to the same intent, so plan::cse merges the two spellings.

Tests (4 new)

test pins
median_lowers_to_the_half_quantile both spellings → Quantile { col: Some(2), q: 0.5 }
median_is_the_same_intent_as_an_explicit_half_percentile canonical form
median_threads_the_accuracy_target approx_median under Epsilon(0.01)
median_over_an_expression_is_rejected inherits the #115 rule

Verification

cargo test --workspace      # 367 passed, 0 failed  (+4)
cargo clippy --all-targets  # clean

cargo fmt --all was not run — main carries 159 pre-existing rustfmt diffs, so it would bury the change. Each touched file has the same violation count as on main.

Note on array_agg

The other aggregate in #111. It has no sketch or summary representation and pre-aggregates nothing, so it does not belong in the intent algebra. I would close that item as working-as-intended rather than implement it — happy to be overruled.

🤖 Generated with Claude Code

`median(c)` and `approx_median(c)` reached the aggregate map and were rejected
as `unsupported aggregate`. Both are the φ=0.5 quantile, which the intent
algebra already has: `AggIntent::Quantile { col, q: 0.5, accuracy }`.

They share one intent with `approx_percentile_cont(c, 0.5)`, so CSE can merge
the two spellings. The `approx_` prefix does not force an approximation — the
sketch-vs-exact choice belongs to the AccuracyTarget (`plan::boundary`), which
is how `approx_distinct` and `approx_percentile_cont` already behave.

The argument resolves through `reducer_col`, so `median(a * b)` is rejected
rather than silently lowered with no column (#115).

This closes the `median` part of #111; subquery predicates, DISTINCT ON, and
array_agg remain open there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit e66a6e0 into main Jul 9, 2026
1 check passed
@zzylol
zzylol deleted the feat/111-median branch July 9, 2026 18:28
zzylol added a commit that referenced this pull request Jul 10, 2026
…122)

`array_agg` has no test, so "deliberately rejected" and "not implemented yet"
look identical in the code. Assert the rejection, with the reasoning inline.

AggIntent is the vocabulary the planner binds sketches and mergeable
accumulators to. array_agg pre-aggregates nothing (output is O(input rows)),
has no bounded-memory approximate form, and its partial state is the data
itself. Since plan::boundary::realize is an exhaustive match, an
AggIntent::ArrayAgg would force boundary, bind and schema derivation to each
handle a variant whose every answer is PassThrough.

Contrast median (#117), from the same bucket in #111: it *is* Quantile{q:0.5}
and feeds the sketch path, so it was implemented rather than rejected.

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