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


##########
contrib/delta-spark/src/main/scala/org/apache/comet/contrib/delta/CometDeltaNativeScan.scala:
##########
@@ -0,0 +1,548 @@
+/*
+ * 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.comet.contrib.delta
+
+import scala.jdk.CollectionConverters._
+
+import org.apache.hadoop.fs.Path
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.catalyst.expressions.Literal
+import org.apache.spark.sql.comet.{CometScanExec, DeltaPlanDataInjector}
+import org.apache.spark.sql.delta.DeltaParquetFileFormat
+import org.apache.spark.sql.delta.RowIndexFilterType
+import org.apache.spark.sql.delta.actions.DeletionVectorDescriptor
+import org.apache.spark.sql.execution.{FileSourceScanExec, ScalarSubquery => 
ExecScalarSubquery}
+import org.apache.spark.sql.execution.datasources.{FilePartition, 
PartitionedFile}
+import org.apache.spark.sql.types.{ByteType, LongType, MetadataBuilder, 
StructField, StructType}
+
+import org.apache.comet.objectstore.NativeConfig
+import org.apache.comet.serde.OperatorOuterClass
+import org.apache.comet.serde.OperatorOuterClass.Operator
+import org.apache.comet.serde.QueryPlanSerde.{exprToProto, serializeDataType}
+import org.apache.comet.serde.operator.{literalToProto, partition2Proto, 
schema2Proto, CometNativeScan}
+import org.apache.comet.shims.ShimFileFormat
+
+/**
+ * Serde for the native Delta scan. Two shapes:
+ *   - Plain reads reuse core's `NativeScanCommon` builder wholesale.
+ *   - Deletion-vector reads: Delta's planner appends 
`__delta_internal_is_row_deleted` (tinyint)
+ *     and Spark's row-index temp column (bigint) to the read schema and 
filters on is_row_deleted
+ *     above the scan. The native reader applies the DV as a row selection, so 
both internal
+ *     columns are emitted as per-file constants (0), the parquet read schema 
is stripped to the
+ *     real data columns, and the DV descriptor ships per file for native to 
fetch and decode.
+ */
+object CometDeltaNativeScan
+    extends Logging
+    with org.apache.spark.sql.catalyst.expressions.PredicateHelper {
+
+  val IsRowDeletedColumn: String = 
DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME
+  val RowIndexColumn: String = ShimFileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME
+
+  private[delta] val internalColumnNames: Set[String] = 
Set(IsRowDeletedColumn, RowIndexColumn)
+
+  // Prefix for the internal columns' slots in the partition schema, mirroring 
core's
+  // _comet_metadata_ prefix rationale: DataFusion matches partition columns 
by name.
+  // [[allocateUniqueInternalFields]] additionally suffixes on collision with 
a real column.
+  private val deltaConstFieldPrefix = "_comet_delta_"
+
+  def isDvShape(scanExec: FileSourceScanExec): Boolean =
+    scanExec.requiredSchema.exists(f => internalColumnNames.contains(f.name))
+
+  private def deltaFormat(scanExec: FileSourceScanExec): 
DeltaParquetFileFormat =
+    scanExec.relation.fileFormat.asInstanceOf[DeltaParquetFileFormat]
+
+  private def columnMappingMode(scanExec: FileSourceScanExec): String =
+    deltaFormat(scanExec).metadata.columnMappingMode.name
+
+  /**
+   * Under column mapping, parquet files store physical column names (stable 
UUIDs / ids), so the
+   * schemas passed to the native parquet reader must be physical. Positions 
and structure are
+   * preserved, so output binding and projection are unaffected. The scan's 
internal DV columns
+   * are not part of the table schema and must be stripped before calling this.
+   */
+  private def toPhysical(scanExec: FileSourceScanExec, schema: StructType): 
StructType = {
+    val format = deltaFormat(scanExec)
+    if (format.metadata.columnMappingMode.name == "none") {
+      schema
+    } else {
+      // Name mode matches file columns by physical NAME. Strip the 
parquet.field.id metadata
+      // createPhysicalSchema also stamps: files written before the 
column-mapping upgrade have
+      // no field ids and would fail the reader's id expectations.
+      stripFieldIds(org.apache.spark.sql.delta.DeltaColumnMapping
+        .createPhysicalSchema(schema, format.metadata.schema, 
format.metadata.columnMappingMode))
+    }
+  }
+
+  private def stripFieldIds(schema: StructType): StructType = {
+    import org.apache.spark.sql.types._
+    def stripType(dt: DataType): DataType = dt match {
+      case s: StructType => stripFieldIds(s)
+      case a: ArrayType => a.copy(elementType = stripType(a.elementType))
+      case m: MapType =>
+        m.copy(keyType = stripType(m.keyType), valueType = 
stripType(m.valueType))
+      case other => other
+    }
+    StructType(schema.fields.map { f =>
+      val metadata = new MetadataBuilder()
+        .withMetadata(f.metadata)
+        .remove("parquet.field.id")
+        // Sibling key Delta stamps on array/map fields under 
IcebergCompat/Uniform.
+        .remove("parquet.field.nested.ids")
+        .build()
+      f.copy(dataType = stripType(f.dataType), metadata = metadata)
+    })
+  }
+
+  /**
+   * Build the planning-time `DeltaScan` operator (common data only; file 
partitions are injected
+   * lazily at execution). Returns None when an output data type cannot be 
serialized or the plan
+   * shape is not one we can translate faithfully.
+   */
+  def convert(scanExec: FileSourceScanExec, scanHelper: CometScanExec): 
Option[Operator] = {
+    val relation = scanExec.relation
+
+    val firstFileUri = scanHelper.selectedPartitions
+      .flatMap(_.files.headOption)
+      .headOption
+      .map(_.getPath.toUri)
+
+    val hadoopConf = relation.sparkSession.sessionState
+      .newHadoopConfWithOptions(relation.options)
+
+    val tableRootPath = relation.location.rootPaths.head
+    val tableRoot = tableRootPath.toString
+
+    val commonOpt = if (!isDvShape(scanExec)) {
+      // Under column mapping (name mode) the parquet reader must see physical 
names;
+      // positions are preserved so output binding and projection stay 
untouched.
+      CometNativeScan.buildNativeScanCommon(
+        source = scanExec.simpleStringWithNodeId(),
+        output = scanExec.output,
+        requiredSchema = toPhysical(scanExec, scanExec.requiredSchema),
+        dataSchema = toPhysical(scanExec, relation.dataSchema),

Review Comment:
   **[P1] Current-head reader reproduction of the Unicode mismatch**
   
   Follow-up on `4ca3207af385`: the current native library reads a real Parquet 
value `42` as follows with case sensitivity disabled. The reference column is 
Spark 4.1.3's actual `ParquetReadSupport.clipParquetSchema` on JDK21:
   
   | Physical / requested name | Spark footer lookup | Native value |
   | --- | --- | --- |
   | `A1Σ` / `a1σ` | Missing field | `42` |
   | `A1Σ` / `a1ς` | Match | `NULL` |
   | U+A7C0 / U+A7C1 | Match | `NULL` |
   
   The inverse sigma case therefore reads a field Spark considers missing, in 
addition to the previously reported lost values. A covering `IS NOT NULL` 
filter retains or drops the wrong row. Latin and case-sensitive controls pass.
   
   Could the 
[matcher](https://github.com/apache/datafusion-comet/blob/4ca3207af3853f8c6572098e14b53f79ab1d6fea/native/core/src/parquet/schema_adapter.rs#L242-L248)
 account for the executing JVM's Unicode version and contextual sigma behavior? 
This is native Parquet execution plus Spark footer-method execution, not a full 
Spark/Delta/JNI query. The unchanged JVM parity suite also fails its full sweep 
on JDK21 (2 passed, 1 failed; only the test repository locator was stubbed).



##########
contrib/delta-spark/src/main/scala/org/apache/comet/contrib/delta/CometDeltaNativeScan.scala:
##########
@@ -0,0 +1,533 @@
+/*
+ * 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.comet.contrib.delta
+
+import scala.jdk.CollectionConverters._
+
+import org.apache.hadoop.fs.Path
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.catalyst.expressions.Literal
+import org.apache.spark.sql.comet.{CometScanExec, DeltaPlanDataInjector}
+import org.apache.spark.sql.delta.DeltaParquetFileFormat
+import org.apache.spark.sql.delta.RowIndexFilterType
+import org.apache.spark.sql.delta.actions.DeletionVectorDescriptor
+import org.apache.spark.sql.execution.{FileSourceScanExec, ScalarSubquery => 
ExecScalarSubquery}
+import org.apache.spark.sql.execution.datasources.{FilePartition, 
PartitionedFile}
+import org.apache.spark.sql.types.{ByteType, LongType, MetadataBuilder, 
StructField, StructType}
+
+import org.apache.comet.objectstore.NativeConfig
+import org.apache.comet.serde.OperatorOuterClass
+import org.apache.comet.serde.OperatorOuterClass.Operator
+import org.apache.comet.serde.QueryPlanSerde.{exprToProto, serializeDataType}
+import org.apache.comet.serde.operator.{literalToProto, partition2Proto, 
schema2Proto, CometNativeScan}
+import org.apache.comet.shims.ShimFileFormat
+
+/**
+ * Serde for the native Delta scan. Two shapes:
+ *   - Plain reads reuse core's `NativeScanCommon` builder wholesale.
+ *   - Deletion-vector reads: Delta's planner appends 
`__delta_internal_is_row_deleted` (tinyint)
+ *     and Spark's row-index temp column (bigint) to the read schema and 
filters on is_row_deleted
+ *     above the scan. The native reader applies the DV as a row selection, so 
surviving rows are
+ *     by construction not deleted: both internal columns are emitted as 
per-file constants (0),
+ *     the parquet read schema is stripped to the real data columns, and the 
DV descriptor ships
+ *     per file for the native side to fetch and decode.
+ */
+object CometDeltaNativeScan
+    extends Logging
+    with org.apache.spark.sql.catalyst.expressions.PredicateHelper {
+
+  val IsRowDeletedColumn: String = 
DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME
+  val RowIndexColumn: String = ShimFileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME
+
+  private[delta] val internalColumnNames: Set[String] = 
Set(IsRowDeletedColumn, RowIndexColumn)
+
+  // Prefix for the internal columns' slots in the partition schema, mirroring 
core's
+  // _comet_metadata_ prefix rationale: DataFusion matches partition columns 
by name.
+  private val deltaConstFieldPrefix = "_comet_delta_"
+
+  def isDvShape(scanExec: FileSourceScanExec): Boolean =
+    scanExec.requiredSchema.exists(f => internalColumnNames.contains(f.name))
+
+  private def deltaFormat(scanExec: FileSourceScanExec): 
DeltaParquetFileFormat =
+    scanExec.relation.fileFormat.asInstanceOf[DeltaParquetFileFormat]
+
+  private def columnMappingMode(scanExec: FileSourceScanExec): String =
+    deltaFormat(scanExec).metadata.columnMappingMode.name
+
+  /**
+   * Under column mapping, parquet files store physical column names (stable 
UUIDs / ids), so the
+   * schemas passed to the native parquet reader must be physical. Positions 
and structure are
+   * preserved, so all positional output binding and projection are 
unaffected. The scan's
+   * internal DV columns are not part of the table schema and must be stripped 
before calling
+   * this.
+   */
+  private def toPhysical(scanExec: FileSourceScanExec, schema: StructType): 
StructType = {
+    val format = deltaFormat(scanExec)
+    if (format.metadata.columnMappingMode.name == "none") {
+      schema
+    } else {
+      // Name mode matches file columns by physical NAME. createPhysicalSchema 
also stamps
+      // parquet.field.id metadata, but files written before the 
column-mapping upgrade have
+      // no field ids and would fail the reader's id expectations, strip the 
ids so the
+      // reader stays purely name-based (id mode, when enabled, will keep 
them).
+      stripFieldIds(org.apache.spark.sql.delta.DeltaColumnMapping
+        .createPhysicalSchema(schema, format.metadata.schema, 
format.metadata.columnMappingMode))
+    }
+  }
+
+  private def stripFieldIds(schema: StructType): StructType = {
+    import org.apache.spark.sql.types._
+    def stripType(dt: DataType): DataType = dt match {
+      case s: StructType => stripFieldIds(s)
+      case a: ArrayType => a.copy(elementType = stripType(a.elementType))
+      case m: MapType =>
+        m.copy(keyType = stripType(m.keyType), valueType = 
stripType(m.valueType))
+      case other => other
+    }
+    StructType(schema.fields.map { f =>
+      val metadata = new MetadataBuilder()
+        .withMetadata(f.metadata)
+        .remove("parquet.field.id")
+        // Sibling key Delta stamps on array/map fields under 
IcebergCompat/Uniform.
+        .remove("parquet.field.nested.ids")
+        .build()
+      f.copy(dataType = stripType(f.dataType), metadata = metadata)
+    })
+  }
+
+  /**
+   * Build the planning-time `DeltaScan` operator (common data only; file 
partitions are injected
+   * lazily at execution). Returns None when an output data type cannot be 
serialized or the plan
+   * shape is not one we can translate faithfully.
+   */
+  def convert(scanExec: FileSourceScanExec, scanHelper: CometScanExec): 
Option[Operator] = {
+    val relation = scanExec.relation
+
+    val firstFileUri = scanHelper.selectedPartitions
+      .flatMap(_.files.headOption)
+      .headOption
+      .map(_.getPath.toUri)
+
+    val hadoopConf = relation.sparkSession.sessionState
+      .newHadoopConfWithOptions(relation.options)
+
+    val tableRootPath = relation.location.rootPaths.head
+    val tableRoot = tableRootPath.toString
+
+    val commonOpt = if (!isDvShape(scanExec)) {
+      // Under column mapping (name mode) the parquet reader must see physical 
names;
+      // positions are preserved so output binding and projection stay 
untouched.
+      CometNativeScan.buildNativeScanCommon(
+        source = scanExec.simpleStringWithNodeId(),
+        output = scanExec.output,
+        requiredSchema = toPhysical(scanExec, scanExec.requiredSchema),
+        dataSchema = toPhysical(scanExec, relation.dataSchema),
+        partitionSchema = relation.partitionSchema,
+        fileConstantMetadataColumns = scanExec.fileConstantMetadataColumns,
+        dataFilters = scanHelper.supportedDataFilters,
+        firstFileUri = firstFileUri,
+        hadoopConf = hadoopConf,
+        conf = scanExec.conf)
+    } else {
+      buildDvScanCommon(scanExec, scanHelper, firstFileUri, hadoopConf)
+    }
+
+    commonOpt.map { commonBuilder =>
+      // Union object-store options over every authority a partition of this 
scan may need a
+      // store for, not just the first data file's scheme (finding 8).
+      val dvDescriptors = DeltaScanSupport.selectedDvDescriptors(scanHelper, 
tableRoot)
+      commonBuilder.putAllObjectStoreOptions(
+        mergedObjectStoreOptions(
+          hadoopConf,
+          storeUris(dvDescriptors, tableRootPath, firstFileUri)).asJava)

Review Comment:
   **[P2] Empty short bucket aliases still bypass the credential comparison**
   
   One remaining case at `4ca3207af385`: with `SimpleAWSCredentialsProvider`, 
set global and long bucket access/secret aliases 
(`fs.s3a.bucket.b.fs.s3a.access.key` / `secret.key`) to the same nonempty 
synthetic pair, and set the short aliases (`fs.s3a.bucket.b.access.key` / 
`secret.key`) to empty strings.
   
   Both admission gates return `None`. Actual Hadoop 3.4.2 
`S3AUtils.propagateBucketOptions` plus `SimpleAWSCredentialsProvider` resolves 
the populated long aliases. Current `NativeConfig` forwards the empty short 
aliases, and the unchanged Rust `get_config_trimmed` returns `Some("")` for 
both credentials. A protected S3 read is therefore claimed with different 
credentials and fails instead of falling back. Removing the empty short aliases 
is a passing control.
   
   Could the native side of the comparator preserve empty overrides? 
[plainValue](https://github.com/apache/datafusion-comet/blob/4ca3207af3853f8c6572098e14b53f79ab1d6fea/contrib/delta-spark/src/main/scala/org/apache/comet/contrib/delta/DeltaScanSupport.scala#L509-L510)
 filters them out, unlike [native 
lookup](https://github.com/apache/datafusion-comet/blob/4ca3207af3853f8c6572098e14b53f79ab1d6fea/native/core/src/parquet/objectstore/s3.rs#L300-L317).
 These are executed Hadoop/admission/extraction/Rust component probes, not a 
live S3 or full Spark/JNI scan.



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