andygrove commented on code in PR #5361:
URL: https://github.com/apache/datafusion-comet/pull/5361#discussion_r3916282742


##########
spark/src/main/scala/org/apache/comet/serde/operator/CometIcebergNativeWrite.scala:
##########
@@ -263,6 +275,15 @@ object CometIcebergNativeWrite extends 
CometOperatorSerde[IcebergWriteExec] with
       .map(_ => s"$key=true (variant shredding changes the parquet schema)")
   }
 
+  // iceberg-java throws NumberFormatException at write time for a non-integer 
level, while the
+  // native translation would silently substitute the codec default. Fall back 
so the failure
+  // behaviour matches the stock path.
+  private val requireParseableCompressionLevel: TriggerRule = ctx =>
+    ctx.properties
+      .get(PropertyKeys.ParquetCompressionLevel)
+      .filter(v => scala.util.Try(java.lang.Integer.parseInt(v)).isFailure)

Review Comment:
   This checks that the level parses as a Java int, but the native side is 
stricter than that. parquet-rs enforces zstd `1..=22`, gzip `0..=9` and brotli 
`0..=11`, and `build_writer_properties` returns an error outside those, which 
surfaces as a task failure rather than a fallback.
   
   iceberg-java does not validate the level at all. I pulled 
`Parquet$WriteBuilder$Context` out of the 1.10.0 runtime jar to check, and 
unlike the four size properties, `compressionLevel` is kept as a raw `String` 
and dropped straight into `parquet.compression.codec.zstd.level` / 
`zlib.compress.level` / `compression.brotli.quality`. No `propertyAsInt`, no 
`checkArgument`. So `write.parquet.compression-level=0` with zstd, or a 
negative zstd fast level, writes fine on the stock path and dies mid-task on 
this one.
   
   Could this rule resolve the codec (`resolveCompression` is right there in 
`IcebergWriteProtoTranslation`) and decline anything the corresponding 
parquet-rs level type would reject? That is the same treatment 
`requirePositiveIntParquetSizes` just gave the size properties. Boundary cases 
worth pinning in the detection suite next to the existing `fast` test: zstd 
`0`, zstd `-3`, zstd `23`, gzip `-1`, brotli `12`.
   



