dwsmith1983 commented on code in PR #5365:
URL: https://github.com/apache/datafusion-comet/pull/5365#discussion_r3920753757


##########
spark/src/main/scala/org/apache/comet/serde/operator/CometNativeScan.scala:
##########
@@ -125,166 +150,233 @@ object CometNativeScan extends 
CometOperatorSerde[CometScanExec] with CometTypeS
       scan: CometScanExec,
       builder: Operator.Builder,
       childOp: OperatorOuterClass.Operator*): 
Option[OperatorOuterClass.Operator] = {
-    val nativeScanBuilder = OperatorOuterClass.NativeScan.newBuilder()
+    // Extract object store options from first file (S3 configs apply to all 
files in scan).
+    // Use selectedPartitions (static) instead of getFilePartitions() because 
at planning time
+    // DPP subqueries haven't been resolved yet. Object store options don't 
depend on DPP.
+    val firstFileUri = scan.selectedPartitions
+      .flatMap(_.files.headOption)
+      .headOption
+      .map(_.getPath.toUri)
+
+    // Collect S3/cloud storage configurations
+    val hadoopConf = scan.relation.sparkSession.sessionState
+      .newHadoopConfWithOptions(scan.relation.options)
+
+    buildNativeScanCommon(
+      source = scan.simpleStringWithNodeId(),
+      output = scan.output,
+      requiredSchema = scan.requiredSchema,
+      dataSchema = scan.relation.dataSchema,
+      partitionSchema = scan.relation.partitionSchema,
+      fileConstantMetadataColumns = scan.wrapped.fileConstantMetadataColumns,
+      dataFilters = scan.supportedDataFilters,
+      firstFileUri = firstFileUri,
+      hadoopConf = hadoopConf,
+      conf = scan.conf) match {
+      case Some(commonBuilder) =>
+        // Sink operators don't have children
+        builder.clearChildren()
+        val nativeScanBuilder = OperatorOuterClass.NativeScan.newBuilder()
+        // Set common data in NativeScan (file_partition will be populated at 
execution time)
+        nativeScanBuilder.setCommon(commonBuilder.build())
+        Some(builder.setNativeScan(nativeScanBuilder).build())
+      case None =>
+        // There are unsupported scan type
+        withFallbackReason(
+          scan,
+          s"unsupported Comet operator: ${scan.nodeName}, due to unsupported 
data types above")
+        None
+    }
+  }
+
+  /**
+   * Build the `NativeScanCommon` proto shared by the core parquet scan and 
contrib scans that
+   * delegate to the same native parquet machinery (e.g. a Delta scan contrib, 
which passes
+   * physical-name schemas under column mapping). Returns `None` when an 
output data type cannot
+   * be serialized; the caller is responsible for tagging a fallback reason.
+   *
+   * Visibility note: `private[comet]` means a contrib caller must live under 
an
+   * `org.apache.comet.*` package (the same constraint `PlanDataInjector` 
implementers have).
+   */
+  private[comet] def buildNativeScanCommon(
+      source: String,
+      output: Seq[Attribute],
+      requiredSchema: StructType,
+      dataSchema: StructType,
+      partitionSchema: StructType,
+      fileConstantMetadataColumns: Seq[AttributeReference],
+      dataFilters: Seq[Expression],
+      firstFileUri: Option[URI],
+      hadoopConf: Configuration,
+      conf: SQLConf): Option[OperatorOuterClass.NativeScanCommon.Builder] = {
     val commonBuilder = OperatorOuterClass.NativeScanCommon.newBuilder()
 
     // Set source in common (used as part of injection key)
-    commonBuilder.setSource(scan.simpleStringWithNodeId())
+    commonBuilder.setSource(source)
 
-    val scanTypes = scan.output.flatten { attr =>
+    val scanTypes = output.flatten { attr =>
       serializeDataType(attr.dataType)
     }
 
-    if (scanTypes.length == scan.output.length) {
-      commonBuilder.addAllFields(scanTypes.asJava)
-
-      // Sink operators don't have children
-      builder.clearChildren()
-
-      if (scan.conf.getConf(SQLConf.PARQUET_FILTER_PUSHDOWN_ENABLED)) {
-        val dataFilters = new ListBuffer[Expr]()
-        for (filter <- scan.supportedDataFilters) {
-          exprToProto(filter, scan.output) match {
-            case Some(proto) => dataFilters += proto
-            case _ =>
-              logWarning(s"Unsupported data filter $filter")
-          }
+    if (scanTypes.length != output.length) {
+      // There are unsupported scan types
+      return None
+    }
+    commonBuilder.addAllFields(scanTypes.asJava)
+
+    if (conf.getConf(SQLConf.PARQUET_FILTER_PUSHDOWN_ENABLED)) {
+      val filterProtos = new ListBuffer[Expr]()
+      for (filter <- dataFilters) {
+        exprToProto(filter, output) match {
+          case Some(proto) => filterProtos += proto
+          case _ =>
+            logWarning(s"Unsupported data filter $filter")
         }
-        commonBuilder.addAllDataFilters(dataFilters.asJava)
-      }
-
-      val possibleDefaultValues = 
getExistenceDefaultValues(scan.requiredSchema)
-      if (possibleDefaultValues.exists(_ != null)) {
-        // Our schema has default values. Serialize two lists, one with the 
default values
-        // and another with the indexes in the schema so the native side can 
map missing
-        // columns to these default values.
-        val (defaultValues, indexes) = 
possibleDefaultValues.iterator.zipWithIndex
-          .filter { case (expr, _) => expr != null }
-          .map { case (expr, index) =>
-            // ResolveDefaultColumnsUtil.getExistenceDefaultValues has 
evaluated these
-            // expressions and they should now just be literals.
-            (Literal(expr), index.toLong.asInstanceOf[java.lang.Long])
-          }
-          .toList
-          .unzip
-        commonBuilder.addAllDefaultValues(
-          defaultValues.flatMap(exprToProto(_, scan.output)).asJava)
-        commonBuilder.addAllDefaultValuesIndexes(indexes.asJava)
       }
+      commonBuilder.addAllDataFilters(filterProtos.asJava)
+    }
 
-      // Extract object store options from first file (S3 configs apply to all 
files in scan).
-      // Use selectedPartitions (static) instead of getFilePartitions() 
because at planning time
-      // DPP subqueries haven't been resolved yet. Object store options don't 
depend on DPP.
-      val firstFileUri = scan.selectedPartitions
-        .flatMap(_.files.headOption)
-        .headOption
-        .map(_.getPath.toUri)
-
-      // Constant metadata columns (file_path, file_name, file_size, 
file_block_start,
-      // file_block_length, file_modification_time) are known before opening 
the file and
-      // constant for every row read from it, exactly like partition columns. 
Spark places
-      // them immediately after partition columns in `scan.output`
-      // (FileSourceStrategy.scala: readDataColumns ++ 
generatedMetadataColumns ++
-      // partitionColumns ++ constantMetadataColumns), so appending them after 
the real
-      // partition schema here keeps the two in lockstep.
-      val constantMetadataFields = 
scan.wrapped.fileConstantMetadataColumns.map(attr =>
-        StructField(s"$constantMetadataFieldPrefix${attr.name}", 
attr.dataType, attr.nullable))
-      val partitionSchemaFields = scan.relation.partitionSchema.fields.toSeq ++
-        constantMetadataFields
-      val partitionSchema = schema2Proto(partitionSchemaFields)
-      val requiredSchema = schema2Proto(scan.requiredSchema)
-
-      // Spark's required schema can prune a Variant column, including a 
Variant nested under an
-      // unrequested struct. The complete relation schema still contains that 
unsupported type,
-      // and serializing it would throw even though the native reader never 
needs those bytes.
-      // Keep ordinary fields unchanged and replace a requested 
Variant-bearing root with its
-      // already-validated, pruned required field. A requested actual Variant 
never reaches this
-      // point because CometScanRule keeps those scans on Spark.
-      val nativeDataSchema = 
StructType(scan.relation.dataSchema.fields.flatMap { field =>
-        if (containsVariantType(field.dataType)) {
-          scan.requiredSchema.fields.find(requiredField =>
-            scan.conf.resolver(requiredField.name, field.name))
-        } else {
-          Some(field)
-        }
-      })
-      val dataSchema = schema2Proto(nativeDataSchema)
-
-      val dataSchemaIndexes = scan.requiredSchema.map(field => {
-        nativeDataSchema.fieldIndex(field.name)
-      })
-      val partitionSchemaIndexes = nativeDataSchema.fields.length until
-        (nativeDataSchema.length + partitionSchemaFields.length)
-
-      val projectionVector = (dataSchemaIndexes ++ 
partitionSchemaIndexes).map(idx =>
-        idx.toLong.asInstanceOf[java.lang.Long])
-
-      commonBuilder.addAllProjectionVector(projectionVector.asJava)
-
-      // In `CometScanRule`, we ensure partitionSchema (including constant 
metadata columns)
-      // is supported.
-      assert(partitionSchema.length == partitionSchemaFields.length)
-
-      commonBuilder.addAllDataSchema(dataSchema.asJava)
-      commonBuilder.addAllRequiredSchema(requiredSchema.asJava)
-      commonBuilder.addAllPartitionSchema(partitionSchema.asJava)
-      
commonBuilder.setSessionTimezone(scan.conf.getConfString("spark.sql.session.timeZone"))
-      
commonBuilder.setCaseSensitive(scan.conf.getConf[Boolean](SQLConf.CASE_SENSITIVE))
-
-      // SPARK-53535 (Spark 4.1+): when reading a struct whose requested 
fields are all
-      // missing in the Parquet file, the new default preserves the parent 
struct's
-      // nullness from the file (so non-null parents materialize as a struct 
of all-null
-      // fields). Pre-4.1 Spark hardcodes the legacy behavior (whole struct 
null), which
-      // matches the Comet default we use as fallback.
-      val returnNullStructConfKey =
-        "spark.sql.legacy.parquet.returnNullStructIfAllFieldsMissing"
-      val returnNullStructDefault = if (isSpark41Plus) "false" else "true"
-      commonBuilder.setReturnNullStructIfAllFieldsMissing(
-        scan.conf.getConfString(returnNullStructConfKey, 
returnNullStructDefault).toBoolean)
-
-      // Field-ID matching: only ask the native side to do extra work when the 
conf is on AND
-      // the requested schema actually carries IDs. Spark's ParquetReadSupport 
applies the same
-      // gate before invoking matchIdField.
-      val useFieldId =
-        scan.conf.getConf(SQLConf.PARQUET_FIELD_ID_READ_ENABLED) &&
-          ParquetUtils.hasFieldIds(scan.requiredSchema)
-      commonBuilder.setUseFieldId(useFieldId)
-      commonBuilder.setIgnoreMissingFieldId(
-        scan.conf.getConf(SQLConf.IGNORE_MISSING_PARQUET_FIELD_ID))
-
-      
commonBuilder.setAllowTypePromotion(CometConf.COMET_SCHEMA_EVOLUTION_ENABLED)
-      
commonBuilder.setAllowTimestampLtzToNtz(CometConf.COMET_ALLOW_TIMESTAMP_LTZ_AS_NTZ)
-
-      // Collect S3/cloud storage configurations
-      val hadoopConf = scan.relation.sparkSession.sessionState
-        .newHadoopConfWithOptions(scan.relation.options)
-
-      
commonBuilder.setEncryptionEnabled(CometParquetUtils.encryptionEnabled(hadoopConf))
-
-      firstFileUri.foreach { uri =>
-        val objectStoreOptions =
-          NativeConfig.extractObjectStoreOptions(hadoopConf, uri)
-        objectStoreOptions.foreach { case (key, value) =>
-          commonBuilder.putObjectStoreOptions(key, value)
+    val possibleDefaultValues = getExistenceDefaultValues(requiredSchema)
+    if (possibleDefaultValues.exists(_ != null)) {
+      // Our schema has default values. Serialize two lists, one with the 
default values
+      // and another with the indexes in the schema so the native side can map 
missing
+      // columns to these default values.
+      val (defaultValues, indexes) = 
possibleDefaultValues.iterator.zipWithIndex
+        .filter { case (expr, _) => expr != null }
+        .map { case (expr, index) =>
+          // ResolveDefaultColumnsUtil.getExistenceDefaultValues has evaluated 
these
+          // expressions and they should now just be literals.
+          (Literal(expr), index.toLong.asInstanceOf[java.lang.Long])
         }
+        .toList
+        .unzip
+      commonBuilder.addAllDefaultValues(defaultValues.flatMap(exprToProto(_, 
output)).asJava)
+      commonBuilder.addAllDefaultValuesIndexes(indexes.asJava)
+    }
+
+    // Constant metadata columns (file_path, file_name, file_size, 
file_block_start,
+    // file_block_length, file_modification_time) are known before opening the 
file and
+    // constant for every row read from it, exactly like partition columns. 
Spark places
+    // them immediately after partition columns in the scan output
+    // (FileSourceStrategy.scala: readDataColumns ++ generatedMetadataColumns 
++
+    // partitionColumns ++ constantMetadataColumns), so appending them after 
the real
+    // partition schema here keeps the two in lockstep.
+    val constantMetadataFields = uniqueConstantMetadataFields(
+      fileConstantMetadataColumns,
+      dataSchema.fields.map(_.name).toSet ++ 
partitionSchema.fields.map(_.name).toSet)
+    val partitionSchemaFields = partitionSchema.fields.toSeq ++ 
constantMetadataFields
+    val partitionSchemaProto = schema2Proto(partitionSchemaFields)
+    val requiredSchemaProto = schema2Proto(requiredSchema)
+
+    // Spark's required schema can prune a Variant column, including a Variant 
nested under an
+    // unrequested struct. The complete relation schema still contains that 
unsupported type,
+    // and serializing it would throw even though the native reader never 
needs those bytes.
+    // Keep ordinary fields unchanged and replace a requested Variant-bearing 
root with its
+    // already-validated, pruned required field. A requested actual Variant 
never reaches this
+    // point because CometScanRule keeps those scans on Spark.
+    val nativeDataSchema = StructType(dataSchema.fields.flatMap { field =>
+      if (containsVariantType(field.dataType)) {
+        requiredSchema.fields.find(requiredField => 
conf.resolver(requiredField.name, field.name))
+      } else {
+        Some(field)
       }
+    })
+    val dataSchemaProto = schema2Proto(nativeDataSchema)
 
-      // Set common data in NativeScan (file_partition will be populated at 
execution time)
-      nativeScanBuilder.setCommon(commonBuilder.build())
+    val dataSchemaIndexes = requiredSchema.map(field => {
+      nativeDataSchema.fieldIndex(field.name)
+    })
+    val partitionSchemaIndexes = nativeDataSchema.fields.length until
+      (nativeDataSchema.length + partitionSchemaFields.length)
 
-      Some(builder.setNativeScan(nativeScanBuilder).build())
+    val projectionVector = (dataSchemaIndexes ++ 
partitionSchemaIndexes).map(idx =>
+      idx.toLong.asInstanceOf[java.lang.Long])
 
-    } else {
-      // There are unsupported scan type
-      withFallbackReason(
-        scan,
-        s"unsupported Comet operator: ${scan.nodeName}, due to unsupported 
data types above")
-      None
+    commonBuilder.addAllProjectionVector(projectionVector.asJava)
+
+    // In `CometScanRule`, we ensure partitionSchema (including constant 
metadata columns)
+    // is supported.
+    assert(partitionSchemaProto.length == partitionSchemaFields.length)
+
+    commonBuilder.addAllDataSchema(dataSchemaProto.asJava)
+    commonBuilder.addAllRequiredSchema(requiredSchemaProto.asJava)
+    commonBuilder.addAllPartitionSchema(partitionSchemaProto.asJava)
+
+    populateScanConfFlags(commonBuilder, requiredSchema, firstFileUri, 
hadoopConf, conf)
+
+    Some(commonBuilder)
+  }
+
+  /**
+   * Populate the configuration-derived flags of a `NativeScanCommon`: session 
timezone, case
+   * sensitivity, struct-nullness legacy flag, field-ID matching, type 
promotion, encryption, and
+   * object-store options. Shared with contrib scans that assemble their own 
schemas/projection
+   * (e.g. the Delta contrib's deletion-vector shape) so new flags added here 
reach them without
+   * drift.
+   */
+  private[comet] def populateScanConfFlags(
+      commonBuilder: OperatorOuterClass.NativeScanCommon.Builder,
+      requiredSchema: StructType,
+      firstFileUri: Option[URI],
+      hadoopConf: Configuration,
+      conf: SQLConf): Unit = {
+    
commonBuilder.setSessionTimezone(conf.getConfString("spark.sql.session.timeZone"))
+    val caseSensitive = conf.getConf[Boolean](SQLConf.CASE_SENSITIVE)
+    commonBuilder.setCaseSensitive(caseSensitive)
+    if (!caseSensitive) {
+      // Ship THIS JVM's case data so native's case-insensitive footer 
matching reproduces
+      // this JVM's `toLowerCase(Locale.ROOT)` exactly, whatever Unicode 
version it bundles.
+      JvmCaseTables.populate(commonBuilder)

Review Comment:
   Gone with the case tables: nothing rides in NativeScanCommon now, so plans 
are back to their interned size and QueryContextInternerSuite passes on the 
default configuration again with no pin. Good catch on the pin masking that, 
the suite is untouched from main on this branch.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to