andygrove opened a new issue, #5635:
URL: https://github.com/apache/datafusion-comet/issues/5635

   ### What is the problem the feature request solves?
   
   Now that the native Iceberg writer has landed (#5361), it declines the case 
most users will
   actually hit: a partitioned table written with Iceberg's default 
distribution mode.
   
   `CometIcebergNativeWrite` sets `requiresNativeChildren = true`
   
(`spark/src/main/scala/org/apache/comet/serde/operator/CometIcebergNativeWrite.scala:43`),
 so the
   entire sub-plan feeding `IcebergWrite` has to be a `CometNativeExec`. For a 
partitioned table with
   no explicit `write.distribution-mode`, Iceberg's 
`SparkWriteConf.defaultWriteDistributionMode()`
   returns `HASH`, so Spark plans a `ShuffleExchangeExec` whose 
`HashPartitioning` keys are the
   partition transform expressions. Comet's native shuffle gate requires every 
one of those to
   serialize:
   
   ```scala
   case HashPartitioning(expressions, _) =>
     ...
     for (expr <- expressions) {
       if (QueryPlanSerde.exprToProto(expr, inputs).isEmpty) {
         reasons += s"unsupported hash partitioning expression: $expr"
       }
     }
   ```
   
   (`CometShuffleExchangeExec.scala:473-484`, and again at `:610` for the 
columnar-shuffle gate.)
   
   Iceberg's system functions expose Spark's static magic method, so
   `V2ExpressionUtils.resolveScalarFunction` binds them as `StaticInvoke` 
rather than
   `ApplyFunctionExpression`:
   
   ```
   $ javap 'org.apache.iceberg.spark.functions.BucketFunction$BucketInt'
     public static int invoke(int, int);
   $ javap 'org.apache.iceberg.spark.functions.TruncateFunction$TruncateString'
     public static org.apache.spark.unsafe.types.UTF8String invoke(int, 
org.apache.spark.unsafe.types.UTF8String);
   $ javap 'org.apache.iceberg.spark.functions.DaysFunction$DateToDaysFunction'
     public static int invoke(int);
   ```
   
   `CometStaticInvoke` dispatches on `(functionName, staticObject)` against a 
fixed allowlist
   (`spark/src/main/scala/org/apache/comet/serde/statics.scala:35-52`), and 
none of the Iceberg
   classes are in it. So `exprToProto` returns `None`, the exchange stays on 
the JVM, the write's
   child is not a `CometNativeExec`, and the native write declines with a 
fall-back reason.
   
   The benchmark in #5361 says as much: the 5.5x partitioned number was 
measured with
   `write.distribution-mode=none` plus `write.spark.fanout.enabled=true`, 
chosen so all three
   scenarios shared one plan shape. Under stock table settings a partitioned 
append gets no
   acceleration at all, so the headline win is currently reachable only by 
reconfiguring the table.
   
   The same gap costs us on the read and DML side. Iceberg's own
   `TestSystemFunctionPushDownInRowLevelOperations` exercises `bucket`, 
`truncate`, `years`,
   `months`, `days`, and `hours` in copy-on-write `DELETE` / `UPDATE` / `MERGE` 
predicates (it shows
   up as bucket 2 in #5259), and #5339 wants the same transforms for reporting 
Iceberg sort orders.
   
   ### Describe the potential solution
   
   Implement the transforms natively and register them in
   `CometStaticInvoke.staticInvokeExpressions`, keyed on the Iceberg function 
classes. That is the
   narrowest hook that fixes shuffle, filter, projection, and sort in one go, 
since everything routes
   through `exprToProto`.
   
   The classes to cover, all under `org.apache.iceberg.spark.functions`:
   
   | Function | Implementations |
   | --- | --- |
   | `BucketFunction` | `BucketInt`, `BucketLong`, `BucketString`, 
`BucketBinary`, `BucketDecimal` |
   | `TruncateFunction` | `TruncateTinyInt`, `TruncateSmallInt`, `TruncateInt`, 
`TruncateBigInt`, `TruncateString`, `TruncateBinary`, `TruncateDecimal` |
   | `YearsFunction` | `DateToYearsFunction`, `TimestampToYearsFunction`, 
`TimestampNtzToYearsFunction` |
   | `MonthsFunction` | `DateToMonthsFunction`, `TimestampToMonthsFunction`, 
`TimestampNtzToMonthsFunction` |
   | `DaysFunction` | `DateToDaysFunction`, `TimestampToDaysFunction`, 
`TimestampNtzToDaysFunction` |
   | `HoursFunction` | `TimestampToHoursFunction`, 
`TimestampNtzToHoursFunction` |
   
   `IcebergVersionFunction` is a zero-arg constant and not worth native support.
   
   Semantics have to match Iceberg's spec exactly, not approximately:
   
   - **bucket** is `(murmur3_32_x86(value) & Integer.MAX_VALUE) % numBuckets`, 
over the byte encoding
     in Appendix B of the Iceberg spec — 8-byte little-endian for int/long/date 
(days)/timestamp
     (micros), UTF-8 bytes for string, raw bytes for binary/fixed, minimal 
big-endian two's-complement
     unscaled bytes for decimal, 16-byte big-endian for UUID. Note that Iceberg 
maps `uuid` to Spark's
     `StringType`, which is the same mapping the write eligibility gate already 
reasons about.
   - **truncate** is `v - ((v % W) + W) % W` for integrals, the same on the 
unscaled value for
     decimals, a byte prefix for binary, and `UTF8String.substring(0, W)` for 
strings — code points,
     not bytes and not UTF-16 units.
   - **years / months** are `ChronoUnit.between(EPOCH, value)`, so they are 
calendar-aware and
     relative to 1970 and go negative before it; **days / hours** are plain 
floor division on the
     epoch value.
   - Spark binds these with `propagateNull = false`, so null handling belongs 
to the implementation
     rather than to the `StaticInvoke` wrapper. `BucketString.invoke` returns a 
boxed `Integer`
     precisely so it can return null.
   
   Some of these may already exist in DataFusion or `datafusion-spark` in a 
compatible form; worth
   checking before writing Rust, per the `wire-datafusion-function` skill.
   
   On testing: divergence here mostly fails loudly rather than silently, but 
not always, so it is
   worth being explicit about which is which. If Comet's bucket disagrees with 
Iceberg's, rows of one
   Iceberg partition get split across reducers, and the clustered (non-fanout) 
writer rejects
   out-of-order partitions — a task failure, not corruption. The silent case is 
these functions in a
   filter or projection, where a wrong hash is just a wrong answer. That argues 
for per-type fuzz
   comparison against Iceberg's own `Transforms` / `BucketFunction` 
implementations rather than
   hand-picked cases, which is the same bar #5339 sets for transformed sort 
orders.
   
   ### Additional context
   
   Small related diagnostic gap: the fall-back reason for an unlisted static 
invoke is
   `s"Static invoke expression: ${expr.functionName} is not supported"` 
(`statics.scala:62-64`).
   Every Iceberg system function has `functionName == "invoke"`, so a user 
hitting this sees
   `Static invoke expression: invoke is not supported` with nothing pointing at 
Iceberg. Including
   `expr.staticObject` in the message would make this self-diagnosing.
   
   Related:
   
   - #5361 — the native Iceberg writer, whose partitioned benchmark had to 
disable hash distribution
   - #5259 — split-operator CI failures, whose bucket 2 is Iceberg's 
system-function pushdown tests
   - #5339 — transformed sort orders (bucket / truncate / etc.) for Iceberg 
reads
   - #5121 — accelerate DataSource V2 writes
   


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