parthchandra commented on code in PR #4752:
URL: https://github.com/apache/datafusion-comet/pull/4752#discussion_r3687466695


##########
native/core/src/execution/planner.rs:
##########
@@ -3835,6 +3835,41 @@ fn parse_file_scan_tasks_from_common(
         })
         .collect::<Result<Vec<_>, _>>()?;
 
+    // Compute unified partition type from the partition_type_pool.
+    // Each entry is a StructType JSON with the resolved field types for one 
partition spec.
+    // Merge all specs into a single unified type (dedup by field_id).
+    //
+    // NOTE: The type information here comes from Iceberg Java's 
PartitionSpec.partitionType()
+    // (via Scala reflection). This is the same type computation as 
iceberg-rust's
+    // Transform::result_type() -- both implement the Iceberg spec's transform 
result rules.
+    // When iceberg-rust's own scan planning is used (not Comet's proto path), 
it computes
+    // this via compute_unified_partition_type(specs, schema) instead.
+    let unified_partition_type = {
+        let mut seen_field_ids = std::collections::HashSet::new();
+        let mut struct_fields: Vec<iceberg::spec::NestedFieldRef> = Vec::new();
+
+        for type_json in &proto_common.partition_type_pool {
+            match serde_json::from_str::<iceberg::spec::StructType>(type_json) 
{
+                Ok(struct_type) => {
+                    for field in struct_type.fields() {
+                        if seen_field_ids.insert(field.id) {
+                            struct_fields.push(Arc::clone(field));
+                        }
+                    }
+                }
+                Err(e) => {
+                    return Err(ExecutionError::GeneralError(format!(
+                        "Failed to deserialize partition type JSON from pool: 
{e}"
+                    )));
+                }
+            }
+        }

Review Comment:
   You're right. This was wrong. 
   Fixed and this should match 
`compute_unified_partition_type`/`Partitioning.buildPartitionProjectionType`. 
To get `spec_id` on the native side, I made `partition_type_pool` index-aligned 
with `partition_spec_pool` (documented in the .proto) and read 
`partition_spec_cache[i].spec_id()`.
   One caveat: because Comet ships resolved types (not the original 
`Transform`), the void-vs-non-void type preference isn't replicated — but that 
only affects a dropped-source-column field, which Comet already filters upstream



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -603,6 +626,25 @@ case class CometScanRule(session: SparkSession)
           false
         }
 
+        // The `_partition` metadata column projects the table's unified 
partition type -- the
+        // merge of all historical specs. iceberg-rust computes that merge at 
scan time and errors
+        // (DataInvalid) when specs bind one field id to incompatible 
source/transform pairs (only
+        // possible in V1 tables, which do not keep partition field ids unique 
across specs). That
+        // error would fail the native scan with no chance to fall back, so 
when `_partition` is
+        // projected we run Iceberg Java's equivalent check here and fall back 
if it cannot merge.
+        val unifiedPartitionTypeSupported =
+          if (scanExec.output.exists(a => a.isMetadataCol && a.name == 
"_partition")) {
+            IcebergReflection.validateUnifiedPartitionType(metadata.table) 
match {
+              case Some(reason) =>
+                fallbackReasons += "Iceberg table has partition specs whose 
unified partition " +
+                  s"type cannot be computed for the _partition metadata 
column: $reason"
+                false
+              case None => true
+            }
+          } else {
+            true
+          }

Review Comment:
   Added a test. This couldn't be tested end to end via SQL because Spark's 
analyzer catches it and throws a validation error.  The test hand-builds the 
conflicting V1 metadata.json (round-tripped through `TableMetadataParser` to 
catch malformed edits) and asserts directly on 
`IcebergReflection.validateUnifiedPartitionType`



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -361,9 +361,30 @@ case class CometScanRule(session: SparkSession)
           return withFallbackReasons(scanExec, fallbackReasons.toSet)
         }
 
+        // Check for unsupported metadata columns in Iceberg scans
+        val unsupportedMetadataCols = 
scanExec.output.filter(_.isMetadataCol).filterNot { attr =>
+          CometIcebergNativeScan.MetadataFieldIds.keySet.contains(attr.name)
+        }
+        if (unsupportedMetadataCols.nonEmpty) {
+          fallbackReasons += "Unsupported Iceberg metadata columns: " +
+            unsupportedMetadataCols.map(_.name).mkString(", ")
+          return withFallbackReasons(scanExec, fallbackReasons.toSet)
+        }

