|
16 | 16 | */ |
17 | 17 | package org.apache.spark.sql.execution |
18 | 18 |
|
19 | | -import org.apache.spark.sql.GlutenSQLTestsTrait |
| 19 | +import org.apache.spark.sql.{GlutenSQLTestsTrait, Row} |
| 20 | +import org.apache.spark.sql.internal.SQLConf |
| 21 | +import org.apache.spark.sql.types.StructType |
20 | 22 |
|
21 | 23 | class GlutenRemoveRedundantProjectsSuite |
22 | 24 | extends RemoveRedundantProjectsSuite |
23 | | - with GlutenSQLTestsTrait {} |
| 25 | + with GlutenSQLTestsTrait { |
| 26 | + |
| 27 | + // The original tests count Spark ProjectExec nodes, while Gluten converts offloaded projects to |
| 28 | + // ProjectExecTransformer. PullOutPreProject and PullOutPostProject may also insert additional |
| 29 | + // projects while rewriting the Spark physical plan, so project counts are not directly |
| 30 | + // comparable. Therefore, these tests only verify that query results are identical with redundant |
| 31 | + // project removal enabled and disabled. |
| 32 | + private def assertProjectExec(query: String, enabled: Int, disabled: Int): Unit = { |
| 33 | + val df = sql(query) |
| 34 | + // When enabling AQE, the DPP subquery filters is replaced in runtime. |
| 35 | + df.collect() |
| 36 | + // assertProjectExecCount(df, enabled) |
| 37 | + val result = df.collect() |
| 38 | + withSQLConf(SQLConf.REMOVE_REDUNDANT_PROJECTS_ENABLED.key -> "false") { |
| 39 | + val df2 = sql(query) |
| 40 | + df2.collect() |
| 41 | + // assertProjectExecCount(df2, disabled) |
| 42 | + checkAnswer(df2, result) |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + testGluten("project with filter") { |
| 47 | + val query = "select * from testView where a > 5" |
| 48 | + assertProjectExec(query, 0, 1) |
| 49 | + } |
| 50 | + |
| 51 | + testGluten("project with specific column ordering") { |
| 52 | + val query = "select key, a, b, c from testView" |
| 53 | + assertProjectExec(query, 1, 1) |
| 54 | + } |
| 55 | + |
| 56 | + testGluten("project with extra columns") { |
| 57 | + val query = "select a, b, c, key, a from testView" |
| 58 | + assertProjectExec(query, 1, 1) |
| 59 | + } |
| 60 | + |
| 61 | + testGluten("project with fewer columns") { |
| 62 | + val query = "select a from testView where a > 3" |
| 63 | + assertProjectExec(query, 1, 1) |
| 64 | + } |
| 65 | + |
| 66 | + testGluten("aggregate without ordering requirement") { |
| 67 | + val query = "select sum(a) as sum_a, key, last(b) as last_b " + |
| 68 | + "from (select key, a, b from testView where a > 100) group by key" |
| 69 | + assertProjectExec(query, 0, 1) |
| 70 | + } |
| 71 | + |
| 72 | + testGluten("aggregate with ordering requirement") { |
| 73 | + val query = "select a, sum(b) as sum_b from testView group by a" |
| 74 | + assertProjectExec(query, 1, 1) |
| 75 | + } |
| 76 | + |
| 77 | + testGluten("join without ordering requirement") { |
| 78 | + val query = "select t1.key, t2.key, t1.a, t2.b from (select key, a, b, c from testView)" + |
| 79 | + " as t1 join (select key, a, b, c from testView) as t2 on t1.c > t2.c and t1.key > 10" |
| 80 | + assertProjectExec(query, 1, 3) |
| 81 | + } |
| 82 | + |
| 83 | + testGluten("join with ordering requirement") { |
| 84 | + val query = "select * from (select key, a, c, b from testView) as t1 join " + |
| 85 | + "(select key, a, b, c from testView) as t2 on t1.key = t2.key where t2.a > 50" |
| 86 | + assertProjectExec(query, 2, 2) |
| 87 | + } |
| 88 | + |
| 89 | + testGluten("window function") { |
| 90 | + val query = "select key, b, avg(a) over (partition by key order by a " + |
| 91 | + "rows between 1 preceding and 1 following) as avg from testView" |
| 92 | + assertProjectExec(query, 1, 2) |
| 93 | + } |
| 94 | + |
| 95 | + testGluten("generate should require column ordering") { |
| 96 | + withTempView("testData") { |
| 97 | + spark.range(0, 10, 1) |
| 98 | + .selectExpr("id as key", "id * 2 as a", "id * 3 as b") |
| 99 | + .createOrReplaceTempView("testData") |
| 100 | + |
| 101 | + val data = sql("select key, a, b, count(*) from testData group by key, a, b limit 2") |
| 102 | + val df = data.selectExpr("a", "b", "key", "explode(array(key, a, b)) as d").filter("d > 0") |
| 103 | + df.collect() |
| 104 | + val plan = df.queryExecution.executedPlan |
| 105 | + val numProjects = collectWithSubqueries(plan) { case p: ProjectExec => p }.length |
| 106 | + |
| 107 | + // Create a new plan that reverse the GenerateExec output and add a new ProjectExec between |
| 108 | + // GenerateExec and its child. This is to test if the ProjectExec is removed, the output of |
| 109 | + // the query will be incorrect. |
| 110 | + val newPlan = stripAQEPlan(plan).transform { |
| 111 | + case g @ GenerateExec(_, requiredChildOutput, _, _, child) => |
| 112 | + g.copy( |
| 113 | + requiredChildOutput = requiredChildOutput.reverse, |
| 114 | + child = ProjectExec(requiredChildOutput.reverse, child)) |
| 115 | + } |
| 116 | + |
| 117 | + // Re-apply remove redundant project rule. |
| 118 | + val rule = RemoveRedundantProjects |
| 119 | + val newExecutedPlan = rule.apply(newPlan) |
| 120 | + // The manually added ProjectExec node shouldn't be removed. |
| 121 | + // assert(collectWithSubqueries(newExecutedPlan) { |
| 122 | + // case p: ProjectExec => p |
| 123 | + // }.size == numProjects + 1) |
| 124 | + |
| 125 | + // Check the original plan's output and the new plan's output are the same. |
| 126 | + val expectedRows = plan.executeCollect() |
| 127 | + val actualRows = newExecutedPlan.executeCollect() |
| 128 | + assert(expectedRows.length == actualRows.length) |
| 129 | + expectedRows.zip(actualRows).foreach { case (expected, actual) => assert(expected == actual) } |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + testGluten("subquery") { |
| 134 | + withTempView("testData") { |
| 135 | + val data = spark.sparkContext.parallelize((1 to 100).map(i => Row(i, i.toString))) |
| 136 | + val schema = new StructType().add("key", "int").add("value", "string") |
| 137 | + spark.createDataFrame(data, schema).createOrReplaceTempView("testData") |
| 138 | + val query = "select key, value from testData where key in " + |
| 139 | + "(select sum(a) from testView where a > 5 group by key)" |
| 140 | + assertProjectExec(query, 0, 1) |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + testGluten("SPARK-33697: UnionExec should require column ordering") { |
| 145 | + withTable("t1", "t2") { |
| 146 | + spark.range(-10, 20) |
| 147 | + .selectExpr( |
| 148 | + "id", |
| 149 | + "date_add(date '1950-01-01', cast(id as int)) as datecol", |
| 150 | + "cast(id as string) strcol") |
| 151 | + .write.mode("overwrite").format("parquet").saveAsTable("t1") |
| 152 | + spark.range(-10, 20) |
| 153 | + .selectExpr( |
| 154 | + "cast(id as string) strcol", |
| 155 | + "id", |
| 156 | + "date_add(date '1950-01-01', cast(id as int)) as datecol") |
| 157 | + .write.mode("overwrite").format("parquet").saveAsTable("t2") |
| 158 | + |
| 159 | + val queryTemplate = |
| 160 | + """ |
| 161 | + |SELECT DISTINCT datecol, strcol FROM |
| 162 | + |( |
| 163 | + |(SELECT datecol, id, strcol from t1) |
| 164 | + | %s |
| 165 | + |(SELECT datecol, id, strcol from t2) |
| 166 | + |) |
| 167 | + |""".stripMargin |
| 168 | + |
| 169 | + Seq(("UNION", 1, 2), ("UNION ALL", 1, 2)).foreach { |
| 170 | + case (setOperation, enabled, disabled) => |
| 171 | + val query = queryTemplate.format(setOperation) |
| 172 | + assertProjectExec(query, enabled = enabled, disabled = disabled) |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + testGluten("SPARK-33697: remove redundant projects under expand") { |
| 178 | + val query = |
| 179 | + """ |
| 180 | + |SELECT t1.key, t2.key, sum(t1.a) AS s1, sum(t2.b) AS s2 FROM |
| 181 | + |(SELECT a, key FROM testView) t1 |
| 182 | + |JOIN |
| 183 | + |(SELECT b, key FROM testView) t2 |
| 184 | + |ON t1.key = t2.key |
| 185 | + |GROUP BY t1.key, t2.key GROUPING SETS(t1.key, t2.key) |
| 186 | + |ORDER BY t1.key, t2.key, s1, s2 |
| 187 | + |LIMIT 10 |
| 188 | + |""".stripMargin |
| 189 | + // The Project above the Expand is not removed due to SPARK-36020. |
| 190 | + assertProjectExec(query, 1, 3) |
| 191 | + } |
| 192 | + |
| 193 | + testGluten("SPARK-36020: Project should not be removed when child's logical link is different") { |
| 194 | + val query = |
| 195 | + """ |
| 196 | + |WITH t AS ( |
| 197 | + | SELECT key, a, b, c, explode(d) AS d FROM testView |
| 198 | + |) |
| 199 | + |SELECT t1.key, t1.d, t2.key |
| 200 | + |FROM (SELECT d, key FROM t) t1 |
| 201 | + |JOIN testView t2 ON t1.key = t2.key |
| 202 | + |""".stripMargin |
| 203 | + // The ProjectExec above the GenerateExec should not be removed because |
| 204 | + // they have different logical links. |
| 205 | + assertProjectExec(query, enabled = 2, disabled = 3) |
| 206 | + } |
| 207 | +} |
0 commit comments