andygrove opened a new pull request, #5763:
URL: https://github.com/apache/datafusion-comet/pull/5763
## Which issue does this PR close?
Part of #2967 and #1625. Restructures the native write path on Spark 4.0+ 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.
Supersedes #5293, which made the same change but removed the Spark 3.x
writer along
the way. That regression is what held #5293 back, so this version is purely
additive:
Spark 3.4/3.5 keep the existing native writer, unchanged.
## Rationale for this change
Native writes replace the whole `DataWritingCommandExec`, which means
`InsertIntoHadoopFsRelationCommand.run` never runs. Everything that method
does has to
be re-implemented inside `CometNativeWriteExec`: a hardcoded
`SQLHadoopMapReduceCommitProtocol` (so
`spark.sql.sources.commitProtocolClass` is
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 4.0 added the right seam. `V1WritesUtils.getWriteFilesOpt` matches the
`WriteFilesExecBase` **trait** there (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 this is additive rather than a replacement.** 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. So `CometDataWritingCommand` and
`CometNativeWriteExec` stay
exactly as they are and remain the 3.4/3.5 path. `CometExecRule` picks the
path by
Spark version and the two never both fire. The 3.x path goes away with 3.x
support.
## What changes are included in this PR?
Spark 4.0+:
```
Execute InsertIntoHadoopFsRelationCommand <- Spark: SaveMode, catalog,
commitJob, _SUCCESS
+- CometWriteFiles <- Comet: native per-task write
only
+- CometNativeScan ...
```
Spark 3.4/3.5 is unchanged:
```
CometNativeWrite <- Comet: the whole write
+- 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`.
- **Nothing is deleted.** `CometNativeWriteExec`, `CometDataWritingCommand`,
`CometMetricNode.reportNativeWriteOutputMetrics` and the
`EliminateRedundantTransitions` rule for native writes all remain and
serve 3.x.
- 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. The 3.x
writer keeps
inventing its own names.
- Column names, nullability and Parquet field IDs 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 reports `0` there.
- Proto: `ParquetWriter.work_dir` becomes genuinely optional. When it is set
(3.x) the
native writer derives the file name from it as before; when it is unset
(4.0+),
`output_path` is the exact file to write and is used verbatim.
`output_path` was
already unused on the 3.x path, so no field changes meaning for an
existing plan.
- On 4.0+ the opt-in moves to
`spark.comet.operator.WriteFilesExec.allowIncompatible`,
with the old `DataWritingCommandExec` key kept as a deprecated alternative.
`CometConf.isOperatorAllowIncompat` now resolves alternatives; the
planner's by-name
lookup previously bypassed the `ConfigEntry`, so an old key would have
read `true`
from the entry while the planner saw `false`.
- `WriteFilesExec` declines dynamic partition overwrite (it is always a
partitioned
write) and `spark.sql.files.maxRecordsPerFile`, which Spark's own writer
uses to roll
a new file every N rows.
### Fixed along the way
AQE re-plans the write command's child and **re-inserts a `WriteFilesExec`**
above the
node Comet already converted. On the 3.x path that needs an explicit guard in
`CometExecRule` (still present). Leaving `DataWritingCommandExec` in place
on 4.0+
means the situation cannot arise there.
## How are these changes tested?
- `CometParquetWriterSuite`: **41/41** on Spark 4.0 and 4.1: the 33 existing
tests plus
eight new ones for `_SUCCESS` (#2985), Spark-compatible file naming,
`INSERT INTO ... SELECT` visibility (#3521), dynamic-overwrite fallback,
the
`maxRecordsPerFile` fallback (both the write option and the conf,
verifying Spark's
writer rolls 10 files), the schema-only empty-input write (SPARK-23271),
task abort
and retry through an injected failing commit protocol, and the deprecated
opt-in key.
The eight new tests `assume(isSpark40Plus)`.
- `CometParquetWriterSuite` on **Spark 3.4 (32/32 + 9 skipped)** and **3.5
(33/33 + 8
skipped)**: every pre-existing test still passes on the 3.x writer, which
is the point
of keeping it.
- `CometTaskMetricsSuite`: 15/15 on both 3.5 and 4.1. The suite's
native-write test now
picks the version-appropriate opt-in key, so it genuinely exercises the
native path on
both.
- Regression sweep on 4.1: `CometExecSuite` (144),
`CometFallbackInvarianceSuite` (6),
`CometPublicApiSuite` (1).
- Native: new `parquet_writer` unit test asserting the writer uses a
commit-protocol-chosen path verbatim (and that a non-zero partition id
does not leak
into the name). `cargo test -p datafusion-comet parquet_writer` and
`cargo clippy --all-targets --workspace -- -D warnings` pass.
- Compiles and test-compiles against Spark 3.4, 3.5, 4.0, 4.1 and 4.2.
## 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), and the four footer metadata keys (#3427: `legacyINT96`
and
`timeZone` drive rebase decisions on read, so omitting them is a correctness
risk).
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]