Skip to content

feat: rewrite uniqExact to COUNT(DISTINCT ...) - #221

Merged
zzylol merged 2 commits into
mainfrom
uniqexact-countdistinct
Aug 22, 2026
Merged

zzylol merged 2 commits into
mainfrom
uniqexact-countdistinct

Conversation

@milindsrivastava1997

@milindsrivastava1997 milindsrivastava1997 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • ClickHouse's uniqExact(x) has no DataFusion planner equivalent and always failed at "unknown function". Register a stub UDAF plus a FunctionRewrite that turns every call into count(x) DISTINCT before lower_plan runs — lower_agg_intent already maps that to AggIntent::Cardinality, so it needs no ClickHouse-specific name.
  • Using just the ApplyFunctionRewrites rule because Analyzer::execute_and_check will call other rules that are currently not needed.

Coverage

  • bgp_jan2024_workload (200-query corpus): Lowered 64 → 85, Plan 105 (was 127).
  • bgp_analytics queries 6 and 12 now lower end to end.

Known limitations (not fixed here)

  • Multi-argument uniqExact(a, b, c) isn't handled — the stub UDAF's signature accepts exactly one arg.
  • NULL-handling parity between ClickHouse's uniqExact and SQL COUNT(DISTINCT x) wasn't verified.

Test plan

  • cargo test -p asap-frontend-sql (all suites green, zero test changes needed beyond the pinned-coverage corpus tallies)
  • cargo test --workspace (green)
  • cargo clippy -p asap-frontend-sql --all-targets (clean)
  • cargo fmt -p asap-frontend-sql -- --check (clean)

🤖 Generated with Claude Code

milindsrivastava1997 and others added 2 commits August 21, 2026 15:59
…ion Analyzer

uniqExact(x) has no DataFusion planner equivalent, so it always failed at
"unknown function". Register a stub UDAF so the name resolves, then a
FunctionRewrite that turns every call into count(x) DISTINCT before
lower_plan runs -- lower_agg_intent already maps that to
AggIntent::Cardinality, so it needs no ClickHouse-specific name of its own.

Making the rewrite fire requires running DataFusion's Analyzer (a
FunctionRewrite only applies as part of one), which the SQL front end
previously skipped entirely. Scoped to a standalone zero-rule Analyzer
carrying only this rewrite, not the default 5-rule one, to avoid pulling in
unrelated normalization (TypeCoercion, ExpandWildcardRule, ...) for every SQL
query in the system. One unavoidable side effect: the Analyzer's
unconditional post-check now rejects a multi-column IN (subquery) before
lower_in_subquery's own arity check runs, with a different error message
(same rejection either way) -- one test updated to match.

bgp_jan2024_workload corpus: Lowered 64 -> 85, Plan 105 (was 127).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…_and_check

Analyzer::execute_and_check (even with an otherwise-empty rule set) runs an
unconditional post-check baked into that method, not gated by which rules
are registered -- it independently rejected a multi-column IN (subquery)
before lower_in_subquery's own arity check got a chance to.

ApplyFunctionRewrites is the actual AnalyzerRule DataFusion's own Analyzer
uses to apply FunctionRewrites; its analyze() already does a full
transform_up_with_subqueries over the whole plan, so it needs no wrapping
Analyzer at all. Calling it directly skips that unconditional check
entirely: zero behavior change for every query that doesn't call uniqExact.
Reverts the sql_lowering test for the multi-column IN (subquery) case, now
back to its original assertion.

Coverage unchanged (bgp_jan2024_workload: Lowered 85, Plan 105).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@milindsrivastava1997
milindsrivastava1997 marked this pull request as ready for review August 21, 2026 21:15
@zzylol
zzylol merged commit cb70086 into main Aug 22, 2026
7 of 8 checks passed
@zzylol
zzylol deleted the uniqexact-countdistinct branch August 22, 2026 17:08
zzylol added a commit that referenced this pull request Aug 22, 2026
…f rewrite (#227)

Pulls the SQL aggregate-function name -> canonical AggIntent mapping out of
frontend-sql::lower_agg_intent's hand-written match and into a new
independent crate, asap-sql-function-catalog (issue #225), structured after
tobilg/polyglot's polyglot-sql-function-catalogs: flat data (name, arity,
canonical semantic), no dependency on the SQL front end or DataFusion.
lower_agg_intent keeps only call-site logic that isn't a function of the
name alone (the DISTINCT rule, the bare-column reducer-argument rule, phi
extraction, the ambient AccuracyTarget).

Also generalizes uniqExact's stub-UDAF + FunctionRewrite mechanism (#221)
so a new ClickHouse-only builtin DataFusion doesn't know at all is a
catalog data entry (name, arity, RewriteKind) rather than a new one-off
Rust type and a new one-off stub-UDAF function. uniqExact and the new
countIf entry both go through the single ClickHouseBuiltinRewrite +
clickhouse_builtin_stub_udaf mechanism now. countIf(cond) rewrites to
sum(CASE WHEN cond THEN 1 ELSE 0 END) rather than count(...) FILTER
(WHERE cond), since AggIntent::Count never consults its argument (it
always means "row count") and would silently drop the filter; summing a
0/1 indicator keeps cond observable through the existing Sum path,
including the general non-column-argument materialization
lower_aggregate already does (#110).

uniqExact's own behavior is unchanged (same signature, same rewrite
target) and its existing corpus tests (bgp_analytics, #221) pass
unmodified. countIf support moves 13 bgp_jan2024_workload corpus queries
out of Category::Plan (12 now lower end to end, one hits a second,
pre-existing NotImplemented gap) -- the pinned tally is updated
accordingly. New direct unit tests cover countIf's derived-column shape
and composition with GROUP BY.

Deliberately out of scope (per the issue): generating the catalog from a
live ClickHouse/DataFusion introspection source (item 3 -- flagged in the
issue as needing a decision on where such tooling would run).

Co-authored-by: Claude Sonnet 5 <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.

2 participants