##########
spark/src/main/scala/org/apache/spark/sql/comet/CometIcebergWriteExec.scala:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.spark.sql.comet
+
+import org.apache.spark.TaskContext
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{Attribute, 
AttributeReference}
+import org.apache.spark.sql.catalyst.expressions.UnsafeProjection
+import org.apache.spark.sql.comet.execution.arrow.CometArrowStream
+import org.apache.spark.sql.comet.util.{Utils => CometUtils}
+import org.apache.spark.sql.connector.write.{BatchWrite, WriterCommitMessage}
+import org.apache.spark.sql.execution.{ColumnarToRowTransition, SparkPlan, 
UnaryExecNode}
+import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
+import org.apache.spark.sql.types.BinaryType
+import org.apache.spark.sql.vectorized.ColumnarBatch
+
+import com.google.protobuf.CodedOutputStream
+
+import org.apache.comet.CometExecIterator
+import org.apache.comet.iceberg.IcebergReflection
+import org.apache.comet.serde.OperatorOuterClass.Operator
+
+/**
+ * Native variant of [[IcebergWriteExec]]. Drives the iceberg-rust writer 
stack via Comet's native
+ * execution pipeline; the JVM side decodes the per-task Avro-encoded 
`DataFile` blob the native
+ * operator emits and packages it as a [[WriterCommitMessage]] so the outer 
[[IcebergCommitExec]]
+ * consumes it unchanged.
+ *
+ * Selected by [[org.apache.comet.serde.operator.CometIcebergNativeWrite]] 
when the table's
+ * properties allow it (parquet, V2, no encryption, ...) and the child plan is 
fully Comet-native;
+ * otherwise the JVM path's [[IcebergWriteExec]] runs instead.
+ *
+ * @param nativeOp
+ *   Template operator carrying the `IcebergWrite` proto. Per-task 
`partition_id` /
+ *   `task_attempt_id` get stamped on a copy at execution time.
+ * @param child
+ *   Comet native child (must be a [[CometNativeExec]] so columnar batches 
flow through FFI).
+ * @param batchWrite
+ *   Shared with the outer [[IcebergCommitExec]] -- the same instance the 
strategy materialised
+ *   via `write.toBatch`. Used here only to provide the `dataLocation` / 
partition spec needed by
+ *   the native side; never invoked for commit.
+ * @param partitionSpecId
+ *   Output partition spec id (from `SparkWrite.outputSpecId`). Decoded 
`DataFile`s are stamped
+ *   with this spec id; required because iceberg-rust's `DataFile` is 
spec-agnostic at the wire.
+ */
+case class CometIcebergWriteExec(
+    nativeOp: Operator,
+    child: SparkPlan,
+    @transient batchWrite: BatchWrite,
+    @transient table: AnyRef,
+    partitionSpecId: Int)
+    extends CometNativeExec
+    with UnaryExecNode
+    // We consume Arrow batches (via FFI) and emit row-shaped commit messages, 
so we are a
+    // columnar-to-row transition. Without this trait Spark's
+    // `ApplyColumnarRulesAndInsertTransitions` wedges a 
`CometNativeColumnarToRowExec` between
+    // us and the Comet-native child, which would then fail 
`child.executeColumnar()` in
+    // `doExecuteColumnar`.
+    with ColumnarToRowTransition {
+
+  override def originalPlan: SparkPlan = child
+
+  // Same output schema as IcebergWriteExec so the outer IcebergCommitExec 
consumes the
+  // commit messages identically regardless of which inner exec emitted them.
+  override def output: Seq[Attribute] = Seq(
+    AttributeReference(IcebergWriteExec.CommitMessageColumn, BinaryType, 
nullable = false)())
+
+  // Native exec emits a single Binary column; the surrounding command 
framework expects rows, so
+  // the outer commit exec calls executeCollect on us. supportsColumnar = 
false keeps Spark from
+  // inserting a ColumnarToRow that would clash with our (Nil-output-like) row 
contract.
+  override def supportsColumnar: Boolean = false
+
+  override def executeCollect(): Array[InternalRow] = {
+    val rdd = doExecute()
+    // SparkPlan.executeCollect defaults to byteArrayRdd which goes through 
UnsafeRow encoding;
+    // doExecute already projects each row through UnsafeProjection (see the 
per-task closure
+    // below) so a plain `collect()` is safe.
+    rdd.collect()
+  }
+
+  override def serializedPlanOpt: SerializedPlan = {
+    val size = nativeOp.getSerializedSize
+    val bytes = new Array[Byte](size)
+    val codedOutput = CodedOutputStream.newInstance(bytes)
+    nativeOp.writeTo(codedOutput)
+    codedOutput.checkNoSpaceLeft()
+    SerializedPlan(Some(bytes))
+  }
+
+  override def withNewChildInternal(newChild: SparkPlan): SparkPlan = 
copy(child = newChild)
+
+  override def nodeName: String = "CometIcebergWrite"

Review Comment:
   Now that `catalog_properties` picks up the translated `fs.s3a.*` settings, 
could this override `stringArgs` the way `CometIcebergNativeScanExec` does?
   
   `nativeOp` is the first field of the case class and there is no `stringArgs` 
override, so `TreeNode.argString` falls through to its `case other => other :: 
Nil` branch and calls `toString` on the protobuf, which gives the whole 
TextFormat dump. That lands in `explain()`, in the SQL UI node description, and 
in `SparkListenerSQLExecutionStart` in the event log. With the merge you just 
added in `buildIcebergWriteProto`, the dump now includes `catalog_properties { 
key: "s3.secret-access-key" ... }` for an S3A table whose keys live in the 
Hadoop configuration.
   
   Spark's own redaction will not catch this. `argString` redacts through 
`redactMapString` only for args that are Scala `Map`s, and a protobuf message 
falls past that to the plain `toString`. 
`CometIcebergNativeScanExec.stringArgs` sidesteps exactly this by emitting 
`output` plus a short descriptor instead of the proto, and it carries the same 
property bag. Something like `Iterator(output, s"$dataLocation, $writerMode")` 
here would keep the node readable and keep the secret out of the log.
   
   Your new "write proto forwards fs.s3a.*" test looks like a good place to pin 
it, by asserting the exec's `simpleString` does not contain the secret value.
   



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