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


##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -835,12 +835,38 @@ case class CometScanRule(session: SparkSession)
           }
         }
 
+        // If Iceberg reports an ordering, EnsureRequirements may have already 
dropped the Sort
+        // above this scan (it decides that on the vanilla BatchScanExec, 
before Comet converts the
+        // scan). If the native scan cannot guarantee that ordering, reading 
unordered here would
+        // silently return wrong results, so stay on Spark -- its Iceberg 
reader produces the sorted
+        // output it promised. reportableOrdering is the same gate the native 
scan/serde use, so the
+        // decision here cannot diverge from what the native path would do.
+        val orderingHonored: Boolean = {

Review Comment:
   Good point.  Changes so we evaluate the gate once and save the result on 
`nativeIcebergScanMetadata`; `outputOrdering` and the proto serde both read 
that field.



##########
native/core/src/execution/spark_config.rs:
##########
@@ -26,6 +26,21 @@ pub(crate) const COMET_PARQUET_ROW_FILTER_PUSHDOWN_ENABLED: 
&str =
     "spark.comet.parquet.rowFilterPushdown.enabled";
 pub(crate) const SPARK_EXECUTOR_CORES: &str = "spark.executor.cores";
 
+/// Above this many files in a single Spark partition, the native Iceberg scan 
does not attempt a
+/// per-file-stream k-way merge (which would open one reader per file at 
once); it reads the
+/// partition unordered and sorts with a spillable SortExec instead. Read from 
the config below and
+/// carried to the physical planner as an [`IcebergSortMergeConfig`] session 
extension.
+pub(crate) const COMET_ICEBERG_SORT_MERGE_MAX_FILES_PER_PARTITION: &str =
+    "spark.comet.scan.icebergNative.sortMerge.maxFilesPerPartition";
+pub(crate) const DEFAULT_ICEBERG_SORT_MERGE_MAX_FILES_PER_PARTITION: usize = 
64;

Review Comment:
   Moved `max_files_per_partition` onto `IcebergScanCommon` next to 
`data_file_concurrency_limit`, so there's one default (in CometConf) and 
removed the SessionConfig extension. Added 
`iceberg_scan_honors_max_files_per_partition_from_proto`, which sets the proto 
field to 2 and asserts a 3-file scan takes SortExec — so the value is actually 
read from the plan  now, not the compiled-in constant



##########
spark/src/main/scala/org/apache/comet/CometConf.scala:
##########
@@ -121,6 +121,30 @@ object CometConf extends ShimCometConf {
       .booleanConf
       .createWithDefault(true)
 
+  val COMET_ICEBERG_SORT_MERGE_ENABLED: ConfigEntry[Boolean] =
+    conf("spark.comet.scan.icebergNative.sortMerge.enabled")
+      .category(CATEGORY_SCAN)
+      .doc("Whether the native Iceberg scan reports the table sort order and 
performs a " +
+        "per-partition streaming merge of already-sorted files. When enabled 
and Iceberg reports " +
+        "an ordering (requires Iceberg's 
spark.sql.iceberg.planning.preserve-data-ordering), " +
+        "each Spark partition reads its files as separate sorted streams 
merged into one sorted " +
+        "output, and the ordering is surfaced to Spark so redundant sorts are 
eliminated. When " +
+        "disabled, files are read unordered as before.")

Review Comment:
   You're right, doing what you suggest: `enabled=false` now behaves like 
`maxFilesPerPartition=0` — the scan stays native and still honors the reported 
order via the spillable `SortExec`, it just skips the k-way merge. The two 
disabled-fallback tests are now "stays native" tests.
   



##########
spark/src/main/scala/org/apache/comet/iceberg/IcebergReflection.scala:
##########
@@ -935,6 +935,32 @@ object IcebergReflection extends Logging {
     }
   }
 
+  /**
+   * Names of top-level columns whose Iceberg sort order can differ from 
Spark's comparison of the
+   * Spark type they map to, so a native k-way merge keyed on the Spark value 
could mis-order
+   * rows. Today that is UUID: Iceberg maps UUID to Spark StringType (see 
TypeToSparkType) but
+   * sorts by its own UUID comparator, not by the canonical string, so the 
file order and a string
+   * comparison can disagree. reportableOrdering refuses a sort key in this 
set. v1 only reports
+   * identity, top-level sort keys, so top-level columns are enough.
+   */
+  def orderingUnsafeColumns(schema: Any): Set[String] = {
+    import scala.jdk.CollectionConverters._
+    try {
+      val columns = getMethod(schema.getClass, "columns")
+        .invoke(schema)
+        .asInstanceOf[java.util.List[_]]
+      columns.asScala.flatMap { column =>
+        val name = getMethod(column.getClass, 
"name").invoke(column).asInstanceOf[String]
+        val typeStr = getMethod(column.getClass, 
"type").invoke(column).toString
+        if (typeStr == "uuid") Some(name) else None
+      }.toSet
+    } catch {
+      case e: Exception =>
+        logWarning(s"Failed to inspect schema for ordering-unsafe columns: 
${e.getMessage}")

Review Comment:
   Agreed. `orderingUnsafeColumns` now returns `None` when the schema can't be 
read, and the caller treats `None` as "refuse the ordering" — a reflection 
failure makes it un-reportable instead of assuming no UUID. The 
`nativeIcebergScanMetadata == null `branch is gone too; see the 
single-evaluation change below.



##########
native/core/src/execution/operators/iceberg_scan.rs:
##########
@@ -86,6 +86,24 @@ pub struct IcebergScanExec {
     tasks: Vec<FileScanTask>,
     /// Number of data files to read concurrently
     data_file_concurrency_limit: usize,
+    /// FileIO (and, for S3, the JVM credential bridge behind it) built once 
at plan time and shared
+    /// across partitions. FileIO is cheap to clone (Arc-backed), so each 
`execute` clones this
+    /// rather than rebuilding the storage factory + credential bridge. This 
matters in the ordered
+    /// path, where the scan is one partition per file and `execute` is called 
once per file.
+    file_io: FileIO,
+    /// Table sort order Iceberg reported, translated against `output_schema`. 
`Some` makes this a
+    /// multi-partition scan: one sorted stream per task, which a 
SortPreservingMergeExec above
+    /// merges back into one sorted partition. It is also advertised in 
`plan_properties`. `None`
+    /// keeps the old single-partition unordered read (all tasks streamed 
together).
+    ///
+    /// Concurrency note: in the ordered path each partition reads exactly one 
task, so
+    /// `data_file_concurrency_limit` no longer bounds cross-file concurrency; 
instead the wrapping
+    /// SortPreservingMergeExec drives one reader per file to merge them. That 
fan-out (files per
+    /// Spark partition) is intrinsic to a k-way merge of per-file sorted 
streams -- the files must
+    /// be read as separate streams to stay individually sorted -- and is the 
natural granularity
+    /// for a sorted Iceberg table. `data_file_concurrency_limit` still bounds 
delete-file stats and

Review Comment:
   Comment updated



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