cloud-fan commented on code in PR #59402:
URL: https://github.com/apache/spark/pull/59402#discussion_r4306451250
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/RewriteDistinctAggregatesSuite.scala:
##########
@@ -125,4 +126,685 @@ class RewriteDistinctAggregatesSuite extends PlanTest {
fail(s"Plan is not as expected:\n$rewrite")
}
}
+
+ //
---------------------------------------------------------------------------
+ // COUNT(DISTINCT IF/CASE) canonicalization (SPARK-56898)
+ //
---------------------------------------------------------------------------
+
+ val conditionalTestRelation = LocalRelation(
+ Symbol("a").int, Symbol("b").int, Symbol("c").int, Symbol("d").string)
+
+ private def countDistinctIf(cond: Expression, base: Expression): Expression
= {
+ Count(If(cond, base, Literal(null))).toAggregateExpression(isDistinct =
true)
+ }
+
+ private def countDistinctCaseWhen(cond: Expression, base: Expression):
Expression = {
+ val caseWhen = CaseWhen(
+ Seq((cond, base)),
+ None)
+ Count(caseWhen).toAggregateExpression(isDistinct = true)
+ }
+
+ private def countDistinctCaseWhenElseNull(cond: Expression, base:
Expression): Expression = {
+ val caseWhen = CaseWhen(
+ Seq((cond, base)),
+ Some(Literal(null)))
+ Count(caseWhen).toAggregateExpression(isDistinct = true)
+ }
+
+ private def collectAggregateExpressions(plan: LogicalPlan):
Seq[AggregateExpression] = {
+ plan.collect { case a: Aggregate => a.aggregateExpressions }
+ .flatten
+ .flatMap(_.collect { case ae: AggregateExpression => ae })
+ }
+
+ /**
+ * Asserts that the optimized plan has exactly one Expand node with one
projection,
+ * that the shared distinct key is guarded by the union of the member
filters,
+ * and that the outer aggregate has moved the conditions into FILTER clauses.
+ */
+ private def assertSingleDistinctGroupExpand(
+ optimized: LogicalPlan,
+ baseColName: String,
+ conds: Seq[Expression]): Unit = {
+ // rewrite() adds EqualTo(gid, id) to every distinct aggregate, so
filter.isDefined
+ // alone always passes. Require a user-condition reference (same as
assertCanonicalized).
+ val hasUserConditionFilter = collectAggregateExpressions(optimized)
+ .flatMap(_.filter)
+ .exists(_.references.exists(_.name != "gid"))
+ assert(hasUserConditionFilter,
+ "expected at least one AggregateExpression with a user-condition FILTER
clause")
+ val baseAttr = conditionalTestRelation.output.find(_.name ==
baseColName).get
+ assertGuardedExpandKey(optimized, baseAttr, conds)
+ }
+
+ /**
+ * Asserts that the collapsed distinct group's Expand key is guarded by the
union
+ * of the member filters, i.e. IF(Or(cond1, ..., condN), base, NULL): rows
matching
+ * no predicate project NULL as the distinct key and collapse into a single
key in
+ * the first aggregate, keeping its output cardinality bounded by the
pre-rewrite
+ * plan (distinct(base | Or(conds)) + 1 <= sum_i distinct(base | cond_i) +
N).
+ */
+ private def assertGuardedExpandKey(
+ optimized: LogicalPlan,
+ base: Expression,
+ conds: Seq[Expression]): Unit = {
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 1,
+ s"expected 1 distinct group but got ${expand.projections.size}")
+ val guard = expand.projections.head.collectFirst {
+ case i @ If(_, trueValue, falseValue)
+ if falseValue == nullInt && trueValue.semanticEquals(base) => i
+ }.getOrElse(fail(s"no union guard over $base in Expand
projections:\n$optimized"))
+ val leaves = orLeaves(guard.predicate)
+ assert(leaves.size == conds.size,
+ s"guard condition should cover all ${conds.size} filters:
${guard.predicate}")
+ conds.foreach { c =>
+ assert(leaves.exists(_.semanticEquals(c)),
+ s"guard condition misses $c: ${guard.predicate}")
+ }
+ }
+
+ private def orLeaves(expr: Expression): Seq[Expression] = expr match {
+ case Or(left, right) => orLeaves(left) ++ orLeaves(right)
+ case other => Seq(other)
+ }
+
+ /** EXISTS / IN / NOT IN nodes in Expand projections. The guard must not
copy these. */
+ private def subqueryExprCount(plan: LogicalPlan): Int = {
+ val exprs = plan.collect { case e: Expand => e
}.flatMap(_.projections.flatten)
+ exprs.map(_.collect { case _: Exists | _: InSubquery => () }.size).sum
+ }
+
+ test("conditional: disabled when config is false") {
+ withSQLConf(SQLConf.REWRITE_COUNT_DISTINCT_CONDITIONAL_ENABLED.key ->
"false") {
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ // The general RewriteDistinctAggregates still fires, but conditional
+ // canonicalization is disabled so the two conditional counts stay as 2
+ // distinct groups instead of collapsing to 1.
+ val expands = optimized.collect { case e: Expand => e }
+ assert(expands.head.projections.size == 2,
+ "expected 2 distinct groups when conditional canonicalization is
disabled")
+ }
+ }
+
+ test("conditional: rewrite COUNT(DISTINCT IF(cond, col, NULL)) to
COUNT(DISTINCT col) FILTER") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertSingleDistinctGroupExpand(optimized, "c", Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: rewrite COUNT(DISTINCT CASE WHEN cond THEN col END) to " +
+ "COUNT(DISTINCT col) FILTER") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctCaseWhen(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctCaseWhen(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertSingleDistinctGroupExpand(optimized, "c", Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: rewrite COUNT(DISTINCT CASE WHEN cond THEN col ELSE NULL
END)") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctCaseWhenElseNull(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctCaseWhenElseNull(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertSingleDistinctGroupExpand(optimized, "c", Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: multiple conditional distinct counts collapse to single
distinct group") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Symbol("b") > 3, Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ // All three counts share the same base column c, collapsed to 1 distinct
group.
+ assertSingleDistinctGroupExpand(
+ optimized, "c", Seq(bAttr > 1, bAttr > 2, bAttr > 3))
+ }
+
+ test("conditional: guarded Expand key is IF(union of conditions, base,
NULL)") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: guarded Expand key covers CASE WHEN conditions too") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctCaseWhen(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctCaseWhenElseNull(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: guarded Expand key covers all conditions of a 3-count
collapse") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Symbol("b") > 3, Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2, bAttr >
3))
+ }
+
+ test("conditional: user-written FILTER counts sharing a base get the union
guard") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 1)).as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 2)).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: config false disables union guard for user-written
FILTER") {
+ // The config is the kill switch for both IF/CASE normalize and the
union-of-filters
+ // guard. With it off, user-written FILTER distincts still Expand
(mustRewrite) but
+ // keep the raw base as the Expand key.
+ withSQLConf(SQLConf.REWRITE_COUNT_DISTINCT_CONDITIONAL_ENABLED.key ->
"false") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 1))
+ .as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 2))
+ .as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 1)
+ assert(expand.projections.head.exists(_.semanticEquals(base)),
+ "raw base expected when rewriteCountDistinctConditional is disabled")
+ assert(
+ !expand.projections.head.exists {
+ case If(_, trueValue, falseValue) =>
+ falseValue == nullInt && trueValue.semanticEquals(base)
+ case _ => false
+ },
+ "union guard should be off when rewriteCountDistinctConditional is
disabled")
+ }
+ }
+
+ test("conditional: unfiltered count in the group keeps the raw base key") {
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true).as("cnt0"),
+ Count(base).toAggregateExpression(isDistinct = true,
+ filter = Some(Symbol("b") > 1)).as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true,
+ filter = Some(Symbol("b") > 2)).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // One distinct group {c}, but the unfiltered count needs the raw child
over all
+ // rows, so the Expand key must not be guarded.
+ assert(expand.projections.size == 1)
+ assert(expand.projections.head.exists(_.semanticEquals(base)),
+ "raw base attribute expected when the group contains an unfiltered
count")
+ }
+
+ test("conditional: non-deterministic filter keeps the raw base key") {
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val nondet = Rand(42) < Literal(0.5, DoubleType)
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(nondet)).as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(nondet)).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // The guard re-evaluates the filters, so non-deterministic ones must not
be
+ // guarded; the Expand key stays the raw attribute.
+ assert(expand.projections.head.exists(_.semanticEquals(base)),
+ "raw base attribute expected with a non-deterministic filter")
+ }
+
+ test("conditional: EXISTS, IN, and NOT IN filters keep the raw key") {
+ // RewritePredicateSubquery has not run. Copying a predicate subquery into
the
+ // Expand key builds a second ExistenceJoin later, so the key stays raw
and each
+ // predicate appears once, as its flag column.
+ val dim = LocalRelation(Symbol("x").int)
+ val existsPred = Exists(dim)
+ val inPred = InSubquery(Seq(Symbol("b")), ListQuery(dim, numCols = 1))
+ val notInPred = Not(InSubquery(Seq(Symbol("c")), ListQuery(dim, numCols =
1)))
+ val b = conditionalTestRelation.output.find(_.name == "b").get
+ val c = conditionalTestRelation.output.find(_.name == "c").get
+ val d = conditionalTestRelation.output.find(_.name == "d").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(b).toAggregateExpression(isDistinct = true, filter =
Some(existsPred)).as("cnt1"),
+ Count(c).toAggregateExpression(isDistinct = true, filter =
Some(inPred)).as("cnt2"),
+ Count(d).toAggregateExpression(isDistinct = true, filter =
Some(notInPred)).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 3)
+ Seq(b, c, d).foreach { base =>
+ val nullOfBase = Literal.create(null, base.dataType)
+ assert(expand.projections.exists(_.exists(_.semanticEquals(base))),
+ s"raw key expected for $base")
+ assert(!expand.projections.exists(_.exists {
+ case If(_, trueValue, falseValue) =>
+ falseValue.semanticEquals(nullOfBase) &&
trueValue.semanticEquals(base)
+ case _ => false
+ }), s"subquery filter must not guard $base")
+ }
+ assert(subqueryExprCount(optimized) == 3,
+ "each predicate subquery should appear once in the Expand")
+ }
+
+ test("conditional: do not canonicalize counts whose condition is a predicate
subquery") {
+ // Normalizing would merge these into one group that cannot be guarded,
and Expand
+ // would project the raw base. Leave each IF/CASE in its own group.
+ val dim = LocalRelation(Symbol("x").int)
+ val x = dim.output.head
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Exists(dim.where(x > 0)), Symbol("c")).as("cnt1"),
+ countDistinctCaseWhen(Exists(dim.where(x > 1)),
Symbol("c")).as("cnt2"),
+ countDistinctCaseWhenElseNull(Exists(dim.where(x > 2)),
Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 3,
+ "subquery conditions should stay 3 distinct groups")
+ val hasUserConditionFilter = collectAggregateExpressions(optimized)
+ .flatMap(_.filter)
+ .exists(_.references.exists(_.name != "gid"))
+ assert(!hasUserConditionFilter,
+ "subquery condition should not become a user-condition FILTER clause")
+ assert(subqueryExprCount(optimized) == 3,
+ "each predicate subquery should appear once in the Expand")
+ }
+
+ test("conditional: subquery-free filters stay guarded beside a subquery
condition") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val dim = LocalRelation(Symbol("x").int)
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Exists(dim), Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // The two plain predicates share one guarded group. The EXISTS count
stays separate.
+ assert(expand.projections.size == 2)
+ val guard = expand.projections.flatten.collectFirst {
+ case i @ If(_, trueValue, falseValue)
+ if falseValue == nullInt && trueValue.semanticEquals(base) => i
+ }.getOrElse(fail(s"no union guard over c:\n$optimized"))
+ val leaves = orLeaves(guard.predicate)
+ assert(leaves.size == 2, s"guard should cover only the plain predicates:
${guard.predicate}")
+ Seq(bAttr > 1, bAttr > 2).foreach { cond =>
+ assert(leaves.exists(_.semanticEquals(cond)), s"guard misses $cond:
${guard.predicate}")
+ }
+ assert(subqueryExprCount(optimized) == 1,
+ "the EXISTS condition should appear once, in its own group")
+ }
+
+ test("conditional: throwable distinct child keeps the raw key") {
+ // Division can throw under ANSI on rows the filter rejects. Wrapping it in
+ // the guard would skip those evaluations, so the Expand key stays raw.
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(Symbol("c") / Symbol("b")).toAggregateExpression(
+ isDistinct = true, filter = Some(Symbol("a") > 0)).as("cnt"))
+ .analyze
+ val child = input.collectFirst {
+ case a: Aggregate =>
+ a.aggregateExpressions.flatMap(_.collect {
+ case ae: AggregateExpression if ae.isDistinct =>
+ ae.aggregateFunction.children.headOption
+ }).flatten.head
+ }.get
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.head.exists(_.semanticEquals(child)),
+ s"raw distinct child expected, projections:\n${expand.projections.head}")
+ }
+
+ test("conditional: single conditional distinct count is gated out by
mayNeedtoRewrite") {
+ // A lone COUNT(DISTINCT IF(...)) must NOT be pushed onto the Expand path.
+ // The canonicalization runs inside rewrite(), which is only called when
+ // mayNeedtoRewrite returns true. A single conditional distinct count has
no filter
+ // and forms only one distinct group, so mayNeedtoRewrite returns false,
rewrite()
+ // is never called, and no Expand is produced.
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expands = optimized.collect { case e: Expand => e }
+ assert(expands.isEmpty, "single conditional distinct count should not
produce an Expand")
+ }
+
+ test("conditional: COUNT+SUM sharing IF keeps single-distinct path") {
+ // Both share one unfiltered distinct child. Normalizing only the COUNT
would
+ // split the group into {c} and {IF(b>1,c,NULL)} and force a 2-projection
Expand.
+ // Keep the original aggregate so AggregationStrategy handles it without
Expand.
+ // Use the same null literal as countDistinctIf so the IF children are one
+ // ExpressionSet key after analysis (as in SQL COUNT/SUM over the same IF).
+ val sharedIf = If(Symbol("b") > 1, Symbol("c"), Literal(null))
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(sharedIf).toAggregateExpression(isDistinct = true).as("cnt"),
+ Sum(sharedIf).toAggregateExpression(isDistinct = true).as("sum"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ comparePlans(optimized, input)
+ }
+
+ test("conditional: IF shared with an un-normalizable distinct agg is not
normalized") {
+ // COUNT(DISTINCT IF(b>1, c, NULL)) shares its IF child with SUM(DISTINCT
IF(...)),
+ // which is not canonicalized. Normalizing the COUNT would move it into
the guarded
+ // {c} group while the SUM keeps the {IF(b>1, c, NULL)} group, so rows
matching b>1
+ // would carry their c keys in both groups, roughly doubling the first
aggregate's
+ // output. The count must stay in the shared group; only the other two
conditional
+ // counts collapse, and their guard covers exactly b>2 and b>3.
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val sharedIf = If(Symbol("b") > 1, Symbol("c"), Literal(null))
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(sharedIf).toAggregateExpression(isDistinct = true).as("cnt1"),
+ Sum(sharedIf).toAggregateExpression(isDistinct = true).as("sum1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Symbol("b") > 3, Symbol("c")).as("cnt3"))
+ .analyze
+ // The resolved shared IF child of the un-normalized COUNT and SUM.
+ val analyzedSharedIf = input.collectFirst {
+ case a: Aggregate => a.aggregateExpressions.flatMap(_.collect {
+ case ae: AggregateExpression
+ if ae.isDistinct && ae.aggregateFunction.isInstanceOf[Sum] =>
+ ae.aggregateFunction.children.head
+ }).head
+ }.get
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // Two distinct groups: {IF(b>1, c, NULL)} serving cnt1 and sum1, and the
guarded
+ // {c} group serving cnt2 and cnt3.
+ assert(expand.projections.size == 2,
+ s"expected 2 distinct groups:\n$optimized")
+
assert(expand.projections.exists(_.exists(_.semanticEquals(analyzedSharedIf))),
+ s"shared IF should stay a raw distinct key:\n$optimized")
+ // Skip the shared IF, whose trueValue is also c.
+ val guard = expand.projections.flatten.collectFirst {
+ case i @ If(_, trueValue, falseValue)
+ if falseValue == nullInt && trueValue.semanticEquals(base) &&
+ !i.semanticEquals(analyzedSharedIf) => i
+ }.getOrElse(fail(s"no union guard over c in Expand
projections:\n$optimized"))
+ val leaves = orLeaves(guard.predicate)
+ assert(leaves.size == 2,
+ s"guard condition should cover exactly b>2 and b>3: ${guard.predicate}")
+ Seq(bAttr > 2, bAttr > 3).foreach { cond =>
+ assert(leaves.exists(_.semanticEquals(cond)),
+ s"guard condition misses $cond: ${guard.predicate}")
+ }
+ }
+
+ test("conditional: repeated COUNT(DISTINCT IF) with COUNT(*) keeps
single-distinct path") {
+ // Two identical conditional counts form one unfiltered distinct group.
Normalizing
+ // them to FILTER form would make mustRewrite true and introduce an Expand
+ // (regular branch + distinct branch) with no fan-out reduction.
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt"),
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt2"),
+ Count(Literal(1)).toAggregateExpression().as("n"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ comparePlans(optimized, input)
+ }
+
+ test("conditional: do not rewrite IF with non-null else branch") {
Review Comment:
**Nit (P3):** This test covers the IF form of the null-else admission check,
but its CASE mirror has no test. The single-branch `CaseWhen(Seq((cond, base)),
Some(e))` arm of `extractCondAndBase` (RewriteDistinctAggregates.scala:570)
admits a count only when `isNullExpr(e)` holds. That is the only thing that
stops `COUNT(DISTINCT CASE WHEN cond THEN base ELSE <non-null> END)` from
becoming `COUNT(DISTINCT base) FILTER (WHERE cond)`. `SimplifyConditionals`
leaves a single-branch CASE WHEN as a `CaseWhen` and only drops `ELSE NULL`, so
SQL queries reach this arm with exactly the non-null ELSE shapes. If the check
were dropped, two `COUNT(DISTINCT CASE WHEN b > k THEN c ELSE 0 END)` counts on
one base would collapse into one guarded group, and every group with a
non-matching row would lose the `0` key and report one fewer distinct value. No
test in this suite or in `RewriteDistinctAggregatesConditionalQuerySuite`
builds that shape, so every test would still pass.
**Verification:**
- **Regression:** Two conditional distinct counts written as CASE WHEN cond
THEN base ELSE a non-null literal END on the same base stay as two distinct
Expand groups with no user-condition FILTER, and the case fails if the CASE
arm's null-else check is removed.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]