This is an automated email from the ASF dual-hosted git repository.
lgbo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-gluten.git
The following commit(s) were added to refs/heads/main by this push:
new bb161098fa [GLUTEN-8675][CH] Rewrite union of multiple aggregates into
one (#8676)
bb161098fa is described below
commit bb161098faa39dae00a1159391258de2494c8fc1
Author: lgbo <[email protected]>
AuthorDate: Tue Feb 11 20:11:46 2025 +0800
[GLUTEN-8675][CH] Rewrite union of multiple aggregates into one (#8676)
* stage
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
---
.../gluten/backendsapi/clickhouse/CHBackend.scala | 3 +
.../gluten/backendsapi/clickhouse/CHRuleApi.scala | 1 +
.../extension/CoalesceAggregationUnion.scala | 818 +++++++++++++++++++++
.../GlutenCoalesceAggregationUnionSuite.scala | 395 ++++++++++
4 files changed, 1217 insertions(+)
diff --git
a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
index b0f58418ef..31a4fa156a 100644
---
a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
+++
b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
@@ -156,6 +156,9 @@ object CHBackendSettings extends BackendSettingsApi with
Logging {
CHConfig.prefixOf("convert.left.anti_semi.to.right")
val GLUTEN_CLICKHOUSE_CONVERT_LEFT_ANTI_SEMI_TO_RIGHT_DEFAULT_VALUE: String
= "false"
+ val GLUTEN_ENABLE_COALESCE_AGGREGATION_UNION: String =
+ CHConf.prefixOf("enable.coalesce.aggregation.union")
+
def affinityMode: String = {
SparkEnv.get.conf
.get(
diff --git
a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHRuleApi.scala
b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHRuleApi.scala
index 40344e96e7..ecd7e5a241 100644
---
a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHRuleApi.scala
+++
b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHRuleApi.scala
@@ -60,6 +60,7 @@ object CHRuleApi {
(spark, parserInterface) => new GlutenCacheFilesSqlParser(spark,
parserInterface))
injector.injectParser(
(spark, parserInterface) => new GlutenClickhouseSqlParser(spark,
parserInterface))
+ injector.injectResolutionRule(spark => new CoalesceAggregationUnion(spark))
injector.injectResolutionRule(spark => new
RewriteToDateExpresstionRule(spark))
injector.injectResolutionRule(spark => new
RewriteDateTimestampComparisonRule(spark))
injector.injectResolutionRule(spark => new
CollapseGetJsonObjectExpressionRule(spark))
diff --git
a/backends-clickhouse/src/main/scala/org/apache/gluten/extension/CoalesceAggregationUnion.scala
b/backends-clickhouse/src/main/scala/org/apache/gluten/extension/CoalesceAggregationUnion.scala
new file mode 100644
index 0000000000..4a83830e51
--- /dev/null
+++
b/backends-clickhouse/src/main/scala/org/apache/gluten/extension/CoalesceAggregationUnion.scala
@@ -0,0 +1,818 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.extension
+
+import org.apache.gluten.backendsapi.clickhouse.CHBackendSettings
+import org.apache.gluten.exception.GlutenNotSupportException
+
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.catalyst.analysis.MultiInstanceRelation
+import org.apache.spark.sql.catalyst.catalog.HiveTableRelation
+import org.apache.spark.sql.catalyst.expressions._
+import org.apache.spark.sql.catalyst.expressions.aggregate._
+import org.apache.spark.sql.catalyst.plans.logical._
+import org.apache.spark.sql.catalyst.rules.Rule
+import org.apache.spark.sql.execution.datasources._
+import org.apache.spark.sql.types._
+import org.apache.spark.unsafe.types.UTF8String
+
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+import scala.util.{Failure, Success, Try}
+
+/*
+ * Example:
+ * Rewrite query
+ * SELECT a, b, sum(c) FROM t WHERE d = 1 GROUP BY a,b
+ * UNION ALL
+ * SELECT a, b, sum(c) FROM t WHERE d = 2 GROUP BY a,b
+ * into
+ * SELECT a, b, sum(c) FROM (
+ * SELECT s.a as a, s.b as b, s.c as c, s.id as group_id FROM (
+ * SELECT explode(s) as s FROM (
+ * SELECT array(
+ * if(d = 1, named_struct('a', a, 'b', b, 'c', c, 'id', 0), null),
+ * if(d = 2, named_struct('a', a, 'b', b, 'c', c, 'id', 1), null)) as s
+ * FROM t WHERE d = 1 OR d = 2
+ * )
+ * ) WHERE s is not null
+ * ) GROUP BY a,b, group_id
+ *
+ * The first query need to scan `t` multiply, when the output of scan is
large, the query is
+ * really slow. The rewritten query only scan `t` once, and the performance is
much better.
+ */
+
+class CoalesceAggregationUnion(spark: SparkSession) extends Rule[LogicalPlan]
with Logging {
+ def removeAlias(e: Expression): Expression = {
+ e match {
+ case alias: Alias => alias.child
+ case _ => e
+ }
+ }
+
+ def hasAggregateExpression(e: Expression): Boolean = {
+ if (e.children.isEmpty && !e.isInstanceOf[AggregateExpression]) {
+ return false
+ }
+ e match {
+ case _: AggregateExpression => true
+ case _ => e.children.exists(hasAggregateExpression(_))
+ }
+ }
+
+ def isAggregateExpression(e: Expression): Boolean = {
+ e match {
+ case cast: Cast => isAggregateExpression(cast.child)
+ case alias: Alias => isAggregateExpression(alias.child)
+ case agg: AggregateExpression => true
+ case _ => false
+ }
+ }
+
+ def hasAggregateExpressionsWithFilter(e: Expression): Boolean = {
+ if (e.children.isEmpty && !e.isInstanceOf[AggregateExpression]) {
+ return false
+ }
+ e match {
+ case aggExpr: AggregateExpression =>
+ aggExpr.filter.isDefined
+ case _ => e.children.exists(hasAggregateExpressionsWithFilter(_))
+ }
+ }
+
+ def buildAttributesMap(
+ attributes: Seq[Attribute],
+ expressions: Seq[Expression]): Map[ExprId, Expression] = {
+ assert(attributes.length == expressions.length)
+ val map = new mutable.HashMap[ExprId, Expression]()
+ attributes.zip(expressions).foreach {
+ case (attr, expr) =>
+ map.put(attr.exprId, expr)
+ }
+ map.toMap
+ }
+
+ def replaceAttributes(e: Expression, replaceMap: Map[ExprId, Expression]):
Expression = {
+ e match {
+ case attr: Attribute =>
+ replaceMap.get(attr.exprId) match {
+ case Some(replaceAttr) => replaceAttr
+ case None =>
+ throw new GlutenNotSupportException(s"Not found attribute: $attr
${attr.qualifiedName}")
+ }
+ case _ =>
+ e.withNewChildren(e.children.map(replaceAttributes(_, replaceMap)))
+ }
+ }
+
+ case class AggregateAnalzyInfo(originalAggregate: Aggregate) {
+ protected def extractFilter(): Option[Filter] = {
+ originalAggregate.child match {
+ case filter: Filter => Some(filter)
+ case project @ Project(_, filter: Filter) => Some(filter)
+ case subquery: SubqueryAlias =>
+ subquery.child match {
+ case filter: Filter => Some(filter)
+ case project @ Project(_, filter: Filter) => Some(filter)
+ case relation if isRelation(relation) =>
+ Some(Filter(Literal(true, BooleanType), subquery))
+ case nestedRelation: SubqueryAlias if
(isRelation(nestedRelation.child)) =>
+ Some(Filter(Literal(true, BooleanType), nestedRelation))
+ case _ => None
+ }
+ case _ => None
+ }
+ }
+
+ def isValidSource(plan: LogicalPlan): Boolean = {
+ plan match {
+ case relation if isRelation(relation) => true
+ case _: Project | _: Filter | _: SubqueryAlias =>
+ plan.children.forall(isValidSource)
+ case _ => false
+ }
+ }
+
+ // Try to make the plan simple, contain only three steps, source, filter,
aggregate.
+ lazy val extractedSourcePlan = {
+ val filter = extractFilter()
+ if (!filter.isDefined) {
+ None
+ } else {
+ filter.get.child match {
+ case project: Project if isValidSource(project.child) =>
Some(project.child)
+ case other if isValidSource(other) => Some(other)
+ case _ => None
+ }
+ }
+ }
+
+ lazy val constructedFilterPlan = {
+ val filter = extractFilter()
+ if (!filter.isDefined || !extractedSourcePlan.isDefined) {
+ None
+ } else {
+ val project = filter.get.child match {
+ case project: Project => Some(project)
+ case other =>
+ None
+ }
+ val newFilter = project match {
+ case Some(project) =>
+ val replaceMap = buildAttributesMap(
+ project.output,
+ project.child.output.map(_.asInstanceOf[Expression]))
+ val newCondition = replaceAttributes(filter.get.condition,
replaceMap)
+ Filter(newCondition, extractedSourcePlan.get)
+ case None => filter.get.withNewChildren(Seq(extractedSourcePlan.get))
+ }
+ Some(newFilter)
+ }
+ }
+
+ lazy val constructedAggregatePlan = {
+ if (!constructedFilterPlan.isDefined) {
+ None
+ } else {
+ val project = originalAggregate.child match {
+ case p: Project => Some(p)
+ case subquery: SubqueryAlias =>
+ subquery.child match {
+ case p: Project => Some(p)
+ case _ => None
+ }
+ case _ => None
+ }
+
+ val newAggregate = project match {
+ case Some(innerProject) =>
+ val replaceMap = buildAttributesMap(
+ innerProject.output,
+ innerProject.projectList.map(_.asInstanceOf[Expression]))
+ val newGroupExpressions =
originalAggregate.groupingExpressions.map {
+ e => replaceAttributes(e, replaceMap)
+ }
+ val newAggregateExpressions =
originalAggregate.aggregateExpressions.map {
+ e => replaceAttributes(e,
replaceMap).asInstanceOf[NamedExpression]
+ }
+ Aggregate(newGroupExpressions, newAggregateExpressions,
constructedFilterPlan.get)
+ case None =>
originalAggregate.withNewChildren(Seq(constructedFilterPlan.get))
+ }
+ Some(newAggregate)
+ }
+ }
+
+ lazy val hasAggregateWithFilter =
originalAggregate.aggregateExpressions.exists {
+ e => hasAggregateExpressionsWithFilter(e)
+ }
+
+ // The output results which are not aggregate expressions.
+ lazy val resultGroupingExpressions = constructedAggregatePlan match {
+ case Some(agg) =>
+ agg.asInstanceOf[Aggregate].aggregateExpressions.filter(e =>
!hasAggregateExpression(e))
+ case None => Seq.empty
+ }
+
+ lazy val positionInGroupingKeys = {
+ var i = 0
+ // In most cases, the expressions which are not aggregate result could
be matched with one of
+ // groupingk keys. There are some exceptions
+ // 1. The expression is a literal. The grouping keys do not contain the
literal.
+ // 2. The expression is an expression withs gruping keys. For example,
+ // `select k1 + k2, count(1) from t group by k1, k2`.
+ resultGroupingExpressions.map {
+ e =>
+ val aggregate = constructedAggregatePlan.get.asInstanceOf[Aggregate]
+ e match {
+ case literal @ Alias(_: Literal, _) =>
+ var idx = aggregate.groupingExpressions.indexOf(e)
+ if (idx == -1) {
+ idx = aggregate.groupingExpressions.length + i
+ i += 1
+ }
+ idx
+ case _ =>
+ var idx = aggregate.groupingExpressions.indexOf(removeAlias(e))
+ idx = if (idx == -1) {
+ aggregate.groupingExpressions.indexOf(e)
+ } else {
+ idx
+ }
+ idx
+ }
+ }
+ }
+ }
+
+ /*
+ * Case class representing an analyzed plan.
+ *
+ * @param plan The logical plan that to be analyzed.
+ * @param analyzedInfo Optional information about the aggregate analysis.
+ */
+ case class AnalyzedPlan(plan: LogicalPlan, analyzedInfo:
Option[AggregateAnalzyInfo])
+
+ def isResolvedPlan(plan: LogicalPlan): Boolean = {
+ plan match {
+ case isnert: InsertIntoStatement => isnert.query.resolved
+ case _ => plan.resolved
+ }
+ }
+
+ override def apply(plan: LogicalPlan): LogicalPlan = {
+ if (
+ spark.conf
+ .get(CHBackendSettings.GLUTEN_ENABLE_COALESCE_AGGREGATION_UNION,
"true")
+ .toBoolean && isResolvedPlan(plan)
+ ) {
+ Try {
+ visitPlan(plan)
+ } match {
+ case Success(res) => res
+ case Failure(e) => plan
+ }
+ } else {
+ plan
+ }
+ }
+
+ def visitPlan(plan: LogicalPlan): LogicalPlan = {
+ plan match {
+ case union: Union =>
+ val planGroups = groupStructureMatchedAggregate(union)
+ if (planGroups.forall(group => group.length == 1)) {
+ plan.withNewChildren(plan.children.map(visitPlan))
+ } else {
+ val newUnionClauses = planGroups.map {
+ groupedPlans =>
+ if (groupedPlans.length == 1) {
+ groupedPlans.head.plan
+ } else {
+ val firstAggregateAnalzyInfo =
groupedPlans.head.analyzedInfo.get
+ val aggregates =
groupedPlans.map(_.analyzedInfo.get.constructedAggregatePlan.get)
+ val filterConditions =
buildAggregateCasesConditions(groupedPlans)
+ val firstAggregateFilter =
+
firstAggregateAnalzyInfo.constructedFilterPlan.get.asInstanceOf[Filter]
+
+ // Add a filter step with condition `cond1 or cond2 or ...`,
`cond_i` comes from
+ // each union clause. Apply this filter on the source plan.
+ val unionFilter = Filter(
+ buildUnionConditionForAggregateSource(filterConditions),
+ firstAggregateAnalzyInfo.extractedSourcePlan.get)
+
+ // Wrap all the attributes into a single structure attribute.
+ val wrappedAttributesProject =
+ buildProjectFoldIntoStruct(unionFilter, groupedPlans,
filterConditions)
+
+ // Build an array which element are response to each union
clause.
+ val arrayProject =
+ buildProjectBranchArray(wrappedAttributesProject,
filterConditions)
+
+ // Explode the array
+ val explode = buildExplodeBranchArray(arrayProject)
+
+ // Null value means that the union clause does not have the
corresponding data.
+ val notNullFilter = Filter(IsNotNull(explode.output.head),
explode)
+
+ // Destruct the struct attribute.
+ val destructStructProject =
buildProjectUnfoldStruct(notNullFilter)
+
+ buildAggregateWithGroupId(destructStructProject, groupedPlans)
+ }
+ }
+ val coalesePlan = if (newUnionClauses.length == 1) {
+ newUnionClauses.head
+ } else {
+ var firstUnionChild = newUnionClauses.head
+ for (i <- 1 until newUnionClauses.length - 1) {
+ firstUnionChild = Union(firstUnionChild, newUnionClauses(i))
+ }
+ Union(firstUnionChild, newUnionClauses.last)
+ }
+
+ // We need to keep the output atrributes same as the original plan.
+ val outputAttrPairs = coalesePlan.output.zip(union.output)
+ if (outputAttrPairs.forall(pair => pair._1.semanticEquals(pair._2)))
{
+ coalesePlan
+ } else {
+ val reprejectOutputs = outputAttrPairs.map {
+ case (newAttr, oldAttr) =>
+ if (newAttr.exprId == oldAttr.exprId) {
+ newAttr
+ } else {
+ Alias(newAttr, oldAttr.name)(oldAttr.exprId,
oldAttr.qualifier, None, Seq.empty)
+ }
+ }
+ Project(reprejectOutputs, coalesePlan)
+ }
+ }
+ case _ => plan.withNewChildren(plan.children.map(visitPlan))
+ }
+ }
+
+ def isRelation(plan: LogicalPlan): Boolean = {
+ plan.isInstanceOf[MultiInstanceRelation]
+ }
+
+ def areSameRelation(l: LogicalPlan, r: LogicalPlan): Boolean = {
+ (l, r) match {
+ case (lRelation: LogicalRelation, rRelation: LogicalRelation) =>
+ val lTable =
lRelation.catalogTable.map(_.identifier.unquotedString).getOrElse("")
+ val rTable =
rRelation.catalogTable.map(_.identifier.unquotedString).getOrElse("")
+ lRelation.output.length == rRelation.output.length &&
+ lRelation.output.zip(rRelation.output).forall {
+ case (lAttr, rAttr) =>
+ lAttr.dataType.equals(rAttr.dataType) &&
lAttr.name.equals(rAttr.name)
+ } &&
+ lTable.equals(rTable) && lTable.nonEmpty
+ case (lCTE: CTERelationRef, rCTE: CTERelationRef) =>
+ lCTE.cteId == rCTE.cteId
+ case (lHiveTable: HiveTableRelation, rHiveTable: HiveTableRelation) =>
+ lHiveTable.tableMeta.identifier.unquotedString
+ .equals(rHiveTable.tableMeta.identifier.unquotedString)
+ case (_, _) =>
+ logInfo(s"xxx unknow relation: ${l.getClass}, ${r.getClass}")
+ false
+ }
+ }
+
+ def isSupportedAggregate(info: AggregateAnalzyInfo): Boolean = {
+
+ !info.hasAggregateWithFilter &&
+ info.constructedAggregatePlan.isDefined &&
+ info.positionInGroupingKeys.forall(_ >= 0) &&
+ info.originalAggregate.aggregateExpressions.forall {
+ e =>
+ val innerExpr = removeAlias(e)
+ // `agg_fun1(x) + agg_fun2(y)` is supported, but `agg_fun1(x) + y` is
not supported.
+ if (hasAggregateExpression(innerExpr)) {
+ innerExpr.isInstanceOf[AggregateExpression] ||
+ innerExpr.children.forall(e => isAggregateExpression(e))
+ } else {
+ true
+ }
+ } &&
+ info.extractedSourcePlan.isDefined
+ }
+
+ /**
+ * Checks if two AggregateAnalzyInfo instances have the same structure.
+ *
+ * This method compares the aggregate expressions, grouping expressions, and
the source plans of
+ * the two AggregateAnalzyInfo instances to determine if they have the same
structure.
+ *
+ * @param l
+ * The first AggregateAnalzyInfo instance.
+ * @param r
+ * The second AggregateAnalzyInfo instance.
+ * @return
+ * True if the two instances have the same structure, false otherwise.
+ */
+ def areStructureMatchedAggregate(l: AggregateAnalzyInfo, r:
AggregateAnalzyInfo): Boolean = {
+ val lAggregate = l.constructedAggregatePlan.get.asInstanceOf[Aggregate]
+ val rAggregate = r.constructedAggregatePlan.get.asInstanceOf[Aggregate]
+ lAggregate.aggregateExpressions.length ==
rAggregate.aggregateExpressions.length &&
+
lAggregate.aggregateExpressions.zip(rAggregate.aggregateExpressions).forall {
+ case (lExpr, rExpr) =>
+ if (!lExpr.dataType.equals(rExpr.dataType)) {
+ false
+ } else {
+ (hasAggregateExpression(lExpr), hasAggregateExpression(rExpr)) match
{
+ case (true, true) => areStructureMatchedExpressions(lExpr, rExpr)
+ case (false, true) => false
+ case (true, false) => false
+ case (false, false) => true
+ }
+ }
+ } &&
+ lAggregate.groupingExpressions.length ==
rAggregate.groupingExpressions.length &&
+ l.positionInGroupingKeys.length == r.positionInGroupingKeys.length &&
+ l.positionInGroupingKeys.zip(r.positionInGroupingKeys).forall {
+ case (lPos, rPos) => lPos == rPos
+ } &&
+ areSameAggregateSource(l.extractedSourcePlan.get,
r.extractedSourcePlan.get)
+ }
+
+ /*
+ * Finds the index of the first group in `planGroups` that has the same
structure as the given
+ * `analyzedInfo`.
+ *
+ * This method iterates over the `planGroups` and checks if the first
`AnalyzedPlan` in each group
+ * has an `analyzedInfo` that matches the structure of the provided
`analyzedInfo`. If a match is
+ * found, the index of the group is returned. If no match is found, -1 is
returned.
+ *
+ * @param planGroups
+ * An ArrayBuffer of ArrayBuffers, where each inner ArrayBuffer contains
`AnalyzedPlan`
+ * instances.
+ * @param analyzedInfo
+ * The `AggregateAnalzyInfo` to match against the groups in `planGroups`.
+ * @return
+ * The index of the first group with a matching structure, or -1 if no
match is found.
+ */
+ def findStructureMatchedAggregate(
+ planGroups: ArrayBuffer[ArrayBuffer[AnalyzedPlan]],
+ analyzedInfo: AggregateAnalzyInfo): Int = {
+ planGroups.zipWithIndex.find(
+ planWithIndex =>
+ planWithIndex._1.head.analyzedInfo.isDefined &&
+ areStructureMatchedAggregate(
+ planWithIndex._1.head.analyzedInfo.get,
+ analyzedInfo)) match {
+ case Some((_, i)) => i
+ case None => -1
+ }
+
+ }
+
+ // Union only has two children. It's children may also be Union.
+ def collectAllUnionClauses(union: Union): ArrayBuffer[LogicalPlan] = {
+ val unionClauses = ArrayBuffer[LogicalPlan]()
+ union.children.foreach {
+ case u: Union =>
+ unionClauses ++= collectAllUnionClauses(u)
+ case other =>
+ unionClauses += other
+ }
+ unionClauses
+ }
+
+ def groupStructureMatchedAggregate(union: Union):
ArrayBuffer[ArrayBuffer[AnalyzedPlan]] = {
+
+ def tryPutToGroup(
+ groupResults: ArrayBuffer[ArrayBuffer[AnalyzedPlan]],
+ agg: Aggregate): Unit = {
+ val analyzedInfo = AggregateAnalzyInfo(agg)
+ if (isSupportedAggregate(analyzedInfo)) {
+ if (groupResults.isEmpty) {
+ groupResults += ArrayBuffer(
+ AnalyzedPlan(analyzedInfo.originalAggregate, Some(analyzedInfo)))
+ } else {
+ val idx = findStructureMatchedAggregate(groupResults, analyzedInfo)
+ if (idx != -1) {
+ groupResults(idx) += AnalyzedPlan(
+ analyzedInfo.constructedAggregatePlan.get,
+ Some(analyzedInfo))
+ } else {
+ groupResults += ArrayBuffer(
+ AnalyzedPlan(analyzedInfo.constructedAggregatePlan.get,
Some(analyzedInfo)))
+ }
+ }
+ } else {
+ val rewrittenPlan = visitPlan(agg)
+ groupResults += ArrayBuffer(AnalyzedPlan(rewrittenPlan, None))
+ }
+ }
+
+ val groupResults = ArrayBuffer[ArrayBuffer[AnalyzedPlan]]()
+ collectAllUnionClauses(union).foreach {
+ case project @ Project(projectList, agg: Aggregate) =>
+ if (projectList.forall(e => e.isInstanceOf[Alias])) {
+ tryPutToGroup(groupResults, agg)
+ } else {
+ val rewrittenPlan = visitPlan(project)
+ groupResults += ArrayBuffer(AnalyzedPlan(rewrittenPlan, None))
+ }
+ case agg: Aggregate =>
+ tryPutToGroup(groupResults, agg)
+ case other =>
+ val rewrittenPlan = visitPlan(other)
+ groupResults += ArrayBuffer(AnalyzedPlan(rewrittenPlan, None))
+ }
+ groupResults
+ }
+
+ def areStructureMatchedExpressions(l: Expression, r: Expression): Boolean = {
+ if (l.dataType.equals(r.dataType)) {
+ (l, r) match {
+ case (lAttr: Attribute, rAttr: Attribute) =>
+ // The the qualifier may be overwritten by a subquery alias, and
make this check fail.
+ lAttr.qualifiedName.equals(rAttr.qualifiedName)
+ case (lLiteral: Literal, rLiteral: Literal) =>
+ lLiteral.value == rLiteral.value
+ case (lagg: AggregateExpression, ragg: AggregateExpression) =>
+ lagg.isDistinct == ragg.isDistinct &&
+ areStructureMatchedExpressions(lagg.aggregateFunction,
ragg.aggregateFunction)
+ case _ =>
+ l.children.length == r.children.length &&
+ l.getClass == r.getClass &&
+ l.children.zip(r.children).forall {
+ case (lChild, rChild) => areStructureMatchedExpressions(lChild,
rChild)
+ }
+ }
+ } else {
+ false
+ }
+ }
+
+ def areSameAggregateSource(lPlan: LogicalPlan, rPlan: LogicalPlan): Boolean
= {
+ if (lPlan.children.length != rPlan.children.length || lPlan.getClass !=
rPlan.getClass) {
+ false
+ } else {
+ lPlan.children.zip(rPlan.children).forall {
+ case (lRelation, rRelation) if (isRelation(lRelation) &&
isRelation(rRelation)) =>
+ areSameRelation(lRelation, rRelation)
+ case (lSubQuery: SubqueryAlias, rSubQuery: SubqueryAlias) =>
+ areSameAggregateSource(lSubQuery.child, rSubQuery.child)
+ case (lproject: Project, rproject: Project) =>
+ lproject.projectList.length == rproject.projectList.length &&
+ lproject.projectList.zip(rproject.projectList).forall {
+ case (lExpr, rExpr) => areStructureMatchedExpressions(lExpr, rExpr)
+ } &&
+ areSameAggregateSource(lproject.child, rproject.child)
+ case (lFilter: Filter, rFilter: Filter) =>
+ areStructureMatchedExpressions(lFilter.condition, rFilter.condition)
&&
+ areSameAggregateSource(lFilter.child, rFilter.child)
+ case (lChild, rChild) => false
+ }
+ }
+ }
+
+ def buildAggregateCasesConditions(
+ groupedPlans: ArrayBuffer[AnalyzedPlan]): ArrayBuffer[Expression] = {
+ val firstPlanSourceOutputAttrs =
+ groupedPlans.head.analyzedInfo.get.extractedSourcePlan.get.output
+ groupedPlans.map {
+ plan =>
+ val attrsMap =
+ buildAttributesMap(
+ plan.analyzedInfo.get.extractedSourcePlan.get.output,
+ firstPlanSourceOutputAttrs)
+ val filter =
plan.analyzedInfo.get.constructedFilterPlan.get.asInstanceOf[Filter]
+ replaceAttributes(filter.condition, attrsMap)
+ }
+ }
+
+ def buildUnionConditionForAggregateSource(conditions:
ArrayBuffer[Expression]): Expression = {
+ conditions.reduce(Or);
+ }
+
+ def wrapAggregatesAttributesInStructs(
+ groupedPlans: ArrayBuffer[AnalyzedPlan]): Seq[NamedExpression] = {
+ val structAttributes = ArrayBuffer[NamedExpression]()
+ val casePrefix = "case_"
+ val structPrefix = "field_"
+ val firstSourceAttrs =
groupedPlans.head.analyzedInfo.get.extractedSourcePlan.get.output
+ groupedPlans.zipWithIndex.foreach {
+ case (aggregateCase, case_index) =>
+ val analyzedInfo = aggregateCase.analyzedInfo.get
+ val aggregate =
analyzedInfo.constructedAggregatePlan.get.asInstanceOf[Aggregate]
+ val structFields = ArrayBuffer[Expression]()
+ var fieldIndex: Int = 0
+ val attrReplaceMap = buildAttributesMap(
+ aggregateCase.analyzedInfo.get.extractedSourcePlan.get.output,
+ firstSourceAttrs)
+ aggregate.groupingExpressions.foreach {
+ e =>
+ structFields +=
Literal(UTF8String.fromString(s"$structPrefix$fieldIndex"), StringType)
+ structFields += replaceAttributes(e, attrReplaceMap)
+ fieldIndex += 1
+ }
+ for (i <- 0 until analyzedInfo.positionInGroupingKeys.length) {
+ val position = analyzedInfo.positionInGroupingKeys(i)
+ if (position >= fieldIndex) {
+ val expr = analyzedInfo.resultGroupingExpressions(i)
+ structFields +=
Literal(UTF8String.fromString(s"$structPrefix$fieldIndex"), StringType)
+ structFields += replaceAttributes(
+ analyzedInfo.resultGroupingExpressions(i),
+ attrReplaceMap)
+ fieldIndex += 1
+ }
+ }
+
+ aggregate.aggregateExpressions
+ .filter(e => hasAggregateExpression(e))
+ .foreach {
+ e =>
+ def collectExpressionsInAggregateExpression(aggExpr:
Expression): Unit = {
+ aggExpr match {
+ case aggExpr: AggregateExpression =>
+ val aggFunction =
+
removeAlias(aggExpr).asInstanceOf[AggregateExpression].aggregateFunction
+ aggFunction.children.foreach {
+ child =>
+ structFields += Literal(
+ UTF8String.fromString(s"$structPrefix$fieldIndex"),
+ StringType)
+ structFields += replaceAttributes(child,
attrReplaceMap)
+ fieldIndex += 1
+ }
+ case combineAgg if hasAggregateExpression(combineAgg) =>
+ combineAgg.children.foreach {
+ combindAggchild =>
collectExpressionsInAggregateExpression(combindAggchild)
+ }
+ case other =>
+ structFields += Literal(
+ UTF8String.fromString(s"$structPrefix$fieldIndex"),
+ StringType)
+ structFields += replaceAttributes(other, attrReplaceMap)
+ fieldIndex += 1
+ }
+ }
+ collectExpressionsInAggregateExpression(e)
+ }
+ structFields +=
Literal(UTF8String.fromString(s"$structPrefix$fieldIndex"), StringType)
+ structFields += Literal(case_index, IntegerType)
+ structAttributes += makeAlias(
+ CreateNamedStruct(structFields.toSeq),
+ s"$casePrefix$case_index")
+ }
+ structAttributes.toSeq
+ }
+
+ def buildProjectFoldIntoStruct(
+ child: LogicalPlan,
+ groupedPlans: ArrayBuffer[AnalyzedPlan],
+ conditions: ArrayBuffer[Expression]): LogicalPlan = {
+ val wrappedAttributes = wrapAggregatesAttributesInStructs(groupedPlans)
+ val ifAttributes = wrappedAttributes.zip(conditions).map {
+ case (attr, condition) =>
+ makeAlias(If(condition, attr, Literal(null, attr.dataType)), attr.name)
+ .asInstanceOf[NamedExpression]
+ }
+ Project(ifAttributes, child)
+ }
+
+ def buildProjectBranchArray(
+ child: LogicalPlan,
+ conditions: ArrayBuffer[Expression]): LogicalPlan = {
+ assert(
+ child.output.length == conditions.length,
+ s"Expected same length of output and conditions")
+ val array = makeAlias(CreateArray(child.output), "array")
+ Project(Seq(array), child)
+ }
+
+ def buildExplodeBranchArray(child: LogicalPlan): LogicalPlan = {
+ assert(child.output.length == 1, s"Expected single output from $child")
+ val array = child.output.head.asInstanceOf[Expression]
+ assert(array.dataType.isInstanceOf[ArrayType], s"Expected ArrayType from
$array")
+ val explodeExpr = Explode(array)
+ val exploadOutput =
+ AttributeReference("generate_output",
array.dataType.asInstanceOf[ArrayType].elementType)()
+ Generate(
+ explodeExpr,
+ unrequiredChildIndex = Seq(0),
+ outer = false,
+ qualifier = None,
+ generatorOutput = Seq(exploadOutput),
+ child)
+ }
+
+ def makeAlias(e: Expression, name: String): NamedExpression = {
+ Alias(e, name)(
+ NamedExpression.newExprId,
+ e match {
+ case ne: NamedExpression => ne.qualifier
+ case _ => Seq.empty
+ },
+ None,
+ Seq.empty)
+ }
+
+ def buildProjectUnfoldStruct(child: LogicalPlan): LogicalPlan = {
+ assert(child.output.length == 1, s"Expected single output from $child")
+ val structedData = child.output.head
+ assert(
+ structedData.dataType.isInstanceOf[StructType],
+ s"Expected StructType from $structedData")
+ val structType = structedData.dataType.asInstanceOf[StructType]
+ val attributes = ArrayBuffer[NamedExpression]()
+ var index = 0
+ structType.fields.foreach {
+ field =>
+ attributes += Alias(GetStructField(structedData, index), field.name)()
+ index += 1
+ }
+ Project(attributes.toSeq, child)
+ }
+
+ def buildAggregateWithGroupId(
+ child: LogicalPlan,
+ groupedPlans: ArrayBuffer[AnalyzedPlan]): LogicalPlan = {
+ val attributes = child.output
+ val firstAggregateAnalzyInfo = groupedPlans.head.analyzedInfo.get
+ val aggregateTemplate =
+
firstAggregateAnalzyInfo.constructedAggregatePlan.get.asInstanceOf[Aggregate]
+ val analyzedInfo = groupedPlans.head.analyzedInfo.get
+
+ val totalGroupingExpressionsCount =
+ math.max(
+ aggregateTemplate.groupingExpressions.length,
+ analyzedInfo.positionInGroupingKeys.max + 1)
+
+ val groupingExpressions = attributes
+ .slice(0, totalGroupingExpressionsCount)
+ .map(_.asInstanceOf[Expression]) :+ attributes.last
+
+ val normalExpressionPosition = analyzedInfo.positionInGroupingKeys
+ var normalExpressionCount = 0
+ var aggregateExpressionIndex = totalGroupingExpressionsCount
+ val aggregateExpressions = ArrayBuffer[NamedExpression]()
+ aggregateTemplate.aggregateExpressions.foreach {
+ e =>
+ removeAlias(e) match {
+ case aggExpr if hasAggregateExpression(aggExpr) =>
+ val (newAggExpr, count) =
+ constructAggregateExpression(aggExpr, attributes,
aggregateExpressionIndex)
+ aggregateExpressions += makeAlias(newAggExpr,
e.name).asInstanceOf[NamedExpression]
+ aggregateExpressionIndex += count
+ case other =>
+ val position = normalExpressionPosition(normalExpressionCount)
+ val attr = attributes(position)
+ normalExpressionCount += 1
+ aggregateExpressions += makeAlias(attr, e.name)
+ .asInstanceOf[NamedExpression]
+ }
+ }
+ Aggregate(groupingExpressions.toSeq, aggregateExpressions.toSeq, child)
+ }
+
+ def constructAggregateExpression(
+ aggExpr: Expression,
+ attributes: Seq[Attribute],
+ index: Int): (Expression, Int) = {
+ aggExpr match {
+ case singleAggExpr: AggregateExpression =>
+ val aggFunc = singleAggExpr.aggregateFunction
+ val newAggFuncArgs = aggFunc.children.zipWithIndex.map {
+ case (arg, i) =>
+ attributes(index + i)
+ }
+ val newAggFunc =
+
aggFunc.withNewChildren(newAggFuncArgs).asInstanceOf[AggregateFunction]
+ val res = AggregateExpression(
+ newAggFunc,
+ singleAggExpr.mode,
+ singleAggExpr.isDistinct,
+ singleAggExpr.filter,
+ singleAggExpr.resultId)
+ (res, 1)
+ case combineAggExpr if hasAggregateExpression(combineAggExpr) =>
+ val childrenExpressions = ArrayBuffer[Expression]()
+ var totalCount = 0
+ combineAggExpr.children.foreach {
+ child =>
+ val (expr, count) = constructAggregateExpression(child,
attributes, totalCount + index)
+ childrenExpressions += expr
+ totalCount += count
+ }
+ (combineAggExpr.withNewChildren(childrenExpressions.toSeq), totalCount)
+ case _ => (attributes(index), 1)
+ }
+ }
+}
diff --git
a/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenCoalesceAggregationUnionSuite.scala
b/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenCoalesceAggregationUnionSuite.scala
new file mode 100644
index 0000000000..23c6022727
--- /dev/null
+++
b/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenCoalesceAggregationUnionSuite.scala
@@ -0,0 +1,395 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.execution
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.{DataFrame, Row}
+import
org.apache.spark.sql.execution.datasources.v2.clickhouse.ClickHouseConfig
+import org.apache.spark.sql.types._
+
+import java.nio.file.Files
+
+class GlutenCoalesceAggregationUnionSuite extends
GlutenClickHouseWholeStageTransformerSuite {
+ override protected def sparkConf: SparkConf = {
+ super.sparkConf
+ .set("spark.sql.files.maxPartitionBytes", "1g")
+ .set("spark.serializer", "org.apache.spark.serializer.JavaSerializer")
+ .set("spark.sql.shuffle.partitions", "5")
+ .set("spark.sql.adaptive.enabled", "false")
+ .set("spark.sql.files.minPartitionNum", "1")
+ .set(
+ "spark.sql.catalog.spark_catalog",
+
"org.apache.spark.sql.execution.datasources.v2.clickhouse.ClickHouseSparkCatalog")
+ .set("spark.databricks.delta.maxSnapshotLineageLength", "20")
+ .set("spark.databricks.delta.snapshotPartitions", "1")
+ .set("spark.databricks.delta.properties.defaults.checkpointInterval",
"5")
+ .set("spark.databricks.delta.stalenessLimit", "3600000")
+ .set(ClickHouseConfig.CLICKHOUSE_WORKER_ID, "1")
+ .set("spark.gluten.sql.columnar.iterator", "true")
+ .set("spark.gluten.sql.columnar.hashagg.enablefinal", "true")
+ .set("spark.gluten.sql.enable.native.validation", "false")
+ .set("spark.sql.warehouse.dir", warehouse)
+ .set("spark.shuffle.manager", "sort")
+ .set("spark.io.compression.codec", "snappy")
+ .set("spark.sql.shuffle.partitions", "5")
+ .set("spark.sql.autoBroadcastJoinThreshold", "10MB")
+ }
+
+ def createTestTable(tableName: String, data: DataFrame): Unit = {
+ val tempFile = Files.createTempFile("", ".parquet").toFile
+ tempFile.deleteOnExit()
+ val tempFilePath = tempFile.getAbsolutePath
+
data.coalesce(1).write.format("parquet").mode("overwrite").parquet(tempFilePath)
+ spark.catalog.createTable(tableName, tempFilePath, "parquet")
+ }
+
+ override def beforeAll(): Unit = {
+ super.beforeAll()
+
+ val schema = StructType(
+ Array(
+ StructField("a", StringType, nullable = true),
+ StructField("b", IntegerType, nullable = true),
+ StructField("x", StringType, nullable = true),
+ StructField("y", IntegerType, nullable = true)
+ ))
+ val data = sparkContext.parallelize(
+ Seq(
+ Row("a", 1, null, 1),
+ Row("a", 2, "a", 2),
+ Row("a", 3, "b", 3),
+ Row("a", 4, "c", 4),
+ Row("b", 1, "d", 5),
+ Row("b", 2, "e", 6),
+ Row("b", 3, "f", 7),
+ Row("b", 4, "g", null)
+ ))
+
+ val dataFrame = spark.createDataFrame(data, schema)
+ createTestTable("coalesce_union_t1", dataFrame)
+ createTestTable("coalesce_union_t2", dataFrame)
+ }
+
+ def checkNoUnion(df: DataFrame): Unit = {
+ val unions = collectWithSubqueries(df.queryExecution.executedPlan) {
+ case e: ColumnarUnionExec => e
+ }
+ assert(unions.isEmpty)
+ }
+
+ def checkHasUnion(df: DataFrame): Unit = {
+ val unions = collectWithSubqueries(df.queryExecution.executedPlan) {
+ case e: ColumnarUnionExec => e
+ }
+ assert(unions.size == 1)
+ }
+
+ test("coalesce aggregation union. case 1") {
+ val sql =
+ """
+ |select a, x + 1 as x, y from (
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 0
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 1
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 2
+ | group by a
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 2") {
+ val sql =
+ """
+ |select a, x + 1 as x, y from (
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 0
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 1
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
> 1
+ | group by a
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 3") {
+ val sql =
+ """
+ |select a, x + 1 as x, y from (
+ | select a, 1 as t, count(x) as x, sum(y) as y from coalesce_union_t1
where b % 3 = 0
+ | group by a
+ | union all
+ | select a, 2 as t, count(x) as x, sum(y) as y from coalesce_union_t1
where b % 3 = 1
+ | group by a
+ |) order by a, t, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 4") {
+ val sql =
+ """
+ |select * from (
+ | select a, 1 as t, count(x) + sum(y) as n from coalesce_union_t1
where b % 3 = 0
+ | group by a
+ | union all
+ | select a, 2 as t, count(x) + sum(y) as n from coalesce_union_t1
where b % 3 = 1
+ | group by a
+ |) order by a, t, n
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 5") {
+ val sql =
+ """
+ |select * from (
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 0
+ | group by a, b
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 1
+ | group by a, b
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 6") {
+ val sql =
+ """
+ |select * from (
+ | select y + 1 as y , count(x) as x from coalesce_union_t1 where b % 3
= 0
+ | group by y + 1
+ | union all
+ | select y + 1 as y, count(x) as x from coalesce_union_t1 where b % 3
= 1
+ | group by y + 1
+ |) order by y, x
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 7") {
+ val sql =
+ """
+ |select * from (
+ | select a, count(x) as x, sum(y + 1) as y from coalesce_union_t1
where b % 3 = 0
+ | group by a, b
+ | union all
+ | select a, count(x) as x, sum(y + 1) as y from coalesce_union_t1
where b % 3 = 1
+ | group by a, b
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 8") {
+ val sql =
+ """
+ |select * from (
+ | select a as a, sum(y) as y from coalesce_union_t1 where b % 3 = 0
+ | group by a
+ | union all
+ | select x as a , sum(y) as y from coalesce_union_t1 where b % 3 = 1
+ | group by x
+ |) order by a, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 9") {
+ val sql =
+ """
+ |select a, x + 1 as x, y from (
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 0
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 1
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 2
+ | group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 where b
% 3 = 3
+ | group by a
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 10") {
+ val sql =
+ """
+ |select * from (
+ | select a as a, sum(y) as y from (
+ | select concat(a, "x") as a, y from coalesce_union_t1 where b % 3 = 0
+ | ) group by a
+ | union all
+ | select x as a , sum(y) as y from coalesce_union_t1 where b % 3 = 1
+ | group by x
+ |) order by a, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 11") {
+ val sql =
+ """
+ |select t1.a, t1.y, t2.x from (
+ | select a as a, sum(y) as y from (
+ | select concat(a, "x") as a, y from coalesce_union_t1 where b % 3 = 0
+ | ) group by a
+ | union all
+ | select x as a , sum(y) as y from coalesce_union_t1 where b % 3 = 1
+ | group by x
+ |) as t1
+ |left join (
+ | select a, x from coalesce_union_t2
+ |) as t2
+ |on t1.a = t2.a
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("coalesce aggregation union. case 12") {
+ val sql =
+ """
+ |select a, x, y from (
+ | select a, count(distinct x) as x, sum(y) as y from coalesce_union_t1
group by a
+ | union all
+ | select a, count(distinct x) as x, sum(y) as y from coalesce_union_t1
group by a
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkNoUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 1") {
+ val sql =
+ """
+ |select * from (
+ | select a, count(x) + 1 as x, sum(y) as y from coalesce_union_t1
where b % 3 = 0
+ | group by a
+ | union all
+ | select a, count(x) + 1 as x, sum(y) as y from coalesce_union_t1
where b % 3 = 1
+ | group by a
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 2") {
+ val sql =
+ """
+ |select * from (
+ | select y + 1 as y, count(x) as x from coalesce_union_t1 where b % 3
= 0
+ | group by y
+ | union all
+ | select y + 1 as y, count(x) as x from coalesce_union_t1 where b % 3
= 1
+ | group by y
+ |) order by y, x
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 3") {
+ val sql =
+ """
+ |select * from (
+ | select a, count(x) as x from coalesce_union_t1 where b % 3 = 0
+ | group by a
+ | union all
+ | select a, count(y) as x from coalesce_union_t1 where b % 3 = 1
+ | group by a
+ |) order by a, x
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 4") {
+ val sql =
+ """
+ |select * from (
+ | select a, 1 as b, count(x) as x from coalesce_union_t1 where b % 3 = 0
+ | group by a, 1
+ | union all
+ | select a, b, count(x) as x from coalesce_union_t1 where b % 3 = 1
+ | group by a, b
+ |) order by a, b, x
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 5") {
+ val sql =
+ """
+ |select * from (
+ | select a, b, count(x) as x from coalesce_union_t1 where b % 3 = 0
+ | group by a, b
+ | union all
+ | select a, b, count(x) as x from coalesce_union_t2 where b % 3 = 1
+ | group by a, b
+ |) order by a, b, x
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 6") {
+ val sql =
+ """
+ |select * from (
+ | select a as k1, x as k2, count(y) as c from coalesce_union_t1 where
b % 3 = 0
+ | group by a, x
+ | union all
+ | select x as k1, a as k2, count(y) as c from coalesce_union_t1 where
b % 3 = 1
+ | group by a, x
+ |) order by k1, k2, c
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 7") {
+ val sql =
+ """
+ |select * from (
+ | select a, count(y) as y from coalesce_union_t1 where b % 3 = 0
+ | group by a
+ | union all
+ | select a, count(y) as y from coalesce_union_t2
+ | group by a
+ |) order by a, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+ test("no coalesce aggregation union. case 8") {
+ val sql =
+ """
+ |select a, x, y from (
+ | select a, count(distinct x) as x, sum(y) as y from coalesce_union_t1
group by a
+ | union all
+ | select a, count(x) as x, sum(y) as y from coalesce_union_t1 group by
a
+ |) order by a, x, y
+ |""".stripMargin
+ compareResultsAgainstVanillaSpark(sql, true, checkHasUnion, true)
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]