This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new d98f2f1b4752 refactor(spark): remove dead HoodieNestedSchemaPruning 
rule (#19458)
d98f2f1b4752 is described below

commit d98f2f1b4752845167527a69f1912d6ad4980921
Author: voonhous <[email protected]>
AuthorDate: Mon Aug 3 16:02:09 2026 +0800

    refactor(spark): remove dead HoodieNestedSchemaPruning rule (#19458)
    
    * refactor(spark): remove dead HoodieNestedSchemaPruning rule
    
    The rule only rewrites LogicalRelation(relation: HoodieBaseRelation) when
    relation.canPruneRelationSchema is true. Since #14061 removed the
    FILE_GROUP_READER_ENABLED fallback from DefaultSource and #17457 finished
    the FileGroupReader migration, every non-metadata read returns a
    HadoopFsRelation; the only remaining HoodieBaseRelation producers are
    metadata-table reads and the schema-on-read branch, both of which
    canPruneRelationSchema rejects, so the rule can never fire. Nested schema
    pruning for users is performed by Spark's built-in SchemaPruning, since
    the file-group-reader file format extends ParquetFileFormat.
    
    Also remove the members stranded by the rule: canPruneRelationSchema,
    updatePrunedDataSchema, dataSchema, type Relation and the prunedDataSchema
    constructor parameter on HoodieBaseRelation and its subclasses,
    MergeOnReadSnapshotRelation.isProjectionCompatible, and
    BaseHoodieCatalystPlanUtils.projectOverSchema.
    
    TestNestedSchemaPruningOptimization is kept unchanged; it covers the
    Spark-native pruning path (FileSourceScanExec.requiredSchema) that serves
    users today.
    
    Fixes #19447
    
    * refactor(spark): prune stranded members and harden nested-pruning coverage
    
    Follow-up to the HoodieNestedSchemaPruning removal, addressing self-review
    findings:
    
    - Remove HoodieTableState.recordPayloadClassName: its only reader was the
      deleted MergeOnReadSnapshotRelation.isProjectionCompatible. Drop the
      now-unused ParquetFileFormat import in HoodieBaseRelation.
    - SchemaHandlerTestBase#testMor: add a nested-narrowed projection block
      ("fare" pruned to its "amount" leaf) asserting that a
      projection-incompatible custom merger re-expands to the full data
      schema. The #7528 (HUDI-5443) payload gate retires with this PR because
      FileGroupReaderSchemaHandler#generateRequiredSchema enforces the same
      invariant through HoodieRecordMerger#isProjectionCompatible; this pins
      it so a future HoodieBaseRelation-targeting pruning rule cannot
      silently lose it.
    - TestNestedSchemaPruningOptimization: replace the payload-invariant
      DefaultHoodieRecordPayload case with a projection-incompatible custom
      payload that merges through a log file end to end; note on the suite
      that the pruning under test is Spark's own SchemaPruning rule.
    
    * test(common): skip the nested-narrowed assertion for incompatible custom 
mergers
    
    The projection-incompatible custom-merger branch of generateRequiredSchema
    returns the full data schema before looking at the requested schema, so the
    nested-narrowed request asserted exactly what the flat-projection block 
above
    already pins. Run the nested-narrowed block only for the arms where the
    narrowing has to survive.
    
    Addresses review feedback on #19458.
    
    * test(spark): describe the second checkAnswer as its own pruned query
    
    The second checkAnswer issues an independent query that Spark prunes to
    (id, item.price, ts), so it never observes leaves the first query's read
    schema dropped; say what it actually pins.
    
    Addresses review feedback on #19458.
    
    * test(common): pin nested mandatory-field merge into a narrowed record
    
    A merger declaring the nested leaf fare.currency mandatory against a request
    narrowed to fare.amount forces generateRequiredSchema through the
    appendFieldsToSchemaDedupNested/mergeSchemas collision path, which the
    existing mandatory fields (all top-level, disjoint from fare) never reach.
    
    Suggested by review on #19458.
---
 .../common/table/read/SchemaHandlerTestBase.java   |  45 ++++-
 .../read/TestFileGroupReaderSchemaHandler.java     |  14 ++
 .../org/apache/hudi/BaseFileOnlyRelation.scala     |   9 +-
 .../scala/org/apache/hudi/HoodieBaseRelation.scala |  62 +------
 .../hudi/HoodieHadoopFsRelationFactory.scala       |   4 +-
 .../hudi/MergeOnReadIncrementalRelationV1.scala    |  10 +-
 .../hudi/MergeOnReadIncrementalRelationV2.scala    |   8 +-
 .../apache/hudi/MergeOnReadSnapshotRelation.scala  |  40 +---
 .../spark/sql/BaseHoodieCatalystPlanUtils.scala    |   9 +-
 .../datasources/HoodieNestedSchemaPruning.scala    | 206 ---------------------
 .../spark/sql/hudi/analysis/HoodieAnalysis.scala   |   5 +-
 .../TestNestedSchemaPruningOptimization.scala      |  24 ++-
 12 files changed, 95 insertions(+), 341 deletions(-)

diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/SchemaHandlerTestBase.java
 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/SchemaHandlerTestBase.java
index f29c0b53a70b..047c08cdd873 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/SchemaHandlerTestBase.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/SchemaHandlerTestBase.java
@@ -130,6 +130,27 @@ public abstract class SchemaHandlerTestBase {
     }
     assertEquals(expectedRequiredSchema, schemaHandler.getRequiredSchema());
     assertFalse(readerContext.getNeedsBootstrapMerge());
+
+    //read subset of columns with a nested-narrowed field, the shape Spark's 
nested schema pruning
+    //requests: "fare" keeps only its "amount" leaf. Skipped for a 
projection-incompatible custom
+    //merger: that branch returns the full data schema before looking at the 
requested schema, so
+    //the flat-projection assertion above already covers it
+    if (!(mergeMode == CUSTOM && !isProjectionCompatible)) {
+      requestedSchema = 
narrowFareToAmountOnly(generateProjectionSchema("begin_lat", "fare", "rider"));
+      schemaHandler = createSchemaHandler(readerContext, dataSchema, 
requestedSchema, supportsParquetRowIndex);
+      if (mergeMode == EVENT_TIME_ORDERING && hasPrecombine) {
+        expectedRequiredSchema = 
narrowFareToAmountOnly(generateProjectionSchema(hasBuiltInDelete, "begin_lat", 
"fare", "rider", "_hoodie_record_key", "timestamp"));
+      } else if (mergeMode == EVENT_TIME_ORDERING || mergeMode == 
COMMIT_TIME_ORDERING) {
+        expectedRequiredSchema = 
narrowFareToAmountOnly(generateProjectionSchema(hasBuiltInDelete, "begin_lat", 
"fare", "rider", "_hoodie_record_key"));
+      } else {
+        expectedRequiredSchema = 
narrowFareToAmountOnly(generateProjectionSchema("begin_lat", "fare", "rider", 
"begin_lon", "_hoodie_record_key", "timestamp"));
+      }
+      if (supportsParquetRowIndex && mergeUseRecordPosition) {
+        expectedRequiredSchema = addPositionalMergeCol(expectedRequiredSchema);
+      }
+      assertEquals(expectedRequiredSchema, schemaHandler.getRequiredSchema());
+      assertFalse(readerContext.getNeedsBootstrapMerge());
+    }
   }
 
   public void testMorBootstrap(RecordMergeMode mergeMode,
@@ -255,7 +276,7 @@ public abstract class SchemaHandlerTestBase {
     assertEquals(expectedBootstrapFields.getRight(), 
bootstrapFields.getRight());
   }
 
-  private static void setupMORTable(RecordMergeMode mergeMode, boolean 
hasPrecombine, HoodieTableConfig hoodieTableConfig) {
+  static void setupMORTable(RecordMergeMode mergeMode, boolean hasPrecombine, 
HoodieTableConfig hoodieTableConfig) {
     when(hoodieTableConfig.populateMetaFields()).thenReturn(true);
     when(hoodieTableConfig.getRecordMergeMode()).thenReturn(mergeMode);
     
when(hoodieTableConfig.getTableVersion()).thenReturn(HoodieTableVersion.current());
@@ -271,7 +292,7 @@ public abstract class SchemaHandlerTestBase {
     }
   }
 
-  private static HoodieRecordMerger mockRecordMerger(boolean 
isProjectionCompatible, String[] mandatoryFields) throws IOException {
+  static HoodieRecordMerger mockRecordMerger(boolean isProjectionCompatible, 
String[] mandatoryFields) throws IOException {
     HoodieRecordMerger merger = mock(HoodieRecordMerger.class);
     when(merger.isProjectionCompatible()).thenReturn(isProjectionCompatible);
     when(merger.merge(any(), any(), any(), any())).thenReturn(null);
@@ -305,6 +326,26 @@ public abstract class SchemaHandlerTestBase {
     return HoodieSchemaUtils.generateProjectionSchema(DATA_SCHEMA, fieldList);
   }
 
+  /**
+   * Rebuilds the given projection schema, narrowing its "fare" record down to 
the "amount" leaf to
+   * mimic the schema shape Spark's nested schema pruning requests.
+   */
+  static HoodieSchema narrowFareToAmountOnly(HoodieSchema projectionSchema) {
+    List<HoodieSchemaField> fields = new 
ArrayList<>(projectionSchema.getFields().size());
+    for (HoodieSchemaField field : projectionSchema.getFields()) {
+      if (field.name().equals("fare")) {
+        HoodieSchema fare = field.schema();
+        HoodieSchema narrowedFare = HoodieSchema.createRecord(fare.getName(), 
fare.getNamespace().orElse(null), fare.getDoc().orElse(null),
+            
Collections.singletonList(HoodieSchemaUtils.createNewSchemaField(fare.getField("amount").get())));
+        fields.add(HoodieSchemaUtils.createNewSchemaField(field.name(), 
narrowedFare, field.doc().orElse(null), field.defaultVal().orElse(null)));
+      } else {
+        fields.add(HoodieSchemaUtils.createNewSchemaField(field));
+      }
+    }
+    return HoodieSchema.createRecord(projectionSchema.getName(), 
projectionSchema.getNamespace().orElse(null),
+        projectionSchema.getDoc().orElse(null), fields);
+  }
+
   HoodieSchemaField getField(String fieldName) {
     return DATA_SCHEMA.getField(fieldName).get();
   }
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestFileGroupReaderSchemaHandler.java
 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestFileGroupReaderSchemaHandler.java
index 8a2549e16c14..8e9026cd10f7 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestFileGroupReaderSchemaHandler.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestFileGroupReaderSchemaHandler.java
@@ -145,6 +145,20 @@ public class TestFileGroupReaderSchemaHandler extends 
SchemaHandlerTestBase {
     super.testMor(mergeMode, hasPrecombine, isProjectionCompatible, 
mergeUseRecordPosition, supportsParquetRowIndex, hasBuiltInDelete);
   }
 
+  @Test
+  public void testMorNestedMandatoryFieldMergesIntoNarrowedRecord() throws 
IOException {
+    setupMORTable(RecordMergeMode.CUSTOM, false, hoodieTableConfig);
+    HoodieRecordMerger merger = mockRecordMerger(true, new String[] 
{"fare.currency"});
+    HoodieReaderContext<String> readerContext = 
createReaderContext(hoodieTableConfig, false, true, false, false, merger);
+
+    //the request narrows "fare" to its "amount" leaf while the merger 
declares the sibling leaf
+    //"fare.currency" mandatory, so the handler has to merge the two 
projections of "fare"
+    HoodieSchema requestedSchema = 
narrowFareToAmountOnly(generateProjectionSchema("begin_lat", "fare", "rider"));
+    FileGroupReaderSchemaHandler schemaHandler = 
createSchemaHandler(readerContext, DATA_SCHEMA, requestedSchema, false);
+    assertEquals(generateProjectionSchema("begin_lat", "fare", "rider"), 
schemaHandler.getRequiredSchema());
+    assertFalse(readerContext.getNeedsBootstrapMerge());
+  }
+
   @ParameterizedTest
   @MethodSource("testMorParams")
   public void testMorBootstrap(RecordMergeMode mergeMode,
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/BaseFileOnlyRelation.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/BaseFileOnlyRelation.scala
index 301f9c904fed..0a3c5d2b979e 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/BaseFileOnlyRelation.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/BaseFileOnlyRelation.scala
@@ -50,15 +50,13 @@ import org.apache.spark.sql.types.StructType
 case class BaseFileOnlyRelation(override val sqlContext: SQLContext,
                                 override val metaClient: HoodieTableMetaClient,
                                 override val optParams: Map[String, String],
-                                private val userSchema: Option[StructType],
-                                private val prunedDataSchema: 
Option[StructType] = None)
-  extends HoodieBaseRelation(sqlContext, metaClient, optParams, userSchema, 
prunedDataSchema)
+                                private val userSchema: Option[StructType])
+  extends HoodieBaseRelation(sqlContext, metaClient, optParams, userSchema)
     with SparkAdapterSupport {
 
   case class HoodieBaseFileSplit(filePartition: FilePartition) extends 
HoodieFileSplit
 
   override type FileSplit = HoodieBaseFileSplit
-  override type Relation = BaseFileOnlyRelation
 
   // TODO(HUDI-3204) this is to override behavior (exclusively) for COW tables 
to always extract
   //                 partition values from partition path
@@ -74,9 +72,6 @@ case class BaseFileOnlyRelation(override val sqlContext: 
SQLContext,
   // Since Spark 3.4.0: FileIndexOptions.BASE_PATH_PARAM
   val BASE_PATH_PARAM = "basePath"
 
-  override def updatePrunedDataSchema(prunedSchema: StructType): Relation =
-    this.copy(prunedDataSchema = Some(prunedSchema))
-
   protected override def composeRDD(fileSplits: Seq[HoodieBaseFileSplit],
                                     tableSchema: HoodieTableSchema,
                                     requiredSchema: HoodieTableSchema,
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieBaseRelation.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieBaseRelation.scala
index d6f045338350..e1e01103bdf2 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieBaseRelation.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieBaseRelation.scala
@@ -45,7 +45,6 @@ import org.apache.hudi.exception.HoodieException
 import org.apache.hudi.hadoop.fs.HadoopFSUtils
 import org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToStoragePath
 import org.apache.hudi.io.storage.HoodieSparkIOFactory
-import org.apache.hudi.metadata.HoodieTableMetadata
 import org.apache.hudi.storage.{StoragePath, StoragePathInfo}
 
 import org.apache.avro.generic.GenericRecord
@@ -64,7 +63,7 @@ import org.apache.spark.sql.catalyst.expressions.{Expression, 
SubqueryExpression
 import org.apache.spark.sql.execution.FileRelation
 import org.apache.spark.sql.execution.datasources._
 import org.apache.spark.sql.execution.datasources.orc.OrcFileFormat
-import 
org.apache.spark.sql.execution.datasources.parquet.{LegacyHoodieParquetFileFormat,
 ParquetFileFormat}
+import 
org.apache.spark.sql.execution.datasources.parquet.LegacyHoodieParquetFileFormat
 import org.apache.spark.sql.hudi.{HoodieSqlCommonUtils, ProvidesHoodieConfig}
 import org.apache.spark.sql.sources.{BaseRelation, Filter, PrunedFilteredScan}
 import org.apache.spark.sql.types.StructType
@@ -81,7 +80,6 @@ case class HoodieTableState(tablePath: String,
                             recordKeyField: String,
                             orderingFields: List[String],
                             usesVirtualKeys: Boolean,
-                            recordPayloadClassName: String,
                             metadataConfig: HoodieMetadataConfig,
                             recordMergeImplClasses: List[String],
                             recordMergeStrategyId: String)
@@ -93,15 +91,13 @@ case class HoodieTableState(tablePath: String,
 abstract class HoodieBaseRelation(val sqlContext: SQLContext,
                                   val metaClient: HoodieTableMetaClient,
                                   val optParams: Map[String, String],
-                                  private val schemaSpec: Option[StructType],
-                                  private val prunedDataSchema: 
Option[StructType])
+                                  private val schemaSpec: Option[StructType])
   extends BaseRelation
     with FileRelation
     with PrunedFilteredScan
     with Logging {
 
   type FileSplit <: HoodieFileSplit
-  type Relation <: HoodieBaseRelation
 
   imbueConfigs(sqlContext)
 
@@ -235,10 +231,6 @@ abstract class HoodieBaseRelation(val sqlContext: 
SQLContext,
     shouldOmitPartitionColumns || shouldExtractPartitionValueFromPath || 
shouldUseBootstrapFastRead
   }
 
-  /**
-   * NOTE: This fields are accessed by [[NestedSchemaPruning]] component which 
is only enabled for
-   *       Spark >= 3.1
-   */
   protected lazy val (fileFormat: FileFormat, fileFormatClassName: String) =
     metaClient.getTableConfig.getBaseFileFormat match {
       case HoodieFileFormat.ORC => (new OrcFileFormat, "orc")
@@ -268,7 +260,6 @@ abstract class HoodieBaseRelation(val sqlContext: 
SQLContext,
       recordKeyField = recordKeyField,
       orderingFields = orderingFields,
       usesVirtualKeys = !tableConfig.populateMetaFields(),
-      recordPayloadClassName = tableConfig.getPayloadClass,
       metadataConfig = fileIndex.getMetadataConfig,
       recordMergeImplClasses = recordMergerImpls,
       recordMergeStrategyId = tableConfig.getRecordMergeStrategyId
@@ -297,39 +288,9 @@ abstract class HoodieBaseRelation(val sqlContext: 
SQLContext,
    */
   def hasSchemaOnRead: Boolean = internalSchemaOpt.isDefined
 
-  /**
-   * Data schema is determined as the actual schema of the Table's Data Files 
(for ex, parquet/orc/etc);
-   *
-   * In cases when partition values are not persisted w/in the data files, 
data-schema is defined as
-   * <pre>table's schema - partition columns</pre>
-   *
-   * Check scala-doc for [[shouldExtractPartitionValuesFromPartitionPath]] for 
more details
-   */
-  def dataSchema: StructType = if 
(shouldExtractPartitionValuesFromPartitionPath) {
-    prunePartitionColumns(tableStructSchema)
-  } else {
-    tableStructSchema
-  }
-
-  /**
-   * Determines whether relation's schema could be pruned by Spark's Optimizer
-   */
-  def canPruneRelationSchema: Boolean =
-    !HoodieTableMetadata.isMetadataTable(basePath.toString) &&
-      (fileFormat.isInstanceOf[ParquetFileFormat] || 
fileFormat.isInstanceOf[OrcFileFormat]) &&
-      // NOTE: In case this relation has already been pruned there's no point 
in pruning it again
-      prunedDataSchema.isEmpty &&
-      // TODO(HUDI-5421) internal schema doesn't support nested schema pruning 
currently
-      !hasSchemaOnRead
-
   override def sizeInBytes: Long = fileIndex.sizeInBytes
 
-  override def schema: StructType = {
-    // NOTE: Optimizer could prune the schema (applying for ex, 
[[NestedSchemaPruning]] rule) setting new updated
-    //       schema in-place (via [[setPrunedDataSchema]] method), therefore 
we have to make sure that we pick
-    //       pruned data schema (if present) over the standard table's one
-    prunedDataSchema.getOrElse(tableStructSchema)
-  }
+  override def schema: StructType = tableStructSchema
 
   /**
    * This method controls whether relation will be producing
@@ -355,13 +316,8 @@ abstract class HoodieBaseRelation(val sqlContext: 
SQLContext,
     //   (!) Please note, however, that it's critical to avoid _reordering_ of 
the requested columns as this
     //       will break the upstream projection
     val targetColumns: Array[String] = appendMandatoryColumns(requiredColumns)
-    // NOTE: We explicitly fallback to default table's Avro schema to make 
sure we avoid unnecessary Catalyst > Avro
-    //       schema conversion, which is lossy in nature (for ex, it doesn't 
preserve original Avro type-names) and
-    //       could have an effect on subsequent de-/serializing records in 
some exotic scenarios (when Avro unions
-    //       w/ more than 2 types are involved)
-    val sourceSchema = prunedDataSchema.map(s => convertToHoodieSchema(s, 
tableName)).getOrElse(tableSchema)
     val (requiredProjectedSchema, requiredStructSchema, 
requiredInternalSchema) =
-      projectSchema(Either.cond(internalSchemaOpt.isDefined, 
internalSchemaOpt.get, sourceSchema), targetColumns)
+      projectSchema(Either.cond(internalSchemaOpt.isDefined, 
internalSchemaOpt.get, tableSchema), targetColumns)
 
     val filterExpressions = convertToExpressions(filters)
     val (partitionFilters, dataFilters) = 
filterExpressions.partition(isPartitionPredicate)
@@ -509,16 +465,6 @@ abstract class HoodieBaseRelation(val sqlContext: 
SQLContext,
     }
   }
 
-  /**
-   * Hook for Spark's Optimizer to update expected relation schema after 
pruning
-   *
-   * NOTE: Only limited number of optimizations in respect to schema pruning 
could be performed
-   *       internally w/in the relation itself w/o consideration for how the 
relation output is used.
-   *       Therefore more advanced optimizations (like 
[[NestedSchemaPruning]]) have to be carried out
-   *       by Spark's Optimizer holistically evaluating Spark's [[LogicalPlan]]
-   */
-  def updatePrunedDataSchema(prunedSchema: StructType): Relation
-
   protected def createBaseFileReaders(tableSchema: HoodieTableSchema,
                                       requiredSchema: HoodieTableSchema,
                                       requestedColumns: Array[String],
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieHadoopFsRelationFactory.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieHadoopFsRelationFactory.scala
index c35d57bb101a..7e475dc9971c 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieHadoopFsRelationFactory.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieHadoopFsRelationFactory.scala
@@ -335,7 +335,7 @@ class 
HoodieMergeOnReadIncrementalHadoopFsRelationFactoryV2(override val sqlCont
                                                             isBootstrap: 
Boolean,
                                                             rangeType: 
RangeType = RangeType.OPEN_CLOSED)
   extends HoodieMergeOnReadIncrementalHadoopFsRelationFactory(sqlContext, 
metaClient, options, schemaSpec, isBootstrap,
-    MergeOnReadIncrementalRelationV2(sqlContext, options, metaClient, 
schemaSpec, None, rangeType))
+    MergeOnReadIncrementalRelationV2(sqlContext, options, metaClient, 
schemaSpec, rangeType))
 
 class HoodieMergeOnReadCDCHadoopFsRelationFactory(override val sqlContext: 
SQLContext,
                                                   override val metaClient: 
HoodieTableMetaClient,
@@ -435,7 +435,7 @@ class 
HoodieCopyOnWriteIncrementalHadoopFsRelationFactoryV2(override val sqlCont
                                                             isBootstrap: 
Boolean,
                                                             rangeType: 
RangeType = RangeType.OPEN_CLOSED)
   extends HoodieCopyOnWriteIncrementalHadoopFsRelationFactory(sqlContext, 
metaClient, options, schemaSpec, isBootstrap,
-    MergeOnReadIncrementalRelationV2(sqlContext, options, metaClient, 
schemaSpec, None, rangeType))
+    MergeOnReadIncrementalRelationV2(sqlContext, options, metaClient, 
schemaSpec, rangeType))
 
 class HoodieCopyOnWriteCDCHadoopFsRelationFactory(override val sqlContext: 
SQLContext,
                                                   override val metaClient: 
HoodieTableMetaClient,
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
index e0fc7ceb6824..fabdfdadfe3c 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
@@ -48,16 +48,10 @@ import scala.collection.immutable
 case class MergeOnReadIncrementalRelationV1(override val sqlContext: 
SQLContext,
                                             override val optParams: 
Map[String, String],
                                             override val metaClient: 
HoodieTableMetaClient,
-                                            private val userSchema: 
Option[StructType],
-                                            private val prunedDataSchema: 
Option[StructType] = None)
-  extends BaseMergeOnReadSnapshotRelation(sqlContext, optParams, metaClient, 
userSchema, prunedDataSchema)
+                                            private val userSchema: 
Option[StructType])
+  extends BaseMergeOnReadSnapshotRelation(sqlContext, optParams, metaClient, 
userSchema)
     with HoodieIncrementalRelationV1Trait with MergeOnReadIncrementalRelation {
 
-  override type Relation = MergeOnReadIncrementalRelationV1
-
-  override def updatePrunedDataSchema(prunedSchema: StructType): Relation =
-    this.copy(prunedDataSchema = Some(prunedSchema))
-
   override protected def timeline: HoodieTimeline = {
     if (fullTableScan) {
       handleHollowCommitIfNeeded(metaClient.getCommitsAndCompactionTimeline, 
metaClient, hollowCommitHandling)
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
index aea594d9157c..b465e38623a1 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
@@ -51,16 +51,10 @@ case class MergeOnReadIncrementalRelationV2(override val 
sqlContext: SQLContext,
                                             override val optParams: 
Map[String, String],
                                             override val metaClient: 
HoodieTableMetaClient,
                                             private val userSchema: 
Option[StructType],
-                                            private val prunedDataSchema: 
Option[StructType] = None,
                                             override val rangeType: RangeType 
= RangeType.OPEN_CLOSED)
-  extends BaseMergeOnReadSnapshotRelation(sqlContext, optParams, metaClient, 
userSchema, prunedDataSchema)
+  extends BaseMergeOnReadSnapshotRelation(sqlContext, optParams, metaClient, 
userSchema)
     with HoodieIncrementalRelationV2Trait with MergeOnReadIncrementalRelation {
 
-  override type Relation = MergeOnReadIncrementalRelationV2
-
-  override def updatePrunedDataSchema(prunedSchema: StructType): Relation =
-    this.copy(prunedDataSchema = Some(prunedSchema))
-
   override protected def timeline: HoodieTimeline = {
     if (fullTableScan) {
       metaClient.getCommitsAndCompactionTimeline
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadSnapshotRelation.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadSnapshotRelation.scala
index 66ca8c8d6a25..aa96512ca18f 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadSnapshotRelation.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadSnapshotRelation.scala
@@ -19,8 +19,8 @@
 package org.apache.hudi
 
 import org.apache.hudi.HoodieConversionUtils.toScalaOption
-import org.apache.hudi.MergeOnReadSnapshotRelation.{createPartitionedFile, 
isProjectionCompatible}
-import org.apache.hudi.common.model.{FileSlice, HoodieLogFile, 
OverwriteWithLatestAvroPayload}
+import org.apache.hudi.MergeOnReadSnapshotRelation.createPartitionedFile
+import org.apache.hudi.common.model.{FileSlice, HoodieLogFile}
 import org.apache.hudi.common.table.HoodieTableMetaClient
 import org.apache.hudi.storage.StoragePath
 
@@ -42,14 +42,8 @@ case class HoodieMergeOnReadFileSplit(dataFile: 
Option[PartitionedFile],
 case class MergeOnReadSnapshotRelation(override val sqlContext: SQLContext,
                                        override val optParams: Map[String, 
String],
                                        override val metaClient: 
HoodieTableMetaClient,
-                                       private val userSchema: 
Option[StructType],
-                                       private val prunedDataSchema: 
Option[StructType] = None)
-  extends BaseMergeOnReadSnapshotRelation(sqlContext, optParams, metaClient, 
userSchema, prunedDataSchema) {
-
-  override type Relation = MergeOnReadSnapshotRelation
-
-  override def updatePrunedDataSchema(prunedSchema: StructType): Relation =
-    this.copy(prunedDataSchema = Some(prunedSchema))
+                                       private val userSchema: 
Option[StructType])
+  extends BaseMergeOnReadSnapshotRelation(sqlContext, optParams, metaClient, 
userSchema) {
 
   override protected def shouldIncludeLogFiles(): Boolean = {
     true
@@ -67,9 +61,8 @@ case class MergeOnReadSnapshotRelation(override val 
sqlContext: SQLContext,
 abstract class BaseMergeOnReadSnapshotRelation(sqlContext: SQLContext,
                                                optParams: Map[String, String],
                                                metaClient: 
HoodieTableMetaClient,
-                                               userSchema: Option[StructType],
-                                               prunedDataSchema: 
Option[StructType])
-  extends HoodieBaseRelation(sqlContext, metaClient, optParams, userSchema, 
prunedDataSchema) {
+                                               userSchema: Option[StructType])
+  extends HoodieBaseRelation(sqlContext, metaClient, optParams, userSchema) {
 
   override type FileSplit = HoodieMergeOnReadFileSplit
 
@@ -97,12 +90,6 @@ abstract class BaseMergeOnReadSnapshotRelation(sqlContext: 
SQLContext,
   protected val mergeType: String = 
optParams.getOrElse(DataSourceReadOptions.REALTIME_MERGE.key,
     DataSourceReadOptions.REALTIME_MERGE.defaultValue)
 
-  /**
-   * Determines whether relation's schema could be pruned by Spark's Optimizer
-   */
-  override def canPruneRelationSchema: Boolean =
-    super.canPruneRelationSchema && isProjectionCompatible(tableState)
-
   protected override def composeRDD(fileSplits: 
Seq[HoodieMergeOnReadFileSplit],
                                     tableSchema: HoodieTableSchema,
                                     requiredSchema: HoodieTableSchema,
@@ -152,21 +139,6 @@ abstract class BaseMergeOnReadSnapshotRelation(sqlContext: 
SQLContext,
 
 object MergeOnReadSnapshotRelation extends SparkAdapterSupport {
 
-  /**
-   * List of [[HoodieRecordPayload]] classes capable of merging projected 
records:
-   * in some cases, when for example, user is only interested in a handful of 
columns rather
-   * than the full row we will be able to optimize data throughput by only 
fetching the required
-   * columns. However, to properly fulfil MOR semantic particular 
[[HoodieRecordPayload]] in
-   * question should be able to merge records based on just such projected 
representation (including
-   * columns required for merging, such as primary-key, pre-combine key, etc)
-   */
-  private val projectionCompatiblePayloadClasses: Set[String] = Seq(
-    classOf[OverwriteWithLatestAvroPayload]
-  ).map(_.getName).toSet
-
-  def isProjectionCompatible(tableState: HoodieTableState): Boolean =
-    
projectionCompatiblePayloadClasses.contains(tableState.recordPayloadClassName)
-
   def createPartitionedFile(partitionValues: InternalRow,
                             filePath: StoragePath,
                             start: Long,
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystPlanUtils.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystPlanUtils.scala
index c8ceb713a6a3..6b8644519d2a 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystPlanUtils.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystPlanUtils.scala
@@ -22,7 +22,7 @@ import org.apache.hudi.SparkAdapterSupport
 import org.apache.spark.sql.catalyst.TableIdentifier
 import org.apache.spark.sql.catalyst.analysis.{ResolvedTable, 
TableOutputResolver}
 import org.apache.spark.sql.catalyst.catalog.CatalogStorageFormat
-import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeSet, 
Expression, ProjectionOverSchema}
+import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}
 import org.apache.spark.sql.catalyst.plans.JoinType
 import org.apache.spark.sql.catalyst.plans.logical.{CreateIndex, DropIndex, 
HoodieShowIndexes, InsertIntoStatement, Join, JoinHint, LogicalPlan, 
RefreshIndex}
 import org.apache.spark.sql.connector.catalog.{Identifier, Table, TableCatalog}
@@ -30,16 +30,9 @@ import org.apache.spark.sql.execution.{ExtendedMode, 
SimpleMode}
 import org.apache.spark.sql.execution.command.{CreateTableLikeCommand, 
ExplainCommand, RepairTableCommand}
 import org.apache.spark.sql.execution.datasources.LogicalRelation
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.StructType
 
 trait BaseHoodieCatalystPlanUtils extends HoodieCatalystPlansUtils {
 
-  /**
-   * Instantiates [[ProjectionOverSchema]] utility
-   */
-  def projectOverSchema(schema: StructType, output: AttributeSet): 
ProjectionOverSchema =
-    ProjectionOverSchema(schema, output)
-
   /**
    * Un-applies [[ResolvedTable]] that had its signature changed in Spark 3.2
    */
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/HoodieNestedSchemaPruning.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/HoodieNestedSchemaPruning.scala
deleted file mode 100644
index 42e59eea5c51..000000000000
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/HoodieNestedSchemaPruning.scala
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * 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.spark.sql.execution.datasources
-
-import org.apache.hudi.{HoodieBaseRelation, SparkAdapterSupport}
-
-import org.apache.spark.sql.BaseHoodieCatalystPlanUtils
-import org.apache.spark.sql.catalyst.expressions.{And, AttributeReference, 
AttributeSet, Expression, NamedExpression, ProjectionOverSchema}
-import org.apache.spark.sql.catalyst.planning.PhysicalOperation
-import org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan, 
Project}
-import org.apache.spark.sql.catalyst.rules.Rule
-import org.apache.spark.sql.sources.BaseRelation
-import org.apache.spark.sql.types.{ArrayType, DataType, MapType, StructType}
-import org.apache.spark.sql.util.SchemaUtils.restoreOriginalOutputNames
-
-/**
- * Prunes unnecessary physical columns given a [[PhysicalOperation]] over a 
data source relation.
- * By "physical column", we mean a column as defined in the data source format 
like Parquet format
- * or ORC format. For example, in Spark SQL, a root-level Parquet column 
corresponds to a SQL
- * column, and a nested Parquet column corresponds to a [[StructField]].
- *
- * NOTE: This class is borrowed from Spark 3.2.1, with modifications adapting 
it to handle [[HoodieBaseRelation]],
- *       instead of [[HadoopFsRelation]]
- */
-class HoodieNestedSchemaPruning extends Rule[LogicalPlan] {
-  import org.apache.spark.sql.catalyst.expressions.SchemaPruning._
-
-  override def apply(plan: LogicalPlan): LogicalPlan =
-    if (conf.nestedSchemaPruningEnabled) {
-      apply0(plan)
-    } else {
-      plan
-    }
-
-  private def apply0(plan: LogicalPlan): LogicalPlan =
-    plan transformDown {
-      // NOTE: The relation is matched by type rather than by destructuring 
[[LogicalRelation]],
-      //       since the arity of its unapply differs across the Spark 
versions this module
-      //       compiles against. This is modified to accommodate for Hudi's 
custom relations,
-      //       given that original [[NestedSchemaPruning]] rule is tightly 
coupled w/
-      //       [[HadoopFsRelation]]
-      // TODO generalize to any file-based relation
-      case op @ PhysicalOperation(projects, filters, l: LogicalRelation) =>
-        l.relation match {
-          case relation: HoodieBaseRelation if relation.canPruneRelationSchema 
=>
-            prunePhysicalColumns(l.output, projects, filters, 
relation.dataSchema,
-              prunedDataSchema => {
-                val prunedRelation =
-                  relation.updatePrunedDataSchema(prunedSchema = 
prunedDataSchema)
-                buildPrunedRelation(l, prunedRelation)
-              }).getOrElse(op)
-          case _ => op
-        }
-    }
-
-  // Prune the given output to make it consistent with `requiredSchema`.
-  private def getPrunedOutput(output: Seq[AttributeReference],
-                              requiredSchema: StructType): 
Seq[AttributeReference] = {
-    // We need to replace the expression ids of the pruned relation output 
attributes
-    // with the expression ids of the original relation output attributes so 
that
-    // references to the original relation's output are not broken
-    val outputIdMap = output.map(att => (att.name, att.exprId)).toMap
-    // NOTE: The attributes are constructed inline (equivalent to 
StructType#toAttributes before
-    //       Spark 3.5 and DataTypeUtils#toAttributes since, see SPARK-44353) 
so that this code
-    //       compiles against every supported Spark version
-    requiredSchema
-      .map(f => AttributeReference(f.name, f.dataType, f.nullable, 
f.metadata)())
-      .map {
-        case att if outputIdMap.contains(att.name) =>
-          att.withExprId(outputIdMap(att.name))
-        case att => att
-      }
-  }
-
-  /**
-   * This method returns optional logical plan. `None` is returned if no 
nested field is required or
-   * all nested fields are required.
-   */
-  private def prunePhysicalColumns(output: Seq[AttributeReference],
-                                   projects: Seq[NamedExpression],
-                                   filters: Seq[Expression],
-                                   dataSchema: StructType,
-                                   outputRelationBuilder: StructType => 
LogicalRelation): Option[LogicalPlan] = {
-    val (normalizedProjects, normalizedFilters) =
-      normalizeAttributeRefNames(output, projects, filters)
-    val requestedRootFields = identifyRootFields(normalizedProjects, 
normalizedFilters)
-
-    // If requestedRootFields includes a nested field, continue. Otherwise,
-    // return op
-    if (requestedRootFields.exists { root: RootField => !root.derivedFromAtt 
}) {
-      val prunedDataSchema = pruneSchema(dataSchema, requestedRootFields)
-
-      // If the data schema is different from the pruned data schema, 
continue. Otherwise,
-      // return op. We effect this comparison by counting the number of "leaf" 
fields in
-      // each schemata, assuming the fields in prunedDataSchema are a subset 
of the fields
-      // in dataSchema.
-      if (countLeaves(dataSchema) > countLeaves(prunedDataSchema)) {
-        val planUtils = 
SparkAdapterSupport.sparkAdapter.getCatalystPlanUtils.asInstanceOf[BaseHoodieCatalystPlanUtils]
-
-        val prunedRelation = outputRelationBuilder(prunedDataSchema)
-        val projectionOverSchema = 
planUtils.projectOverSchema(prunedDataSchema, AttributeSet(output))
-
-        Some(buildNewProjection(projects, normalizedProjects, 
normalizedFilters,
-          prunedRelation, projectionOverSchema))
-      } else {
-        None
-      }
-    } else {
-      None
-    }
-  }
-
-  /**
-   * Normalizes the names of the attribute references in the given projects 
and filters to reflect
-   * the names in the given logical relation. This makes it possible to 
compare attributes and
-   * fields by name. Returns a tuple with the normalized projects and filters, 
respectively.
-   */
-  private def normalizeAttributeRefNames(output: Seq[AttributeReference],
-                                         projects: Seq[NamedExpression],
-                                         filters: Seq[Expression]): 
(Seq[NamedExpression], Seq[Expression]) = {
-    val normalizedAttNameMap = output.map(att => (att.exprId, att.name)).toMap
-    val normalizedProjects = projects.map(_.transform {
-      case att: AttributeReference if 
normalizedAttNameMap.contains(att.exprId) =>
-        att.withName(normalizedAttNameMap(att.exprId))
-    }).map { case expr: NamedExpression => expr }
-    val normalizedFilters = filters.map(_.transform {
-      case att: AttributeReference if 
normalizedAttNameMap.contains(att.exprId) =>
-        att.withName(normalizedAttNameMap(att.exprId))
-    })
-    (normalizedProjects, normalizedFilters)
-  }
-
-  /**
-   * Builds the new output [[Project]] Spark SQL operator that has the 
`leafNode`.
-   */
-  private def buildNewProjection(projects: Seq[NamedExpression],
-                                 normalizedProjects: Seq[NamedExpression],
-                                 filters: Seq[Expression],
-                                 prunedRelation: LogicalRelation,
-                                 projectionOverSchema: ProjectionOverSchema): 
Project = {
-    // Construct a new target for our projection by rewriting and
-    // including the original filters where available
-    val projectionChild =
-      if (filters.nonEmpty) {
-        val projectedFilters = filters.map(_.transformDown {
-          case projectionOverSchema(expr) => expr
-        })
-        val newFilterCondition = projectedFilters.reduce(And)
-        Filter(newFilterCondition, prunedRelation)
-      } else {
-        prunedRelation
-      }
-
-    // Construct the new projections of our Project by
-    // rewriting the original projections
-    val newProjects = normalizedProjects.map(_.transformDown {
-      case projectionOverSchema(expr) => expr
-    }).map { case expr: NamedExpression => expr }
-
-    if (log.isDebugEnabled) {
-      logDebug(s"New 
projects:\n${newProjects.map(_.treeString).mkString("\n")}")
-    }
-
-    Project(restoreOriginalOutputNames(newProjects, projects.map(_.name)), 
projectionChild)
-  }
-
-  /**
-   * Builds a pruned logical relation from the output of the output relation 
and the schema of the
-   * pruned base relation.
-   */
-  private def buildPrunedRelation(outputRelation: LogicalRelation,
-                                  prunedBaseRelation: BaseRelation): 
LogicalRelation = {
-    val prunedOutput = getPrunedOutput(outputRelation.output, 
prunedBaseRelation.schema)
-    outputRelation.copy(relation = prunedBaseRelation, output = prunedOutput)
-  }
-
-  /**
-   * Counts the "leaf" fields of the given dataType. Informally, this is the
-   * number of fields of non-complex data type in the tree representation of
-   * [[DataType]].
-   */
-  private def countLeaves(dataType: DataType): Int = {
-    dataType match {
-      case array: ArrayType => countLeaves(array.elementType)
-      case map: MapType => countLeaves(map.keyType) + 
countLeaves(map.valueType)
-      case struct: StructType =>
-        struct.map(field => countLeaves(field.dataType)).sum
-      case _ => 1
-    }
-  }
-}
diff --git 
a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/analysis/HoodieAnalysis.scala
 
b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/analysis/HoodieAnalysis.scala
index c6eb3c3122ee..f9d99cbc0602 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/analysis/HoodieAnalysis.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/analysis/HoodieAnalysis.scala
@@ -30,7 +30,7 @@ import 
org.apache.spark.sql.catalyst.optimizer.ReplaceExpressions
 import org.apache.spark.sql.catalyst.plans.logical._
 import org.apache.spark.sql.catalyst.rules.Rule
 import org.apache.spark.sql.execution.command._
-import org.apache.spark.sql.execution.datasources.{CreateTable, 
HoodieNestedSchemaPruning, LogicalRelation}
+import org.apache.spark.sql.execution.datasources.{CreateTable, 
LogicalRelation}
 import org.apache.spark.sql.hudi.HoodieSqlCommonUtils.{isMetaField, 
removeMetaFields}
 import org.apache.spark.sql.hudi.analysis.HoodieAnalysis.{sparkAdapter, 
MatchCreateIndex, MatchCreateTableLike, MatchDropIndex, 
MatchInsertIntoStatement, MatchMergeIntoTable, MatchRefreshIndex, 
MatchShowIndexes, ResolvesToHudiTable}
 import org.apache.spark.sql.hudi.blob.ReadBlobRule
@@ -142,9 +142,6 @@ object HoodieAnalysis extends SparkAdapterSupport {
       // Default rules
     )
 
-    val nestedSchemaPruningRule = new HoodieNestedSchemaPruning
-    rules += (_ => nestedSchemaPruningRule)
-
     // NOTE: [[HoodiePruneFileSourcePartitions]] is a replica in kind to 
Spark's
     //       [[PruneFileSourcePartitions]] and as such should be executed at 
the same stage.
     //       However, currently Spark doesn't allow [[SparkSessionExtensions]] 
to inject into
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala
index 7bdbef9cd008..be8cf2eda5a7 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestNestedSchemaPruningOptimization.scala
@@ -17,6 +17,7 @@
 
 package org.apache.spark.sql.hudi.common
 
+import org.apache.hudi.common.table.read.CustomPayloadForTesting
 import org.apache.hudi.config.HoodieWriteConfig
 
 import org.apache.spark.sql.DataFrame
@@ -25,6 +26,11 @@ import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types.{ArrayType, IntegerType, MapType, 
StringType, StructField, StructType}
 import org.junit.jupiter.api.Assertions.assertEquals
 
+/**
+ * Verifies nested schema pruning on Hudi tables. The pruning itself is 
performed by Spark's
+ * built-in SchemaPruning rule: the file-group-reader based file format 
extends ParquetFileFormat,
+ * so Hudi does not ship a nested schema pruning rule of its own.
+ */
 class TestNestedSchemaPruningOptimization extends HoodieSparkSqlTestBase {
 
   // NOTE: We disable WCE once for the whole suite so the executed plans stay 
a plain Project over the
@@ -58,26 +64,34 @@ class TestNestedSchemaPruningOptimization extends 
HoodieSparkSqlTestBase {
     }
   }
 
-  test("Test nested schema pruning with DefaultHoodieRecordPayload") {
+  test("Test nested schema pruning with a projection-incompatible custom 
payload") {
     withTempDir { tmp =>
       val tableName = generateTableName
       val tablePath = s"${tmp.getCanonicalPath}/$tableName"
 
-      // NOTE: On the file-group-reader based read path the payload class does 
not affect nested
-      //       schema pruning, so the read schema is pruned the same way as 
with the default payload
+      // NOTE: A payload class outside the well-known set puts the table in 
CUSTOM merge mode, whose
+      //       merger is not projection compatible, so the file group reader 
merges on the full
+      //       table schema internally 
(FileGroupReaderSchemaHandler#generateRequiredSchema) and
+      //       projects the merged rows back down to the pruned read schema 
afterwards
       createTableWithNestedStructSchema("mor", tableName, tablePath,
-        Map(HoodieWriteConfig.WRITE_PAYLOAD_CLASS_NAME.key -> 
"org.apache.hudi.common.model.DefaultHoodieRecordPayload"))
+        Map(HoodieWriteConfig.WRITE_PAYLOAD_CLASS_NAME.key -> 
classOf[CustomPayloadForTesting].getName),
+        populateMetaFields = true)
+
+      // The update writes a log file, so the pruned reads below actually 
merge through that gate
+      spark.sql(s"UPDATE $tableName SET ts = 123457 WHERE id = 1")
 
       val selectDF = spark.sql(s"SELECT id, item.name FROM $tableName")
 
+      // Spark still prunes the scan schema; the full-schema requirement is 
internal to the reader
       val expectedSchema = StructType(Seq(
         StructField("id", IntegerType, nullable = true),
         StructField("item", StructType(Seq(StructField("name", StringType, 
nullable = false))), nullable = true)
       ))
-
       assertPrunedReadSchema(selectDF, tableName, expectedSchema)
 
       checkAnswer(s"SELECT id, item.name FROM $tableName")(Seq(1, "a1"))
+      // A second query pruned to different leaves still observes the 
correctly merged values
+      checkAnswer(s"SELECT id, item.price, ts FROM $tableName")(Seq(1, 10, 
123457))
     }
   }
 

Reply via email to