[GLUTEN-11550][VL] Fix GlutenRemoveRedundantProjectsSuite in Spark 4.x#12506
[GLUTEN-11550][VL] Fix GlutenRemoveRedundantProjectsSuite in Spark 4.x#12506zml1206 wants to merge 3 commits into
Conversation
| enableSuite[GlutenPersistedViewTestSuite] | ||
| // TODO: 4.x enableSuite[GlutenPlannerSuite] // 1 failure | ||
| // TODO: 4.x enableSuite[GlutenProjectedOrderingAndPartitioningSuite] // 6 failures | ||
| // GlutenPlannerSuite is not enabled: it validates Spark planner implementation details. |
There was a problem hiding this comment.
The failures fall into two categories:
7 failures occur because the original Spark tests inspect native Spark physical operators, which are replaced by Gluten transformer operators.
8 failures are caused by spark.sql.shuffle.partitions=1 in GlutenSQLTestsBaseTrait, which changes the expected partitioning, exchange, and sort plans.
| // GlutenProjectedOrderingAndPartitioningSuite is not enabled: it validates Spark planner | ||
| // output ordering and partitioning metadata. |
There was a problem hiding this comment.
6 failures are caused by spark.sql.shuffle.partitions=1 in GlutenSQLTestsBaseTrait, which changes the expected partitioning, exchange, and sort plans.
There was a problem hiding this comment.
Pull request overview
Updates the Spark 4.0/4.1 Velox test configuration and GlutenRemoveRedundantProjectsSuite to avoid relying on Spark ProjectExec node counts (which diverge under Gluten due to transformer nodes and plan rewrites), aiming to validate correctness via result equivalence instead.
Changes:
- Reworks
GlutenRemoveRedundantProjectsSuite(Spark 4.0/4.1) to validate query results with redundant project removal enabled vs disabled. - Enables
GlutenRemoveRedundantProjectsSuitein Spark 4.0/4.1 Velox test settings and attempts to exclude the upstream Spark test cases that assert planner-node counts. - Clarifies why certain Spark-planner-detail suites remain disabled in Spark 4.x Velox settings.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| gluten-ut/spark41/src/test/scala/org/apache/spark/sql/execution/GlutenRemoveRedundantProjectsSuite.scala | Adds Gluten-specific result-based assertions intended to replace Spark’s plan-node-count assertions. |
| gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala | Enables the suite and excludes upstream Spark test names; updates comments about other disabled suites. |
| gluten-ut/spark40/src/test/scala/org/apache/spark/sql/execution/GlutenRemoveRedundantProjectsSuite.scala | Same as Spark 4.1 variant, for Spark 4.0. |
| gluten-ut/spark40/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala | Same enable/exclude configuration as Spark 4.1 variant, for Spark 4.0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { | ||
| val df = sql(query) | ||
| // When enabling AQE, the DPP subquery filters is replaced in runtime. | ||
| df.collect() | ||
| // assertProjectExecCount(df, enabled) | ||
| val result = df.collect() | ||
| withSQLConf(SQLConf.REMOVE_REDUNDANT_PROJECTS_ENABLED.key -> "false") { | ||
| val df2 = sql(query) | ||
| df2.collect() | ||
| // assertProjectExecCount(df2, disabled) | ||
| checkAnswer(df2, result) | ||
| } | ||
| } |
| private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { | ||
| val df = sql(query) | ||
| // When enabling AQE, the DPP subquery filters is replaced in runtime. | ||
| df.collect() | ||
| // assertProjectExecCount(df, enabled) | ||
| val result = df.collect() | ||
| withSQLConf(SQLConf.REMOVE_REDUNDANT_PROJECTS_ENABLED.key -> "false") { | ||
| val df2 = sql(query) | ||
| df2.collect() | ||
| // assertProjectExecCount(df2, disabled) | ||
| checkAnswer(df2, result) | ||
| } | ||
| } |
| val numProjects = collectWithSubqueries(plan) { case p: ProjectExec => p }.length | ||
|
|
||
| // Create a new plan that reverse the GenerateExec output and add a new ProjectExec between | ||
| // GenerateExec and its child. This is to test if the ProjectExec is removed, the output of | ||
| // the query will be incorrect. | ||
| val newPlan = stripAQEPlan(plan).transform { | ||
| case g @ GenerateExec(_, requiredChildOutput, _, _, child) => | ||
| g.copy( | ||
| requiredChildOutput = requiredChildOutput.reverse, | ||
| child = ProjectExec(requiredChildOutput.reverse, child)) | ||
| } | ||
|
|
||
| // Re-apply remove redundant project rule. | ||
| val rule = RemoveRedundantProjects | ||
| val newExecutedPlan = rule.apply(newPlan) | ||
| // The manually added ProjectExec node shouldn't be removed. | ||
| // assert(collectWithSubqueries(newExecutedPlan) { | ||
| // case p: ProjectExec => p | ||
| // }.size == numProjects + 1) |
| val numProjects = collectWithSubqueries(plan) { case p: ProjectExec => p }.length | ||
|
|
||
| // Create a new plan that reverse the GenerateExec output and add a new ProjectExec between | ||
| // GenerateExec and its child. This is to test if the ProjectExec is removed, the output of | ||
| // the query will be incorrect. | ||
| val newPlan = stripAQEPlan(plan).transform { | ||
| case g @ GenerateExec(_, requiredChildOutput, _, _, child) => | ||
| g.copy( | ||
| requiredChildOutput = requiredChildOutput.reverse, | ||
| child = ProjectExec(requiredChildOutput.reverse, child)) | ||
| } | ||
|
|
||
| // Re-apply remove redundant project rule. | ||
| val rule = RemoveRedundantProjects | ||
| val newExecutedPlan = rule.apply(newPlan) | ||
| // The manually added ProjectExec node shouldn't be removed. | ||
| // assert(collectWithSubqueries(newExecutedPlan) { | ||
| // case p: ProjectExec => p | ||
| // }.size == numProjects + 1) |
| enableSuite[GlutenRemoveRedundantProjectsSuite] | ||
| .exclude("project with filter") | ||
| .exclude("project with specific column ordering") | ||
| .exclude("project with extra columns") | ||
| .exclude("project with fewer columns") | ||
| .exclude("aggregate without ordering requirement") | ||
| .exclude("aggregate with ordering requirement") | ||
| .exclude("join without ordering requirement") | ||
| .exclude("join with ordering requirement") | ||
| .exclude("window function") | ||
| .exclude("generate should require column ordering") | ||
| .exclude("subquery") | ||
| .exclude("SPARK-33697: UnionExec should require column ordering") | ||
| .exclude("SPARK-33697: remove redundant projects under expand") | ||
| .exclude("SPARK-36020: Project should not be removed when child's logical link is different") |
| enableSuite[GlutenRemoveRedundantProjectsSuite] | ||
| .exclude("project with filter") | ||
| .exclude("project with specific column ordering") | ||
| .exclude("project with extra columns") | ||
| .exclude("project with fewer columns") | ||
| .exclude("aggregate without ordering requirement") | ||
| .exclude("aggregate with ordering requirement") | ||
| .exclude("join without ordering requirement") | ||
| .exclude("join with ordering requirement") | ||
| .exclude("window function") | ||
| .exclude("generate should require column ordering") | ||
| .exclude("subquery") | ||
| .exclude("SPARK-33697: UnionExec should require column ordering") | ||
| .exclude("SPARK-33697: remove redundant projects under expand") | ||
| .exclude("SPARK-36020: Project should not be removed when child's logical link is different") |
| private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { | ||
| val df = sql(query) | ||
| // When enabling AQE, the DPP subquery filters is replaced in runtime. | ||
| df.collect() | ||
| // assertProjectExecCount(df, enabled) | ||
| val result = df.collect() | ||
| withSQLConf(SQLConf.REMOVE_REDUNDANT_PROJECTS_ENABLED.key -> "false") { | ||
| val df2 = sql(query) | ||
| df2.collect() | ||
| // assertProjectExecCount(df2, disabled) | ||
| checkAnswer(df2, result) | ||
| } | ||
| } |
| val plan = df.queryExecution.executedPlan | ||
| val numProjects = collectWithSubqueries(plan) { case p: ProjectExec => p }.length |
| val rule = RemoveRedundantProjects | ||
| val newExecutedPlan = rule.apply(newPlan) | ||
| // The manually added ProjectExec node shouldn't be removed. | ||
| // assert(collectWithSubqueries(newExecutedPlan) { | ||
| // case p: ProjectExec => p | ||
| // }.size == numProjects + 1) |
| enableSuite[GlutenRemoveRedundantProjectsSuite] | ||
| // Rewrite as result checks because Gluten transforms and may pull out additional projects. | ||
| .exclude("project with filter") | ||
| .exclude("project with specific column ordering") | ||
| .exclude("project with extra columns") | ||
| .exclude("project with fewer columns") | ||
| .exclude("aggregate without ordering requirement") | ||
| .exclude("aggregate with ordering requirement") | ||
| .exclude("join without ordering requirement") | ||
| .exclude("join with ordering requirement") | ||
| .exclude("window function") | ||
| .exclude("generate should require column ordering") | ||
| .exclude("subquery") | ||
| .exclude("SPARK-33697: UnionExec should require column ordering") | ||
| .exclude("SPARK-33697: remove redundant projects under expand") | ||
| .exclude("SPARK-36020: Project should not be removed when child's logical link is different") |
| enableSuite[GlutenRemoveRedundantProjectsSuite] | ||
| // Rewrite as result checks because Gluten transforms and may pull out additional projects. | ||
| .exclude("project with filter") | ||
| .exclude("project with specific column ordering") | ||
| .exclude("project with extra columns") | ||
| .exclude("project with fewer columns") | ||
| .exclude("aggregate without ordering requirement") | ||
| .exclude("aggregate with ordering requirement") | ||
| .exclude("join without ordering requirement") | ||
| .exclude("join with ordering requirement") | ||
| .exclude("window function") | ||
| .exclude("generate should require column ordering") | ||
| .exclude("subquery") | ||
| .exclude("SPARK-33697: UnionExec should require column ordering") | ||
| .exclude("SPARK-33697: remove redundant projects under expand") | ||
| .exclude("SPARK-36020: Project should not be removed when child's logical link is different") |
| // project removal enabled and disabled. | ||
| private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { | ||
| val df = sql(query) | ||
| // When enabling AQE, the DPP subquery filters is replaced in runtime. |
| // TODO: 4.x enableSuite[GlutenPlannerSuite] // 1 failure | ||
| // TODO: 4.x enableSuite[GlutenProjectedOrderingAndPartitioningSuite] // 6 failures | ||
| // GlutenPlannerSuite is not enabled: it validates Spark planner implementation details. | ||
| // GlutenProjectedOrderingAndPartitioningSuite is not enabled: it validates Spark planner |
There was a problem hiding this comment.
@zml1206, Could we add a disableSuite[X]("reason") method in BackendTestSettings.scala to track intentionally disabled suites? This would be more explicit and easier to track than the current comment-based approach. Today we can get a similar effect with enableSuite[X].disable("reason"), but that reads oddly: we enable a suite and then immediately disable it. A dedicated disableSuite would state the intent directly.
private val disabledSuites: java.util.Map[String, String] = new util.HashMap() // name -> reason
protected def disableSuite[T: ClassTag](reason: String): Unit =
disableSuite(implicitly[ClassTag[T]].runtimeClass.getCanonicalName, reason)
protected def disableSuite(suiteName: String, reason: String): Unit = {
require(reason.nonEmpty, "Disable reason must not be empty")
if (enabledSuites.containsKey(suiteName)) {
throw new IllegalArgumentException("Suite is already enabled: " + suiteName)
}
if (disabledSuites.containsKey(suiteName)) {
throw new IllegalArgumentException("Duplicated disabled suite: " + suiteName)
}
disabledSuites.put(suiteName, reason)
}
There was a problem hiding this comment.
Thanks, @philo-he that's a good suggestion, but I'd like to handle it consistently in the next PR, since there are existing instances that need to be addressed in the same way.
| private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { | ||
| val df = sql(query) | ||
| // When enabling AQE, the DPP subquery filters is replaced in runtime. | ||
| df.collect() | ||
| // assertProjectExecCount(df, enabled) | ||
| val result = df.collect() | ||
| withSQLConf(SQLConf.REMOVE_REDUNDANT_PROJECTS_ENABLED.key -> "false") { | ||
| val df2 = sql(query) | ||
| df2.collect() | ||
| // assertProjectExecCount(df2, disabled) | ||
| checkAnswer(df2, result) | ||
| } | ||
| } |
| df.collect() | ||
| val plan = df.queryExecution.executedPlan | ||
| val numProjects = collectWithSubqueries(plan) { case p: ProjectExec => p }.length | ||
|
|
||
| // Create a new plan that reverse the GenerateExec output and add a new ProjectExec between | ||
| // GenerateExec and its child. This is to test if the ProjectExec is removed, the output of | ||
| // the query will be incorrect. | ||
| val newPlan = stripAQEPlan(plan).transform { | ||
| case g @ GenerateExec(_, requiredChildOutput, _, _, child) => | ||
| g.copy( | ||
| requiredChildOutput = requiredChildOutput.reverse, | ||
| child = ProjectExec(requiredChildOutput.reverse, child)) | ||
| } | ||
|
|
||
| // Re-apply remove redundant project rule. | ||
| val rule = RemoveRedundantProjects | ||
| val newExecutedPlan = rule.apply(newPlan) | ||
| // The manually added ProjectExec node shouldn't be removed. | ||
| // assert(collectWithSubqueries(newExecutedPlan) { | ||
| // case p: ProjectExec => p | ||
| // }.size == numProjects + 1) | ||
|
|
||
| // Check the original plan's output and the new plan's output are the same. |
| enableSuite[GlutenRemoveRedundantProjectsSuite] | ||
| // Rewrite as result checks because Gluten transforms and may pull out additional projects. | ||
| .exclude("project with filter") | ||
| .exclude("project with specific column ordering") | ||
| .exclude("project with extra columns") | ||
| .exclude("project with fewer columns") | ||
| .exclude("aggregate without ordering requirement") | ||
| .exclude("aggregate with ordering requirement") | ||
| .exclude("join without ordering requirement") | ||
| .exclude("join with ordering requirement") | ||
| .exclude("window function") | ||
| .exclude("generate should require column ordering") | ||
| .exclude("subquery") | ||
| .exclude("SPARK-33697: UnionExec should require column ordering") | ||
| .exclude("SPARK-33697: remove redundant projects under expand") | ||
| .exclude("SPARK-36020: Project should not be removed when child's logical link is different") |
| private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { | ||
| val df = sql(query) | ||
| // When enabling AQE, the DPP subquery filters is replaced in runtime. | ||
| df.collect() | ||
| // assertProjectExecCount(df, enabled) | ||
| val result = df.collect() | ||
| withSQLConf(SQLConf.REMOVE_REDUNDANT_PROJECTS_ENABLED.key -> "false") { | ||
| val df2 = sql(query) | ||
| df2.collect() | ||
| // assertProjectExecCount(df2, disabled) | ||
| checkAnswer(df2, result) | ||
| } | ||
| } |
| df.collect() | ||
| val plan = df.queryExecution.executedPlan | ||
| val numProjects = collectWithSubqueries(plan) { case p: ProjectExec => p }.length | ||
|
|
||
| // Create a new plan that reverse the GenerateExec output and add a new ProjectExec between | ||
| // GenerateExec and its child. This is to test if the ProjectExec is removed, the output of | ||
| // the query will be incorrect. | ||
| val newPlan = stripAQEPlan(plan).transform { | ||
| case g @ GenerateExec(_, requiredChildOutput, _, _, child) => | ||
| g.copy( | ||
| requiredChildOutput = requiredChildOutput.reverse, | ||
| child = ProjectExec(requiredChildOutput.reverse, child)) | ||
| } | ||
|
|
||
| // Re-apply remove redundant project rule. | ||
| val rule = RemoveRedundantProjects | ||
| val newExecutedPlan = rule.apply(newPlan) | ||
| // The manually added ProjectExec node shouldn't be removed. | ||
| // assert(collectWithSubqueries(newExecutedPlan) { | ||
| // case p: ProjectExec => p | ||
| // }.size == numProjects + 1) | ||
|
|
||
| // Check the original plan's output and the new plan's output are the same. |
| enableSuite[GlutenRemoveRedundantProjectsSuite] | ||
| // Rewrite as result checks because Gluten transforms and may pull out additional projects. | ||
| .exclude("project with filter") | ||
| .exclude("project with specific column ordering") | ||
| .exclude("project with extra columns") | ||
| .exclude("project with fewer columns") | ||
| .exclude("aggregate without ordering requirement") | ||
| .exclude("aggregate with ordering requirement") | ||
| .exclude("join without ordering requirement") | ||
| .exclude("join with ordering requirement") | ||
| .exclude("window function") | ||
| .exclude("generate should require column ordering") | ||
| .exclude("subquery") | ||
| .exclude("SPARK-33697: UnionExec should require column ordering") | ||
| .exclude("SPARK-33697: remove redundant projects under expand") | ||
| .exclude("SPARK-36020: Project should not be removed when child's logical link is different") |
Related issue: #11550