voonhous commented on code in PR #19406:
URL: https://github.com/apache/hudi/pull/19406#discussion_r3690432981


##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -134,9 +118,6 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
 
         val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string,price:int>>"

Review Comment:
   Pre-existing, but worth fixing while this PR reworks the suite: on master 
this test goes through the **COW** arm below and asserts the **pruned** schema 
`{id, item{name}}` -- the opposite of the test's name -- while this unpruned 
`expectedReadSchemaClause` is never checked (the `RowDataSourceScanExec` arm is 
unreachable now that MOR reads resolve to `HadoopFsRelation`, see #17457). The 
commented-out asserts at lines 139/142 have been dead since #10245.
   
   **Action:** rename/rewrite this test around behavior that is actually 
asserted, or delete it; at minimum drop the commented-out asserts.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.

Review Comment:
   **This comment does not hold: `countLeaves` is never executed -- by these 
tests or by any non-metadata read.** The pruning asserted in this suite is done 
by Spark's built-in `SchemaPruning`.
   
   Evidence:
   - All 5 tests in this suite pass unchanged with 
`spark.sql.optimizer.excludedRules=org.apache.spark.sql.execution.datasources.HoodieNestedSchemaPruning`;
 excluding Spark's `SchemaPruning` instead makes the pruning disappear.
   - jacoco over the suite: the only line of `HoodieNestedSchemaPruning` this 
PR newly covers is the `else plan` arm of `apply` (via the disabled-flag test); 
`countLeaves`, `prunePhysicalColumns`, `buildNewProjection`, 
`buildPrunedRelation` all stay at 0.
   - Cause: since #17457 every non-metadata read returns a `HadoopFsRelation` 
(`HoodieFileGroupReaderBasedFileFormat extends ParquetFileFormat`, so Spark 
prunes natively). The rule only matches `HoodieBaseRelation if 
canPruneRelationSchema`, and `canPruneRelationSchema` requires 
`!isMetadataTable` -- but `DefaultSource` only builds a `HoodieBaseRelation` 
**for** the metadata table. The prune branch is unreachable from SQL.
   
   **Action:** reword the rule-internals comments here and at the two no-op 
tests, and retitle the tests and PR description to what is actually pinned: 
end-to-end nested schema pruning over Hudi's `HadoopFsRelation` read path. 
Separately, file a follow-up issue that `HoodieNestedSchemaPruning` has been 
dead code since #17457 -- delete it, or cover it with a direct unit test 
against a `HoodieBaseRelation`.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {

Review Comment:
   This test is assertion-identical to the `mor` leg of `Test 
NestedSchemaPruning optimization successful`: same `SELECT id, item.name`, same 
expected schema, same `ReadSchema` clause. The added `tags`/`props` columns 
change nothing that is asserted, because dropping unreferenced top-level 
columns is done by Spark's `ColumnPruning`, not nested schema pruning.
   
   **Action:** fold the array/map columns into 
`createTableWithNestedStructSchema` so the existing test covers this shape and 
drop this test, or differentiate it by projecting through the complex columns 
(see the comment on the `SELECT` below).



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")

Review Comment:
   No query in this suite has a `WHERE` on a nested field, so the pushed-filter 
plus pruned-schema interaction is untested (it is also the only input that 
would reach the `filters.nonEmpty` branch of `buildNewProjection`, if the rule 
were ever revived).
   
   **Action:** add `WHERE item.name = 'a1'` to one of the pruning cases and 
assert the returned values.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")

Review Comment:
   `tags` and `props` are only ever **dropped** here, never projected through, 
so the pruned schema never contains a complex type and the array/map recursion 
in `HoodieSchemaUtils.pruneDataSchema` (the path #18566 fixed) stays untested.
   
   **Action:** add projections through the complex columns and assert the 
values:
   - `SELECT tags.k FROM t` gives `ReadSchema: 
struct<tags:array<struct<k:string>>>`
   - `SELECT props['m0'].a FROM t` gives `ReadSchema: 
struct<props:map<string,struct<a:int>>>`



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+
+      val expectedSchema = StructType(Seq(
+        StructField("id", IntegerType, nullable = true),
+        StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
+      ))
+      val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string>>"
+
+      assertPrunedReadSchema(selectDF, tableName, expectedSchema, 
expectedReadSchemaClause)
+
+      // Execute the query to make sure it's working as expected (smoke test)
+      selectDF.count

Review Comment:
   No test in this suite ever creates a log file (all tables are CTAS-only), so 
this `count` reads base parquet only and never merges a pruned schema against 
delta records -- exactly the HUDI-5443 / #7528 bug class (exception reading a 
MOR table after `NestedSchemaPruning` when delta-log merging kicks in), also 
the case called out in #18570.
   
   **Action:** run `UPDATE $tableName SET ... WHERE id = 1` before the 
projection so the file slice has a log file, and assert the returned rows with 
`checkAnswer(...)` instead of `selectDF.count`.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+
+      val expectedSchema = StructType(Seq(
+        StructField("id", IntegerType, nullable = true),
+        StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
+      ))
+      val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string>>"
+
+      assertPrunedReadSchema(selectDF, tableName, expectedSchema, 
expectedReadSchemaClause)
+
+      // Execute the query to make sure it's working as expected (smoke test)
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when all nested sub-fields are 
selected") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // Every leaf is projected, so the pruned schema has the same leaf count 
as the data schema and
+      // the rule leaves "item" untouched (the countLeaves comparison is an 
equality, not a >).
+      val selectDF = spark.sql(s"SELECT id, item.name, item.price, ts FROM 
$tableName")
+
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+      assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when nested schema pruning is 
disabled") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // With the optimizer flag off the rule short-circuits, so "item" keeps 
"price" even though only
+      // "item.name" is projected.
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+
+      withSQLConf(SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key -> "false") {
+        val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+        assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+        selectDF.count
+      }
+    }
+  }
+
+  private def assertPrunedReadSchema(selectDF: DataFrame,
+                                     tableName: String,
+                                     expectedSchema: StructType,
+                                     expectedReadSchemaClause: String,
+                                     hint: String = ""): Unit = {
+    // NOTE: Unfortunately, we can't use pattern-matching to extract required 
fields, due to a need to maintain
+    //       compatibility w/ Spark 2.4
+    selectDF.queryExecution.executedPlan match {
+      // COW
+      case ProjectExec(_, fileScan: FileSourceScanExec) =>
+        assertEquals(tableName, fileScan.tableIdentifier.get.table)
+        assertEquals(expectedSchema, fileScan.requiredSchema, hint)
+
+      // MOR
+      case ProjectExec(_, dataScan: RowDataSourceScanExec) =>
+        // NOTE: This is temporary solution to assert for Spark 2.4, until 
it's deprecated
+        val explainedPlan = explain(selectDF.queryExecution.logical)
+        assertTrue(explainedPlan.contains(expectedReadSchemaClause))
+
+        assertEquals(tableName, dataScan.tableIdentifier.get.table)
+        assertEquals(expectedSchema, dataScan.requiredSchema, hint)
+    }
+  }
+
+  private def prunedStructTypeOf(selectDF: DataFrame, fieldName: String): 
StructType = {
+    val requiredSchema = selectDF.queryExecution.executedPlan match {
+      case ProjectExec(_, fileScan: FileSourceScanExec) => 
fileScan.requiredSchema
+      case ProjectExec(_, dataScan: RowDataSourceScanExec) => 
dataScan.requiredSchema
+      case fileScan: FileSourceScanExec => fileScan.requiredSchema
+      case dataScan: RowDataSourceScanExec => dataScan.requiredSchema
+    }
+    requiredSchema(fieldName).dataType.asInstanceOf[StructType]
+  }
+
+  private def createTableWithComplexNestedSchema(tableName: String,

Review Comment:
   nit (feel free to ignore): 17-line copy of 
`createTableWithNestedStructSchema` differing only in the select list and the 
hardcoded `type = 'mor'`. An `extraColumns: String = ""` parameter on the 
existing helper would serve both.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+
+      val expectedSchema = StructType(Seq(
+        StructField("id", IntegerType, nullable = true),
+        StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
+      ))
+      val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string>>"
+
+      assertPrunedReadSchema(selectDF, tableName, expectedSchema, 
expectedReadSchemaClause)
+
+      // Execute the query to make sure it's working as expected (smoke test)
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when all nested sub-fields are 
selected") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // Every leaf is projected, so the pruned schema has the same leaf count 
as the data schema and
+      // the rule leaves "item" untouched (the countLeaves comparison is an 
equality, not a >).
+      val selectDF = spark.sql(s"SELECT id, item.name, item.price, ts FROM 
$tableName")
+
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+      assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when nested schema pruning is 
disabled") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // With the optimizer flag off the rule short-circuits, so "item" keeps 
"price" even though only
+      // "item.name" is projected.
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+
+      withSQLConf(SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key -> "false") {
+        val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+        assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+        selectDF.count
+      }
+    }
+  }
+
+  private def assertPrunedReadSchema(selectDF: DataFrame,
+                                     tableName: String,
+                                     expectedSchema: StructType,
+                                     expectedReadSchemaClause: String,
+                                     hint: String = ""): Unit = {
+    // NOTE: Unfortunately, we can't use pattern-matching to extract required 
fields, due to a need to maintain
+    //       compatibility w/ Spark 2.4

Review Comment:
   nit: stale note copied along -- Spark 2.4 support is long gone (oldest 
supported profile is 3.3, Spark 3.0-3.2 removed in ef8a7ad4f17a).
   
   ```suggestion
   ```



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+
+      val expectedSchema = StructType(Seq(
+        StructField("id", IntegerType, nullable = true),
+        StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
+      ))
+      val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string>>"
+
+      assertPrunedReadSchema(selectDF, tableName, expectedSchema, 
expectedReadSchemaClause)
+
+      // Execute the query to make sure it's working as expected (smoke test)
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when all nested sub-fields are 
selected") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // Every leaf is projected, so the pruned schema has the same leaf count 
as the data schema and
+      // the rule leaves "item" untouched (the countLeaves comparison is an 
equality, not a >).
+      val selectDF = spark.sql(s"SELECT id, item.name, item.price, ts FROM 
$tableName")
+
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+      assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when nested schema pruning is 
disabled") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // With the optimizer flag off the rule short-circuits, so "item" keeps 
"price" even though only
+      // "item.name" is projected.
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+
+      withSQLConf(SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key -> "false") {
+        val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+        assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+        selectDF.count
+      }
+    }
+  }
+
+  private def assertPrunedReadSchema(selectDF: DataFrame,
+                                     tableName: String,
+                                     expectedSchema: StructType,
+                                     expectedReadSchemaClause: String,
+                                     hint: String = ""): Unit = {
+    // NOTE: Unfortunately, we can't use pattern-matching to extract required 
fields, due to a need to maintain
+    //       compatibility w/ Spark 2.4
+    selectDF.queryExecution.executedPlan match {
+      // COW
+      case ProjectExec(_, fileScan: FileSourceScanExec) =>
+        assertEquals(tableName, fileScan.tableIdentifier.get.table)
+        assertEquals(expectedSchema, fileScan.requiredSchema, hint)
+
+      // MOR
+      case ProjectExec(_, dataScan: RowDataSourceScanExec) =>

Review Comment:
   Dead code carried into the new helper: MOR reads resolve to 
`ProjectExec(FileSourceScanExec)` on master (FGR-based `HadoopFsRelation`), so 
this `RowDataSourceScanExec` arm never runs (jacoco: lines 233-239 unexecuted 
across the whole suite). Consequently `expectedReadSchemaClause` is never 
asserted (the literal at line 167 is dead) and `explain`/`executePlan` exist 
only for this arm. Same story for 3 of the 4 cases in `prunedStructTypeOf` 
(lines 246-248).
   
   **Action:** drop the `RowDataSourceScanExec` arms, the 
`expectedReadSchemaClause` parameter, and the `explain`/`executePlan` helpers; 
add `case other => fail(s"unexpected plan shape: $other")` so a future 
plan-shape change fails with a message instead of a `MatchError`.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+
+      val expectedSchema = StructType(Seq(
+        StructField("id", IntegerType, nullable = true),
+        StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
+      ))
+      val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string>>"
+
+      assertPrunedReadSchema(selectDF, tableName, expectedSchema, 
expectedReadSchemaClause)
+
+      // Execute the query to make sure it's working as expected (smoke test)
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when all nested sub-fields are 
selected") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // Every leaf is projected, so the pruned schema has the same leaf count 
as the data schema and
+      // the rule leaves "item" untouched (the countLeaves comparison is an 
equality, not a >).
+      val selectDF = spark.sql(s"SELECT id, item.name, item.price, ts FROM 
$tableName")
+
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+      assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when nested schema pruning is 
disabled") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // With the optimizer flag off the rule short-circuits, so "item" keeps 
"price" even though only
+      // "item.name" is projected.
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+
+      withSQLConf(SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key -> "false") {
+        val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+        assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))
+        selectDF.count
+      }
+    }
+  }
+
+  private def assertPrunedReadSchema(selectDF: DataFrame,
+                                     tableName: String,
+                                     expectedSchema: StructType,
+                                     expectedReadSchemaClause: String,
+                                     hint: String = ""): Unit = {
+    // NOTE: Unfortunately, we can't use pattern-matching to extract required 
fields, due to a need to maintain
+    //       compatibility w/ Spark 2.4
+    selectDF.queryExecution.executedPlan match {
+      // COW
+      case ProjectExec(_, fileScan: FileSourceScanExec) =>
+        assertEquals(tableName, fileScan.tableIdentifier.get.table)
+        assertEquals(expectedSchema, fileScan.requiredSchema, hint)
+
+      // MOR
+      case ProjectExec(_, dataScan: RowDataSourceScanExec) =>
+        // NOTE: This is temporary solution to assert for Spark 2.4, until 
it's deprecated
+        val explainedPlan = explain(selectDF.queryExecution.logical)
+        assertTrue(explainedPlan.contains(expectedReadSchemaClause))
+
+        assertEquals(tableName, dataScan.tableIdentifier.get.table)
+        assertEquals(expectedSchema, dataScan.requiredSchema, hint)
+    }
+  }
+
+  private def prunedStructTypeOf(selectDF: DataFrame, fieldName: String): 
StructType = {
+    val requiredSchema = selectDF.queryExecution.executedPlan match {
+      case ProjectExec(_, fileScan: FileSourceScanExec) => 
fileScan.requiredSchema
+      case ProjectExec(_, dataScan: RowDataSourceScanExec) => 
dataScan.requiredSchema
+      case fileScan: FileSourceScanExec => fileScan.requiredSchema
+      case dataScan: RowDataSourceScanExec => dataScan.requiredSchema
+    }
+    requiredSchema(fieldName).dataType.asInstanceOf[StructType]
+  }
+
+  private def createTableWithComplexNestedSchema(tableName: String,
+                                                 tablePath: String): Unit = {
+    spark.sql(
+      s"""
+         |CREATE TABLE $tableName USING HUDI TBLPROPERTIES (

Review Comment:
   All tables in this suite are non-partitioned, so pruning over a partitioned 
schema is untested. #18570 was exactly this: `FileGroupReader` dropped 
mandatory partition columns from the pruned `dataSchema`, returning silent 
nulls -- which a `count` smoke test cannot catch.
   
   **Action:** add one `PARTITIONED BY` variant and assert returned values 
including the partition column.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala:
##########
@@ -167,6 +148,128 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase with Sp
     }
   }
 
+  test("Test NestedSchemaPruning prunes nested struct when array and map 
columns are present") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithComplexNestedSchema(tableName, tablePath)
+
+      // Only a single nested sub-field is projected, so "item" is pruned down 
to just "name" and the
+      // unreferenced "tags" (array<struct>) and "props" (map<string,struct>) 
columns are dropped.
+      // Traversing the full data schema exercises the array and map branches 
of countLeaves.
+      val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
+
+      val expectedSchema = StructType(Seq(
+        StructField("id", IntegerType, nullable = true),
+        StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
+      ))
+      val expectedReadSchemaClause = "ReadSchema: 
struct<id:int,item:struct<name:string>>"
+
+      assertPrunedReadSchema(selectDF, tableName, expectedSchema, 
expectedReadSchemaClause)
+
+      // Execute the query to make sure it's working as expected (smoke test)
+      selectDF.count
+    }
+  }
+
+  test("Test NestedSchemaPruning is a no-op when all nested sub-fields are 
selected") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+      createTableWithNestedStructSchema("mor", tableName, tablePath)
+
+      // Every leaf is projected, so the pruned schema has the same leaf count 
as the data schema and
+      // the rule leaves "item" untouched (the countLeaves comparison is an 
equality, not a >).
+      val selectDF = spark.sql(s"SELECT id, item.name, item.price, ts FROM 
$tableName")
+
+      val expectedItemStruct = StructType(Seq(
+        StructField("name", StringType, nullable = false),
+        StructField("price", IntegerType, nullable = false)
+      ))
+      assertEquals(expectedItemStruct, prunedStructTypeOf(selectDF, "item"))

Review Comment:
   This assertion cannot fail: with every leaf projected, 
`requiredSchema("item")` is `struct<name,price>` whether the rule no-ops on the 
leaf-count equality, fires and rebuilds an identical relation, or is removed 
from the optimizer entirely (verified: byte-identical plan with the rule in 
`spark.sql.optimizer.excludedRules`). It pins no behavior.
   
   **Action:** drop this test, or make it discriminating, e.g. assert 
`queryExecution.optimizedPlan` equals the plan of the same query executed with 
the pruning rule excluded.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to