Review Comment:
   Test added.



##########
spark/src/main/scala/org/apache/comet/serde/operator/CometIcebergNativeScan.scala:
##########
@@ -487,25 +501,48 @@ object CometIcebergNativeScan extends 
CometOperatorSerde[CometBatchScanExec] wit
               }
             }.toSeq
 
-          // Only serialize partition data if we have non-unknown fields
-          if (partitionValues.nonEmpty) {
-            val partitionDataProto = OperatorOuterClass.PartitionData
-              .newBuilder()
-              .addAllValues(partitionValues.asJava)
-              .build()
-
-            // Deduplicate by protobuf bytes (use Base64 string as key)
-            val partitionDataBytes = partitionDataProto.toByteArray
-            val partitionDataKey = 
Base64.getEncoder.encodeToString(partitionDataBytes)
-
-            val partitionDataIdx = partitionDataToPoolIndex.getOrElseUpdate(
-              partitionDataKey, {
-                val idx = partitionDataToPoolIndex.size
-                commonBuilder.addPartitionDataPool(partitionDataProto)
+          // Native requires a task to carry both a partition spec and 
partition data, or neither:
+          // iceberg-rust errors when the unified partition type has fields 
but a task is missing
+          // its spec/data. A file written while a partition field was dropped 
(partition
+          // evolution) has no partition values, so partitionValues is empty 
here.
+          //
+          // For that value-less case we must NOT send the file's real spec: 
it may retain a void
+          // field whose id collides with a unified field, which would make 
iceberg-rust index the
+          // empty partition data out of range. Instead send an empty-fields 
spec that keeps the
+          // real spec id (so _spec_id stays correct) plus empty data, so 
native fills every
+          // unified _partition field with null -- matching Spark and the 
pre-tightening behaviour.
+          if (partitionValues.isEmpty) {
+            val specId = 
spec.getClass.getMethod("specId").invoke(spec).asInstanceOf[Int]
+            val emptySpecJson = s"""{"spec-id":$specId,"fields":[]}"""
+            val emptySpecIdx = partitionSpecToPoolIndex.getOrElseUpdate(
+              emptySpecJson, {
+                val idx = partitionSpecToPoolIndex.size
+                commonBuilder.addPartitionSpecPool(emptySpecJson)
                 idx
               })
-            taskBuilder.setPartitionDataIdx(partitionDataIdx)
+            // Override the real spec registered above.
+            taskBuilder.setPartitionSpecIdx(emptySpecIdx)
           }

Review Comment:
   Fixed the JSON consistency: the empty-spec literal now uses the json4s DSL 
like the rest of the file



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -451,15 +472,17 @@ case class CometScanRule(session: SparkSession)
         }
 
         // Now perform all validation using the pre-extracted metadata
-        // Check if table uses a FileIO implementation compatible with 
iceberg-rust
-
+        // Check if table uses a FileIO implementation compatible with 
iceberg-rust.
+        // Comet's native reader uses object_store (Rust) for I/O, bypassing 
Iceberg Java's
+        // FileIO entirely. Only allow known-compatible implementations whose 
underlying
+        // storage object_store can reach via standard URL schemes.
         val fileIOCompatible = IcebergReflection.getFileIO(metadata.table) 
match {
-          case Some(fileIO)
-              if fileIO.getClass.getName == 
"org.apache.iceberg.inmemory.InMemoryFileIO" =>
-            fallbackReasons += "InMemoryFileIO is not supported by Comet's 
native reader"
-            false
-          case Some(_) =>
+          case Some(fileIO) if 
CometScanRule.isCompatibleFileIO(fileIO.getClass.getName) =>
             true
+          case Some(fileIO) =>
+            fallbackReasons += s"FileIO ${fileIO.getClass.getName} is not 
supported by " +
+              "Comet's native reader (object_store bypasses Iceberg Java 
FileIO)"
+            false
           case None =>
             fallbackReasons += "Could not check FileIO compatibility"
             false

Review Comment:
   Right again.  Fixed exactly as you suggested.



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