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 c6cce934b987..92b5bae592ea 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 @@ -124,6 +124,7 @@ object CHRuleApi { injector.injectPostTransform(c => AddPreProjectionForHashJoin.apply(c.session)) injector.injectPostTransform(c => ReplaceSubStringComparison.apply(c.session)) injector.injectPostTransform(c => EliminateDeduplicateAggregateWithAnyJoin(c.session)) + injector.injectPostTransform(c => FlattenNestedExpressions.apply(c.session)) // Gluten columnar: Fallback policies. injector.injectFallbackPolicy(c => p => ExpandFallbackPolicy(c.caller.isAqe(), p)) diff --git a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHSparkPlanExecApi.scala b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHSparkPlanExecApi.scala index 3a4267c4b1ca..dcf19204d2af 100644 --- a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHSparkPlanExecApi.scala +++ b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHSparkPlanExecApi.scala @@ -581,7 +581,9 @@ class CHSparkPlanExecApi extends SparkPlanExecApi with Logging { List( Sig[CollectList](ExpressionNames.COLLECT_LIST), Sig[CollectSet](ExpressionNames.COLLECT_SET), - Sig[MonotonicallyIncreasingID](MONOTONICALLY_INCREASING_ID) + Sig[MonotonicallyIncreasingID](MONOTONICALLY_INCREASING_ID), + CHFlattenedExpression.sigAnd, + CHFlattenedExpression.sigOr ) ++ ExpressionExtensionTrait.expressionExtensionSigList ++ SparkShimLoader.getSparkShims.bloomFilterExpressionMappings() @@ -947,4 +949,19 @@ class CHSparkPlanExecApi extends SparkPlanExecApi with Logging { outputAttributes: Seq[Attribute], child: Seq[SparkPlan]): ColumnarRangeBaseExec = CHRangeExecTransformer(start, end, step, numSlices, numElements, outputAttributes, child) + + override def expressionFlattenSupported(expr: Expression): Boolean = expr match { + case ca: FlattenedAnd => CHFlattenedExpression.supported(ca.name) + case co: FlattenedOr => CHFlattenedExpression.supported(co.name) + case _ => false + } + + override def genFlattenedExpressionTransformer( + substraitName: String, + children: Seq[ExpressionTransformer], + expr: Expression): ExpressionTransformer = expr match { + case ce: FlattenedAnd => GenericExpressionTransformer(ce.name, children, ce) + case co: FlattenedOr => GenericExpressionTransformer(co.name, children, co) + case _ => super.genFlattenedExpressionTransformer(substraitName, children, expr) + } } diff --git a/backends-clickhouse/src/main/scala/org/apache/gluten/execution/CHFilterExecTransformer.scala b/backends-clickhouse/src/main/scala/org/apache/gluten/execution/CHFilterExecTransformer.scala index 79ddf2942c3e..686e51fcf6a9 100644 --- a/backends-clickhouse/src/main/scala/org/apache/gluten/execution/CHFilterExecTransformer.scala +++ b/backends-clickhouse/src/main/scala/org/apache/gluten/execution/CHFilterExecTransformer.scala @@ -16,7 +16,9 @@ */ package org.apache.gluten.execution -import org.apache.spark.sql.catalyst.expressions.{And, Expression} +import org.apache.gluten.expression.CHFlattenedExpression + +import org.apache.spark.sql.catalyst.expressions.{And, Expression, ExprId, IsNotNull} import org.apache.spark.sql.execution.SparkPlan case class CHFilterExecTransformer(condition: Expression, child: SparkPlan) @@ -48,4 +50,13 @@ case class FilterExecTransformer(condition: Expression, child: SparkPlan) override protected def getRemainingCondition: Expression = condition override protected def withNewChildInternal(newChild: SparkPlan): FilterExecTransformer = copy(child = newChild) + override protected val notNullAttributes: Seq[ExprId] = condition match { + case s: CHFlattenedExpression => + val (notNullPreds, _) = s.children.partition { + case IsNotNull(a) => isNullIntolerant(a) && a.references.subsetOf(child.outputSet) + case _ => false + } + notNullPreds.flatMap(_.references).distinct.map(_.exprId) + case _ => notNullPreds.flatMap(_.references).distinct.map(_.exprId) + } } diff --git a/backends-clickhouse/src/main/scala/org/apache/gluten/expression/CHFlattenedExpression.scala b/backends-clickhouse/src/main/scala/org/apache/gluten/expression/CHFlattenedExpression.scala new file mode 100644 index 000000000000..e0a4623ce5a4 --- /dev/null +++ b/backends-clickhouse/src/main/scala/org/apache/gluten/expression/CHFlattenedExpression.scala @@ -0,0 +1,84 @@ +/* + * 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.expression + +import org.apache.gluten.config.GlutenConfig +import org.apache.gluten.exception.GlutenException +import org.apache.gluten.expression.CHFlattenedExpression.genFlattenedExpression + +import org.apache.spark.sql.catalyst.InternalRow +import org.apache.spark.sql.catalyst.expressions.Expression +import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode} +import org.apache.spark.sql.types.DataType + +abstract class CHFlattenedExpression(children: Seq[Expression], name: String) extends Expression { + + def this() = { + this(Seq.empty[Expression], "") + } + + override def toString: String = s"$name(${children.mkString(", ")})" + + override def eval(input: InternalRow): Any = null + + override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = null + + override protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]): Expression = + genFlattenedExpression(dataType, newChildren, name, nullable) match { + case Some(f) => f + case None => + throw new GlutenException( + String.format( + "Logical error, the function %s should not be a clickhouse Flattened function.", + name)) + } +} + +case class FlattenedAnd( + dataType: DataType, + children: Seq[Expression], + name: String, + nullable: Boolean) + extends CHFlattenedExpression(children, name) {} + +case class FlattenedOr( + dataType: DataType, + children: Seq[Expression], + name: String, + nullable: Boolean) + extends CHFlattenedExpression(children, name) {} + +object CHFlattenedExpression { + + def sigAnd: Sig = Sig[FlattenedAnd]("FlattenedAnd") + def sigOr: Sig = Sig[FlattenedOr]("FlattenedOr") + + def supported(name: String): Boolean = { + GlutenConfig.get.getSupportedFlattenedExpressions.split(",").exists(p => p.equals(name)) + } + + def genFlattenedExpression( + dataType: DataType, + children: Seq[Expression], + name: String, + nullable: Boolean): Option[CHFlattenedExpression] = name match { + case "and" => Option.apply(FlattenedAnd(dataType, children, name, nullable)) + case "or" => Option.apply(FlattenedOr(dataType, children, name, nullable)) + case _ => Option.empty + } + +} diff --git a/backends-clickhouse/src/main/scala/org/apache/gluten/extension/FlattenNestedExpressions.scala b/backends-clickhouse/src/main/scala/org/apache/gluten/extension/FlattenNestedExpressions.scala new file mode 100644 index 000000000000..8129f75633bd --- /dev/null +++ b/backends-clickhouse/src/main/scala/org/apache/gluten/extension/FlattenNestedExpressions.scala @@ -0,0 +1,175 @@ +/* + * 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.execution.{FilterExecTransformer, ProjectExecTransformer} +import org.apache.gluten.expression.{CHFlattenedExpression, ExpressionMappings} + +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.catalyst.expressions._ +import org.apache.spark.sql.catalyst.rules.Rule +import org.apache.spark.sql.execution.SparkPlan +import org.apache.spark.sql.types.DataType + +/** + * Flatten nested expressions for optimization, to reduce expression calls. Now support `and`, `or`. + * e.g. select ... and(and(a=1, b=2), c=3) => select ... and(a=1, b=2, c=3). + */ +case class FlattenNestedExpressions(spark: SparkSession) extends Rule[SparkPlan] { + + override def apply(plan: SparkPlan): SparkPlan = { + if (canBeOptimized(plan)) { + visitPlan(plan) + } else { + plan + } + } + + private def canBeOptimized(plan: SparkPlan): Boolean = plan match { + case p: ProjectExecTransformer => + var res = p.projectList.exists(c => c.isInstanceOf[And] || c.isInstanceOf[Or]) + if (res) { + return false + } + res = p.projectList.exists(c => canBeOptimized(c)) + if (!res) { + res = p.children.exists(c => canBeOptimized(c)) + } + res + case f: FilterExecTransformer => + var res = canBeOptimized(f.condition) + if (!res) { + res = canBeOptimized(f.child) + } + res + case _ => plan.children.exists(c => canBeOptimized(c)) + } + + private def canBeOptimized(expr: Expression): Boolean = { + var exprCall = expr + expr match { + case a: Alias => exprCall = a.child + case _ => + } + val exprName = getExpressionName(exprCall) + exprName match { + case None => + exprCall match { + case _: LeafExpression => false + case _ => exprCall.children.exists(c => canBeOptimized(c)) + } + case Some(f) => + CHFlattenedExpression.supported(f) + } + } + + private def getExpressionName(expr: Expression): Option[String] = expr match { + case _: And => ExpressionMappings.expressionsMap.get(classOf[And]) + case _: Or => ExpressionMappings.expressionsMap.get(classOf[Or]) + case _ => Option.empty[String] + } + + private def visitPlan(plan: SparkPlan): SparkPlan = plan match { + case p: ProjectExecTransformer => + var newProjectList = Seq.empty[NamedExpression] + p.projectList.foreach { + case a: Alias => + val newAlias = Alias(optimize(a.child), a.name)(a.exprId) + newProjectList :+= newAlias + case p => + newProjectList :+= p + } + val newChild = visitPlan(p.child) + ProjectExecTransformer(newProjectList, newChild) + case f: FilterExecTransformer => + val newCondition = optimize(f.condition) + val newChild = visitPlan(f.child) + FilterExecTransformer(newCondition, newChild) + case _ => + val newChildren = plan.children.map(p => visitPlan(p)) + plan.withNewChildren(newChildren) + } + + private def optimize(expr: Expression): Expression = { + var resultExpr = expr + var name = getExpressionName(expr) + var children = Seq.empty[Expression] + var dataType = null.asInstanceOf[DataType] + var nestedFunctions = 0 + + def f(e: Expression, parent: Option[Expression] = Option.empty[Expression]): Unit = { + parent match { + case None => + name = getExpressionName(e) + dataType = e.dataType + case _ => + } + e match { + case a: And if canBeOptimized(a) => + parent match { + case Some(_: And) | None => + f(a.left, Option.apply(a)) + f(a.right, Option.apply(a)) + nestedFunctions += 1 + case _ => + children +:= optimize(a) + } + case o: Or if canBeOptimized(o) => + parent match { + case Some(_: Or) | None => + f(o.left, parent = Option.apply(o)) + f(o.right, parent = Option.apply(o)) + nestedFunctions += 1 + case _ => + children +:= optimize(o) + } + case _ => + if (parent.nonEmpty) { + children +:= optimize(e) + } else { + children = Seq.empty[Expression] + nestedFunctions = 0 + val exprNewChildren = e.children.map(p => optimize(p)) + resultExpr = e.withNewChildren(exprNewChildren) + } + } + } + f(expr) + if ((nestedFunctions > 1 && name.isDefined) || flattenedExpressionExists(children)) { + CHFlattenedExpression.genFlattenedExpression( + dataType, + children, + name.getOrElse(""), + expr.nullable) match { + case Some(f) => f + case None => resultExpr + } + } else { + resultExpr + } + } + + private def flattenedExpressionExists(children: Seq[Expression]): Boolean = { + var res = false + children.foreach { + case _: CHFlattenedExpression if !res => res = true + case c if !res => res = flattenedExpressionExists(c.children) + case _ => + } + res + } +} diff --git a/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenFunctionValidateSuite.scala b/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenFunctionValidateSuite.scala index aa43ba426551..142d2d138d58 100644 --- a/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenFunctionValidateSuite.scala +++ b/backends-clickhouse/src/test/scala/org/apache/gluten/execution/GlutenFunctionValidateSuite.scala @@ -17,6 +17,7 @@ package org.apache.gluten.execution import org.apache.gluten.backendsapi.clickhouse.CHConfig +import org.apache.gluten.expression.{FlattenedAnd, FlattenedOr} import org.apache.spark.SparkConf import org.apache.spark.sql.{DataFrame, GlutenTestUtils, Row} @@ -381,6 +382,45 @@ class GlutenFunctionValidateSuite extends GlutenClickHouseWholeStageTransformerS } } + test("GLUTEN-8557: Optimize nested and/or") { + def checkFlattenedFunctions(plan: SparkPlan, functionName: String, argNum: Int): Boolean = { + + def checkExpression(expr: Expression, functionName: String, argNum: Int): Boolean = + expr match { + case s: FlattenedAnd if s.name.equals(functionName) && s.children.size == argNum => + true + case o: FlattenedOr if o.name.equals(functionName) && o.children.size == argNum => + true + case _ => expr.children.exists(c => checkExpression(c, functionName, argNum)) + } + plan match { + case f: FilterExecTransformer => return checkExpression(f.condition, functionName, argNum) + case _ => return plan.children.exists(c => checkFlattenedFunctions(c, functionName, argNum)) + } + false + } + runQueryAndCompare( + "SELECT count(1) from json_test where int_field1 = 5 and double_field1 > 1.0" + + " and string_field1 is not null") { + x => assert(checkFlattenedFunctions(x.queryExecution.executedPlan, "and", 5)) + } + runQueryAndCompare( + "SELECT count(1) from json_test where int_field1 = 5 or double_field1 > 1.0" + + " or string_field1 is not null") { + x => assert(checkFlattenedFunctions(x.queryExecution.executedPlan, "or", 3)) + } + runQueryAndCompare( + "SELECT count(1) from json_test where int_field1 = 5 and double_field1 > 1.0" + + " and double_field1 < 10 or int_field1 = 12 or string_field1 is not null") { + x => + assert( + checkFlattenedFunctions( + x.queryExecution.executedPlan, + "and", + 3) && checkFlattenedFunctions(x.queryExecution.executedPlan, "or", 3)) + } + } + test("Test covar_samp") { runQueryAndCompare("SELECT covar_samp(double_field1, int_field1) from json_test") { _ => } } diff --git a/cpp-ch/local-engine/Functions/SparkFunctionTupleElement.cpp b/cpp-ch/local-engine/Functions/SparkFunctionTupleElement.cpp index d52abde2ccfd..884adbaf2b87 100644 --- a/cpp-ch/local-engine/Functions/SparkFunctionTupleElement.cpp +++ b/cpp-ch/local-engine/Functions/SparkFunctionTupleElement.cpp @@ -68,14 +68,12 @@ class SparkFunctionTupleElement : public IFunction DataTypePtr getReturnTypeImpl(const ColumnsWithTypeAndName & arguments) const override { const size_t number_of_arguments = arguments.size(); - if (number_of_arguments < 2 || number_of_arguments > 3) throw Exception( ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH, "Number of arguments for function {} doesn't match: passed {}, should be 2 or 3", getName(), number_of_arguments); - std::vector arrays_is_nullable; DataTypePtr input_type = arguments[0].type; while (const DataTypeArray * array = checkAndGetDataType(removeNullable(input_type).get())) @@ -108,9 +106,6 @@ class SparkFunctionTupleElement : public IFunction if (*it) return_type = makeNullable(return_type); } - - // std::cout << "return_type:" << return_type->getName() << std::endl; - return return_type; } else @@ -163,7 +158,6 @@ class SparkFunctionTupleElement : public IFunction return arguments[2].column; ColumnPtr res = input_col_as_tuple->getColumns()[index.value()]; - /// Wrap into Nullable if needed if (input_col_as_nullable_tuple) { diff --git a/cpp-ch/local-engine/Parser/FunctionParser.cpp b/cpp-ch/local-engine/Parser/FunctionParser.cpp index 4bd35cb8c91a..3d9da46ad1e2 100644 --- a/cpp-ch/local-engine/Parser/FunctionParser.cpp +++ b/cpp-ch/local-engine/Parser/FunctionParser.cpp @@ -100,7 +100,6 @@ std::pair FunctionParser::parseLiteral(const substrait::Expr ActionsDAG::NodeRawConstPtrs FunctionParser::parseFunctionArguments(const substrait::Expression_ScalarFunction & substrait_func, ActionsDAG & actions_dag) const { - ActionsDAG::NodeRawConstPtrs parsed_args; return expression_parser->parseFunctionArguments(actions_dag, substrait_func); } diff --git a/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala b/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala index ccd697eb7249..ea24616ef31e 100644 --- a/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala +++ b/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala @@ -325,6 +325,7 @@ trait SparkPlanExecApi { * * childOutputAttributes may be different from outputAttributes, for example, the * childOutputAttributes include additional shuffle key columns + * * @return */ // scalastyle:off argcount @@ -711,4 +712,12 @@ trait SparkPlanExecApi { numElements: BigInt, outputAttributes: Seq[Attribute], child: Seq[SparkPlan]): ColumnarRangeBaseExec + + def expressionFlattenSupported(expr: Expression): Boolean = false + + def genFlattenedExpressionTransformer( + substraitName: String, + children: Seq[ExpressionTransformer], + expr: Expression): ExpressionTransformer = + GenericExpressionTransformer(substraitName, children, expr) } diff --git a/gluten-substrait/src/main/scala/org/apache/gluten/execution/BasicPhysicalOperatorTransformer.scala b/gluten-substrait/src/main/scala/org/apache/gluten/execution/BasicPhysicalOperatorTransformer.scala index d7f729f4d20f..d1f3462564f1 100644 --- a/gluten-substrait/src/main/scala/org/apache/gluten/execution/BasicPhysicalOperatorTransformer.scala +++ b/gluten-substrait/src/main/scala/org/apache/gluten/execution/BasicPhysicalOperatorTransformer.scala @@ -47,13 +47,14 @@ abstract class FilterExecTransformerBase(val cond: Expression, val input: SparkP BackendsApiManager.getMetricsApiInstance.genFilterTransformerMetrics(sparkContext) // Split out all the IsNotNulls from condition. - private val (notNullPreds, _) = splitConjunctivePredicates(cond).partition { + protected val (notNullPreds, _) = splitConjunctivePredicates(cond).partition { case IsNotNull(a) => isNullIntolerant(a) && a.references.subsetOf(child.outputSet) case _ => false } // The columns that will filtered out by `IsNotNull` could be considered as not nullable. - private val notNullAttributes = notNullPreds.flatMap(_.references).distinct.map(_.exprId) + protected val notNullAttributes: Seq[ExprId] = + notNullPreds.flatMap(_.references).distinct.map(_.exprId) override def isNullIntolerant(expr: Expression): Boolean = expr match { case e: NullIntolerant => e.children.forall(isNullIntolerant) diff --git a/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala b/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala index 48f33c3d5043..04a4e47e0b4b 100644 --- a/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala +++ b/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala @@ -109,6 +109,19 @@ object ExpressionConverter extends SQLConfHelper with Logging { } } + private def replaceFlattenedExpressionWithExpressionTransformer( + substraitName: String, + expr: Expression, + attributeSeq: Seq[Attribute], + expressionsMap: Map[Class[_], String]): ExpressionTransformer = { + val children = + expr.children.map(replaceWithExpressionTransformer0(_, attributeSeq, expressionsMap)) + BackendsApiManager.getSparkPlanExecApiInstance.genFlattenedExpressionTransformer( + substraitName, + children, + expr) + } + private def genRescaleDecimalTransformer( substraitName: String, b: BinaryArithmetic, @@ -742,6 +755,12 @@ object ExpressionConverter extends SQLConfHelper with Logging { substraitExprName, expr.children.map(replaceWithExpressionTransformer0(_, attributeSeq, expressionsMap)), j) + case ce if BackendsApiManager.getSparkPlanExecApiInstance.expressionFlattenSupported(ce) => + replaceFlattenedExpressionWithExpressionTransformer( + substraitExprName, + ce, + attributeSeq, + expressionsMap) case expr => GenericExpressionTransformer( substraitExprName, diff --git a/shims/common/src/main/scala/org/apache/gluten/config/GlutenConfig.scala b/shims/common/src/main/scala/org/apache/gluten/config/GlutenConfig.scala index e035b880c3ac..3af00160d5f4 100644 --- a/shims/common/src/main/scala/org/apache/gluten/config/GlutenConfig.scala +++ b/shims/common/src/main/scala/org/apache/gluten/config/GlutenConfig.scala @@ -355,12 +355,10 @@ class GlutenConfig(conf: SQLConf) extends Logging { def autoAdjustStageFallenNodeThreshold: Double = getConf(AUTO_ADJUST_STAGE_RESOURCES_FALLEN_NODE_RATIO_THRESHOLD) - def parquetEncryptionValidationFileLimit: Int = getConf(ENCRYPTED_PARQUET_FALLBACK_FILE_LIMIT) - def enableColumnarRange: Boolean = getConf(COLUMNAR_RANGE_ENABLED) - def enableColumnarCollectLimit: Boolean = getConf(COLUMNAR_COLLECT_LIMIT_ENABLED) + def getSupportedFlattenedExpressions: String = getConf(GLUTEN_SUPPORTED_FLATTENED_FUNCTIONS) } object GlutenConfig { @@ -697,6 +695,13 @@ object GlutenConfig { .stringConf .createWithDefault("") + val GLUTEN_SUPPORTED_FLATTENED_FUNCTIONS = + buildConf("spark.gluten.sql.supported.flattenNestedFunctions") + .internal() + .doc("Flatten nested functions as one for optimization.") + .stringConf + .createWithDefault("and,or"); + val GLUTEN_SOFT_AFFINITY_ENABLED = buildConf("spark.gluten.soft-affinity.enabled") .doc("Whether to enable Soft Affinity scheduling.")