andygrove opened a new pull request, #5293:
URL: https://github.com/apache/datafusion-comet/pull/5293

   ## Which issue does this PR close?
   
   Part of #2967 and #1625. Restructures the native write path so the following 
can be
   fixed at all, and closes the ones that were purely symptoms of the old 
design:
   
   Closes #2985 (no `_SUCCESS` file)
   Closes #3521 (`INSERT INTO ... SELECT` invisible to subsequent reads)
   Closes #3426 (complex type with different names)
   
   Unblocks (not fixed here, but no longer require re-implementing Spark's write
   framework inside Comet): #2957, #2970, #3015, #3041, #3193, #3194, #3417, 
#3428.
   
   ## Rationale for this change
   
   Native writes replaced the whole `DataWritingCommandExec`, which means
   `InsertIntoHadoopFsRelationCommand.run` never ran. Everything that method 
does had to
   be re-implemented inside `CometNativeWriteExec`: a hardcoded
   `SQLHadoopMapReduceCommitProtocol` (so 
`spark.sql.sources.commitProtocolClass` was
   ignored), `dynamicPartitionOverwrite` pinned to `false`, a hand-ported copy 
of the
   SaveMode logic, a bespoke commit-message accumulator, and its own 
`commitJob` call.
   
   Most of the open native-writer issues are symptoms of that one decision 
rather than
   independent defects. Fixing them one at a time against the old design means 
writing a
   second, worse `FileFormatWriter` inside Comet.
   
   Spark already has the right seam. On Spark 4.0+, 
`V1WritesUtils.getWriteFilesOpt`
   matches the `WriteFilesExecBase` **trait** (introduced in 4.0 precisely for 
this), so a
   Comet node that extends it gets driven through
   `FileFormatWriter.executeWrite` → `SparkPlan.executeWrite` → 
`doExecuteWrite`, and
   Spark keeps ownership of everything above the per-task write.
   
   **Why Spark 4.0+ only.** On 3.4/3.5 `getWriteFilesOpt` matches the concrete
   `WriteFilesExec` case class. A Comet node there would not be found, 
`writeFilesOpt`
   would be `None`, and Spark would silently take `FileFormatWriter`'s 
non-planned,
   row-based branch — ignoring `doExecuteWrite` entirely. The only way in on 
3.x is to
   inherit from a case class, which brings `copy`/`equals` hazards; that isn't 
worth
   carrying, so native writes now require 4.0+ and report a fallback reason on 
3.x.
   
   ## What changes are included in this PR?
   
   ```
   Execute InsertIntoHadoopFsRelationCommand   <- Spark: SaveMode, catalog, 
commitJob, _SUCCESS
   +- CometWriteFiles                          <- Comet: native per-task write 
only
      +- CometNativeScan ...
   ```
   
   - **New** `CometWriteFilesExec` overriding `doExecuteWrite`, mirroring
     `FileFormatWriter.executeTask` for the parts Comet must do itself: build 
the
     `TaskAttemptContext`, ask the commit protocol for a path, run the native 
writer,
     drive the stats trackers, commit or abort. Plus the `CometWriteFiles` 
serde and a
     two-line `ShimCometWriteFilesExec` in `spark-4.x` / `spark-3.x`.
   - **Deleted** `CometNativeWriteExec.scala` and 
`CometDataWritingCommand.scala` (594
     lines). Net −549 lines.
   - File paths come from `FileCommitProtocol.newTaskTempFile` and are used 
**verbatim**,
     so names match Spark's `part-<id>-<uuid>-c000.<codec>.parquet` and 
committers that
     track individual files (S3A magic, streaming manifest) work. Previously 
only
     `.getParent` was kept and the native writer invented its own names.
   - Column names come from `WriteJobDescription.dataColumns` rather than the 
query
     output, so `INSERT INTO t SELECT a+1` writes the target column's name 
(#3426).
   - Byte/row counts come from `BasicWriteTaskStatsTracker`, which stats files 
through the
     `FileSystem` API and is therefore correct on HDFS. The native writer's
     `std::fs::metadata` call reported `0` there. `CometMetricNode`'s 
now-redundant
     `reportNativeWriteOutputMetrics` is removed.
   - Proto: `work_dir` / `job_id` / `task_attempt_id` removed (reserved); 
`output_path` is
     now the exact file to write, set per task.
   - Opt-in moves to `spark.comet.operator.WriteFilesExec.allowIncompatible`, 
with the old
     `DataWritingCommandExec` key kept via `withAlternative`.
   
   ### Fixed along the way
   
   AQE re-plans the write command's child and **re-inserts a `WriteFilesExec`** 
above the
   node Comet already converted. Without a guard that produced nested native 
writes — the
   data written twice, and the inner node's empty output read as a zero-column 
schema.
   `CometExecRule` now collapses the redundant node. The
   `basic parquet write with repartition` test catches this.
   
   ## How are these changes tested?
   
   - `CometParquetWriterSuite`: **33/33** on Spark 4.1 — the 30 existing tests 
plus three
     new regression tests for `_SUCCESS` (#2985), Spark-compatible file naming, 
and
     `INSERT INTO ... SELECT` visibility (#3521). Suite-level 
`assume(isSpark40Plus)`.
   - `CometTaskMetricsSuite`: 6/6. Note this suite's native-write test was 
pinned to the
     old operator's config key, so it would have silently fallen back to 
Spark's writer
     and still passed; it is repointed and now genuinely exercises the native 
path.
   - Regression sweep: `CometExecSuite` (143), `CometExpressionSuite` +
     `CometAggregateSuite` + `CometFuzzTestSuite` (273) — all pass.
   - Compiles against Spark 3.4, 3.5, 4.0 and 4.1. `cargo check` and the native
     `parquet_writer` unit tests pass.
   
   ## Known limitation
   
   `WriteTaskStatsTracker.newRow(filePath, row)` is a per-row callback. Comet 
has columnar
   batches, so rather than materializing every row just to hand it straight 
back,
   `recordRows` passes `InternalRow.empty` and feeds only the count. That is 
exactly right
   for `BasicWriteTaskStatsTracker`, which ignores the row argument, but a 
third-party
   tracker inspecting row contents would see empty rows — so that case logs a 
warning
   rather than silently reporting wrong statistics. A plan-time guard isn't 
possible
   because `statsTrackers` only exists at execution time.
   
   ## Follow-ups
   
   Independent of this change and the next highest-value work, since the Spark 
default is
   affected: full `WriterProperties` (block/page size, dictionary, writer 
version), INT96
   timestamps (#3425 — `spark.sql.parquet.outputTimestampType` defaults to 
`INT96` and we
   write INT64 micros), the four footer metadata keys (#3427 — `legacyINT96` 
and `timeZone`
   drive rebase decisions on read, so omitting them is a correctness risk), 
field IDs, and
   Catalyst nullability. Then partitioned (#3193) → bucketed (#3194) → object 
stores.
   


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