Skip to content

feat: support map inputs for explode - #32

Open
rich7420 wants to merge 62 commits into
mainfrom
feat/2837-native-map-explode
Open

rich7420 wants to merge 62 commits into
mainfrom
feat/2837-native-map-explode

Conversation

@rich7420

@rich7420 rich7420 commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Which issue does this PR close?

CI mirror of upstream apache/datafusion-comet#6107, addressing apache#2837. This fork PR does not close or merge the upstream issue.

Rationale for this change

Map inputs to explode and posexplode currently fall back to Spark.

What changes are included in this PR?

Support maps in all four explode variants by sharing their entry buffers with the existing unnest path. Preserve field metadata, entry order and outer NULL rows. Add regression tests and update support documentation.

How are these changes tested?

Local verification passed 18 Rust tests and 52 targeted tests on each of Spark 3.5.9 and 4.1.3, covering native execution, NULL/empty maps, nested inputs, sliced arrays and batch boundaries.

Upstream CI passed at 9601e81ba, including Required Checks. The corresponding fork run is not fully green: Spark 3.4 scans failed a file without ids next to a file with ids is checked on its own in ParquetReadSuite (544 passed, one failed in that job). Its missing-field-ID error assertion failed. The relationship to this map-explode change has not been established; do not treat the run as a complete cross-profile pass until the failure is classified and resolved.

@rich7420 rich7420 added run-all-spark-profiles Run Comet test suites against all supported Spark profiles run-spark-4.1-tests Run Spark 4.1 SQL tests across all modules labels Sep 21, 2026
@rich7420
rich7420 force-pushed the feat/2837-native-map-explode branch from 76768f2 to 9601e81 Compare September 28, 2026 14:50
rich7420 and others added 7 commits October 2, 2026 18:05
Cover JVM-fed maps, nullable Boolean batch boundaries, all-NULL/empty outer batches, native map producers, and stored floating-point keys. Require native generators and matching Spark results and schemas. Benchmark map variants and nested map/array inputs with sinks that consume keys and positions. Remove the unrelated Parquet field-ID assertion change so it can be reviewed independently.
Keep input arrays as temporary placeholders until generated columns replace them. Build take indices only when a carried column consumes them. Cover recursive unnest with and without carried columns and both drop and outer null handling. Preserve the existing bounded map and array execution path.
@rich7420
rich7420 marked this pull request as ready for review October 3, 2026 16:36
rich7420 and others added 9 commits October 5, 2026 13:43
* feat: convert OneRowRelation to Arrow by default

Turn on spark.comet.convert.oneRowRelation.enabled by default. The row a
query without a FROM clause reads has no columns, so converting it costs
almost nothing, and it lets the operators above it run in Comet. Where
the parent cannot run in Comet, EliminateRedundantTransitions drops the
conversion again, so no extra transition is added.

On Spark 4.2 the TPC-DS q77a golden now records the remaining blocker,
the struct-typed scalar subquery result the projection above each
OneRowRelation reads (apache#5834), instead of the unsupported leaf.

* fix: handle expressions exposed by converting OneRowRelation by default

Folded array literals are UnsafeArrayData and have no backing array, so
serialize them through toArray. might_contain accepts an untyped NULL
for either argument. parse_url raises Spark's INVALID_URL error under
ANSI instead of a generic native error. An Expand with no output columns
falls back to Spark.

* fix: decline codegen dispatch for expressions that contain a subquery

* test: adapt Spark explain tests to the Comet plan for OneRowRelation queries
…eException (apache#6243)

* fix: rethrow the exception a JVM input throws instead of a CometNativeException

A native plan pulls each batch of a JVM input through the Arrow C stream
exported by CometArrowStream.stream. When the JVM producer throws while
building a batch, Arrow Java's exported stream catches the throwable and
hands native only its text, so the task failed with a CometNativeException
("C Data interface error: ...") instead of the exception Spark throws. For
inputs of Arrow-backed batches this happened past the first batch only,
because schema reconciliation reads the first one on the JVM. Row inputs
through CometSparkToColumnarExec hit it on every batch.

CometArrowStream.stream now wraps each exported reader to keep the first
throwable its loadNextBatch throws, keyed by the exported stream until the
task completes. When executePlan fails, CometExecIterator rethrows that
throwable if one of its input streams recorded one.

Closes apache#6234.

* fix: drop unneeded string interpolator flagged by scalafix

* refactor: match the recorded input failure once, share the failed-write test helper

CometExecIterator looked up the recorded input failure twice, in the guard
and in the body of the catch case. An extractor does it once, and the
comment at the catch site shrinks to a line now that
CometArrowStream.inputFailure carries the explanation.

The native-versus-JVM failed-write scaffolding in CometIcebergWriteActionSuite
was copied three times. failedWrites runs a write against a native-writer
table and a JVM-writer table, checks that the native run went through
CometIcebergWriteExec, and returns both failures.
… review skills (apache#6340)

* docs: check timezone handling against the contributor guide in the PR review skills

Point the review skills at docs/source/contributor-guide/timezones.md
(added in apache#6337) when a PR deals with timestamps or the session timezone.

- review-comet-pr: read the page up front, and a cross-cutting
  "Timestamps and timezones" check covering the UTC label invariant,
  where the timezone comes from, Etc/UTC, NTZ values, tests, and which
  parts of the page a PR can make stale.
- review-comet-expression-pr: the page in the guide table, serde and
  kernel checklist items, the page's testing guidance, and a common
  finding for mislabelled timestamp results.
- review-comet-ffi-pr: timestamps cross the boundary unconverted and
  new producers label with NATIVE_TIMEZONE.
- review-comet-iceberg-write-pr: timestamp partition values are UTC,
  and iceberg-rust's years/months depend on the +00:00 relabel.

* docs: update the timezone review notes for CometTimeZone and the Iceberg partition kernels
)

* feat: charge native write buffers to the task's memory pool

Neither native writer reserved what it buffers, so a native Parquet or Iceberg
write held its row groups in memory that no budget accounted for. A fanout
Iceberg write multiplies that by the number of partitions a task writes, and
when it outgrew the executor's memory overhead the container was killed.

Both writers now register one memory consumer per task and resize it after
every batch. ParquetWriterExec reserves parquet-rs's estimate of its
in-progress row group. The Iceberg writer wraps iceberg-rust's
ParquetWriterBuilder so that every open file reports what it holds, capped at
the row-group size because iceberg-rust exposes only the bytes written plus
the in-progress row group, and adds the rows its pacers hold back. Neither
writer can spill, so a resize the pool refuses fails the task with a native
out-of-memory error that Spark can retry.

Closes apache#5648.
Closes apache#6115.

* fix: release an Iceberg file's reservation once its row groups leave memory

A data file's share was its written size capped at one row group, so every fanout file that had
flushed stayed charged a whole row group until it closed. A local file keeps nothing it has
flushed, so a pool of eight 128 KiB row groups failed a twelve-partition write whose files held
almost nothing.

Each file's OutputFile is now wrapped so that its writer counts the bytes that leave memory on
their way to storage, and the file reports what it has written less those. A local file releases
a row group as it is handed on. S3 and GCS do not: OpenDAL collects what a file flushes into parts
of at least 5 MiB and holds each part until the next one is complete or the file closes, so a file
there keeps what it has flushed since its last upload charged.
)

Bumps [rustls](https://github.com/rustls/rustls) from 0.23.44 to 0.23.45.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](rustls/rustls@v/0.23.44...v/0.23.45)

---
updated-dependencies:
- dependency-name: rustls
  dependency-version: 0.23.45
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
apache#5956)

* feat: Use DataFusion date_trunc for compatible trunc and date_trunc paths

Reuse the upstream physical date_trunc for scalar formats while keeping
Comet per-row format, dictionary, and session-timezone handling, with a
fallback outside DataFusion's TimestampNanosecond range.

* perf: retain direct Date32 truncation kernel

* fix: preserve timezone semantics for minute truncation

* fix: take the earlier midnight for non-UTC coarse truncation

* fix: align timestamp truncation with Spark DST semantics and optimize UTC

* fix: preserve timestamp truncation nulls and stabilize DST tests

* fix: truncate extreme UTC timestamps with integer calendar arithmetic

* fix: handle dictionary and fine-grained timestamp truncation boundaries

* perf: Avoid unnecessary dictionary key scans in timestamp truncation
…he codegen dispatcher (apache#6714)

* perf: skip Spark's encoders for boxed primitive Scala UDF values in the codegen dispatcher

A typed Scala UDF converts a boxed primitive parameter (`java.lang.Long`, ...) through its
input encoder and a boxed primitive result through its output encoder, each a projection per
row. The kernel now drops those encoders when their expression is exactly the boxing or
unboxing call, so `ScalaUDF` passes the value through `CatalystTypeConverters` unchanged, as
Spark 4 does for a Java UDF. Every other encoder is left alone.

Closes apache#6706.

* Compare boxed encoders with the trees Spark's builders produce

Check each encoder by building the expected tree with Spark's own
`DeserializerBuildHelper` and `SerializerBuildHelper` builders and comparing with `==`, in
place of field-by-field matching. Drop parameter encoders with `filterNot` like the result's,
and always copy the `ScalaUDF`, since an equal copy leaves its parents untouched.

In the tests, register the shared UDFs once, pass vector classes directly, drop two no-op
casts, and run the mixed-parameter calls as one query.
viirya and others added 30 commits October 7, 2026 23:19
…patcher (apache#6471)

* fix: Route collated array element comparisons through the codegen dispatcher

array_contains, arrays_overlap, array_distinct and array_union compare
array elements by raw bytes in their native kernels, so they returned
wrong results for strings with a non-UTF8_BINARY collation (for example
UTF8_LCASE or UTF8_BINARY_RTRIM) when a dispatched collated cast fed
them.

Report these cases as Incompatible and mix in CodegenDispatchFallback so
they run Spark's collation-aware comparison through the JVM codegen
dispatcher by default. ArrayDistinct gets its own serde for this.

* docs: Mark collation-routed array functions as Hybrid in expressions.md
…apache#6730)

* perf: convert Spark's unsafe rows to Arrow straight from their memory

ArrowWriter's row path read every row through Spark's generic getters. A string
went through a UTF8String and Arrow's per-value setter, each array, map and
struct allocated a view, and every element took a generic write with two or
three virtual or interface calls.

Unsafe rows now take a path of their own, and unsafe arrays are written in bulk:

- Fixed-width values are read from their slot at the vector's width, and arrays
  of them are copied in one block, with the validity inverted from the array's
  null words.
- Strings and binaries are copied from the offset and size in their slot, short
  ones a word at a time. A negative size or an offset overflow is rejected
  before anything is written or allocated, for a value and for every element of
  an array.
- Decimals up to 18 digits are written from the unscaled long, and wider ones
  are range-checked in place.
- Arrays, maps and structs point one reused view at each value.

Closes apache#6721.

* Simplify the unsafe-row writers

- Use Arrow's fillEmpties and setValueLengthSafe in place of hand-written hole
  filling and buffer growth, and share one variable-width overflow check.
- Read the fixed-width vector through its field in every per-value method,
  and have the base defaults delegate to write.
- Move the columnar code StringWriter and BinaryWriter shared into their base.
- Write unsafe arrays of decimals up to 18 digits from the unscaled long,
  range-checked as UnsafeArrayData.getDecimal checks it.
- Mark map entries valid with one masked write rather than a write per bit,
  and call setOne only for long runs.
- Share the tests' reject, root-comparison and fill helpers.

* test: check that a decimal past its precision in an unsafe array still fails

Reads an unsafe array of decimals at a narrower precision than it was
written with, holding 10^p or -10^p, for both the unscaled long and the
unscaled bytes.

* test: write an unsafe array of each primitive type

Each of the twelve vector types whose unsafe array elements are copied in
one block now reaches that copy, at every element width. Only int and long
arrays did before.

* fix: give each nested value a view of its own again

The array, map and struct writers pointed one reused unsafe view at every
value they read. Each view then kept the last row it read reachable after
that row's values were copied to Arrow, along with the row's memory, until
the writer was dropped. They now read each value through Spark's getters,
which allocate a view per value as the generic path does. That keeps about
nine tenths of the gain on nested types.

A test walks the writers' fields after each row and checks that the row's
memory is not reachable from them.

* refactor: share the variable-width growth step between the append paths

writeVariableWidth, writeDictionaryVariableWidth and the unsafe array
path each filled the offsets of values skipped since the last one set and
grew the validity and offset buffers. They now call reserveValues, next to
reserveData.

A new test appends a dictionary-encoded string field as a column after
its struct took the row path, which the dictionary path's growth step had
no test for. The array of each primitive type no longer repeats the two
that nestedTypes lists.
…pache#6740)

* fix: match Spark 4.2 date_trunc overflow for SECOND and MILLISECOND

Spark 4.2.0 (SPARK-56663) raises `long overflow` when SECOND or
MILLISECOND truncation falls below the smallest timestamp, where earlier
versions wrap the Long subtraction. The serde now tells the native
kernel which behavior to follow, and the wrapping fixture from apache#5956 is
split into a 4.1-and-earlier file and a 4.2 file.

* perf: keep values-only truncation for safe non-UTC SECOND/MILLISECOND dictionaries

SECOND and MILLISECOND ignore the timezone, so when they do not wrap they
need key masking only for a value near the lower bound. Previously a
non-UTC dictionary fell through to the unconditional non-UTC masking
branch and scanned every key.
…pache#6778)

* docs: add an FAQ page with an answer on vectorized Java/Scala UDFs

The About section gets a Frequently Asked Questions page. Its first
question is whether Comet plans an API for vectorized Java/Scala UDFs
like the Rust UDF API. The answer gives what the prototype in apache#6697
found: for simple functions the vectorized form saved about 3 ns per
row once the code generator's overheads were removed, which did not
justify the API's cost to users. It also says where a vectorized
function could still pay off, and points to apache#6694 for those use cases.

* docs: shorten the vectorized UDF FAQ answer
* fix: harden Iceberg delete-file reflection handling

* test: cover Iceberg delete-file serde failures

---------

Co-authored-by: Andy Grove <agrove@apache.org>
… is too large (apache#6384)

* perf: keep the sort-merge join when the forced hash join's build side is too large

With spark.comet.exec.forceShuffledHashJoin on, RewriteJoin turned every
eligible SortMergeJoinExec into a shuffled hash join and read the build side's
size estimate only to pick the build side. DataFusion's hash join has to hold
that side in memory, so one large join failed the task while the rest of the
query would have gained from the rewrite.

The rule now rewrites only when the build side's estimate is under a limit and
otherwise keeps the SortMergeJoinExec, which still runs natively, recording
the reason as plan info. The limit is Spark's own rule for choosing a shuffled
hash join, spark.sql.autoBroadcastJoinThreshold times the initial shuffle
partition count, with Spark's 10 MB default threshold standing in when
broadcasts are disabled. spark.comet.exec.forceShuffledHashJoin.maxBuildSize
sets a fixed limit, and a non-positive value removes it, which is what the
hash join benchmark configurations and the macOS benchmarking guide now pin so
they keep measuring every join.

* fix: restore the sort a kept sort-merge join needs after a hash join rewrite

When RewriteJoin keeps a sort-merge join above a join it has already
turned into a hash join on the same key, the kept join reads unsorted
input. EnsureRequirements placed no sort between the two because the
inner sort-merge join's ordering satisfied the outer one, and the
rewrite then removed the inner join's sorts. The native sort-merge join
does not check its input ordering, so matches were silently dropped.

After the rewrite, add a local SortExec wherever a child no longer
satisfies its parent's required ordering, the same check
EnsureRequirements makes. This covers joins kept by the build-size
limit and the LeftSemi and ExistenceJoin joins the rewrite already
keeps, which lost rows the same way.

* test: cover a window over a join the hash join rewrite reordered

A window partitioned by the join key gets no sort of its own, because the
sort-merge join's output ordering already satisfies it. Once RewriteJoin
turns that join into a hash join, the window needs the ordering restored,
or the native window fails its partition-ordering assertion. The test runs
with AQE on and off and checks the join became a hash join.
…#4746)

* fix: honor Spark’s commit protocol in Spark 3.x native writes

* ci: register legacy native write commit protocol tests

* refactor: drop the native Parquet writer's work_dir file naming

Both native writers now take the exact file name from Spark's commit
protocol, so nothing sets work_dir, job_id or task_attempt_id any more.
Remove the Some(work_dir) arm from ParquetWriterExec::execute, reserve
the three proto fields, and correct the comments that still described
the Spark 3.x writer naming its own files.

* fix: handle empty inputs in the Spark 3.x native writer

Mirror CometWriteFilesExec and FileFormatWriter: swap a zero-partition
child for a single empty partition so the output still carries a schema
(SPARK-23271), and only write a file from partition 0 or a partition
that has rows. Move createTaskContext to the companion object so the
task closure no longer captures the exec node and its child subtree.
The SPARK-23271 and empty-partition tests now run on Spark 3.x too.

* fix: match Spark 3.x write admission and cover empty scans

* docs: say native writes over an empty relation stay native

With the empty-relation guard gone from CometDataWritingCommand, the user
guide and the CometWriteFilesExec comment still described Parquet writes
over an empty relation falling back to Spark's writer. Both native write
paths now swap a zero-partition input for one empty partition, so such a
write stays native and still leaves a readable schema-only file.

* fix: take the Spark 3.x native write codec from the job configuration

The Spark 3.x task named its file after the codec in the job configuration,
because getFileExtension reads it from there. The native writer used the
codec that convert resolved at planning time. The two agree today only
because both copy the precedence rule of ParquetOptions.

Read the codec with CodecConfig.from(taskContext) next to setOutputPath, as
CometWriteFilesExec.executeTask does, so the file name and the footer agree
by construction. A new CometNativeWriteSuite test sets a job codec that
differs from the planned one and checks the name and the footer. The
mixed-case codec test now checks the file name on Spark 3.x too.

* test: make the injected cleanup failure throw from SQLMetric.add too

Since apache#6416, CometMetricNode reports a sum metric with add instead of
set. The test metric threw only from set, so the two cleanup-failure
tests in CometNativeWriteSuite stopped injecting their failure and
failed on Spark 3.4 and 3.5.

* ci: pre-compile Spark SQL test classes in the manual writer workflow

The Parquet writer test step caps the sbt heap at 1 GB for the forked
test JVM, but this workflow compiles Spark first, and sbt ran out of heap
while it compiled sql/core. Compile the SQL test classes in an earlier
step with a 3 GB heap. This branch had the same step in July, before a
history rewrite dropped it.

* test: build CometNativeWriteExec with the merged constructor

The merge with main brings in two callers written against the old constructor. apache#5957's
RevertNativeForTransitionHeavyStagesSuite relied on the removed committer and jobTrackerID
defaults, and apache#6247's write_reserving_from helper still passed the removed work_dir, job_id and
task_attempt_id arguments to ParquetWriterExec::try_new. CleanupFailingNativeWriteExec also needs
apache#5957's originalPlan argument.

---------

Co-authored-by: Andy Grove <agrove@apache.org>
… the task's entry (apache#6403)

* fix: retry a shuffle page allocation that Spark failed after dropping the task's entry

Spark's ExecutionMemoryPool registers a task's memoryForTask entry once, before
its wait loop, and reads it on every pass, while releaseMemory removes the
entry when the task's balance reaches zero. When the JVM shuffle allocator's
page or pointer array request was parked below the task's minimum share and
Comet's native memory consumer released the task's last bytes, the request
woke to NoSuchElementException "key not found: <taskAttemptId>" and failed the
task, because the shuffle writers only expect SparkOutOfMemoryError.

CometUnifiedShuffleMemoryAllocator now retries allocate and allocateArray when
Spark throws that exception from ExecutionMemoryPool. The retry registers the
task again and waits as the first attempt would have. Nothing is counted or
allocated before the exception, so retrying cannot leak. After three attempts
it refuses the request with the same SparkOutOfMemoryError it uses for a
refused page, with the last exception as its cause. The Spark issue is
SPARK-59827.

* docs: say which callers the page allocation retry covers

Only the shuffle allocator's callers are guarded; Spark's own operators in the
task can still hit the removed entry until Spark re-registers a waiting task
(apache/spark#59103).

* docs: point the shuffle allocator retry at SPARK-59444

apache/spark#59103 (SPARK-59827) was closed as a duplicate of
apache/spark#58747 (SPARK-59444), which fixes the same race in
ExecutionMemoryPool, so link that issue and PR instead.
Co-authored-by: Andy Grove <agrove@apache.org>
* docs: expand the FAQ and refresh the Gluten comparison

* docs: correct the shuffle manager and PyArrow UDF answers in the FAQ

When spark.shuffle.manager does not name Comet's shuffle manager and
spark.comet.shuffle.enabled is true (the default), Comet disables itself
instead of only running shuffles in Spark. Say so, and name the setting
that keeps Comet enabled.

State that the mapInArrow and mapInPandas acceleration needs Spark 4.0 or
later, and name spark.comet.exec.pyarrowUDF.enabled.
…he operands (apache#6691)

* perf: compare floats in Spark's order without normalizing copies of the operands

Since apache#6447 every native comparison of FLOAT or DOUBLE operands follows
Spark's SQL ordering, but it got there by normalizing each operand that
is not a literal into a copy and running Arrow's kernels on the copies.
For arrays and structs, <=>, <, <=, > and >= deep-copied both operands in
every batch.

A new SparkComparison expression compares in place instead. Flat floats
go through branch-free kernels over the value buffers, column against
column or against a scalar, for =, <>, <, <=, >, >=, <=> and IS DISTINCT
FROM. Lists and structs use spark_comparator for the ordering operators
and spark_equality for the null-safe ones. The nested comparisons, nested
= and <> included, now compare only the rows where neither side is null,
iterating the validity bitmap, rather than testing both null buffers on
every row.

Planning is unchanged: a literal is still normalized while the plan is
built, a float column compared with a literal in a scan's data filters is
still a plain BinaryExpr so that Parquet pruning recognizes it, and
operands of other types, such as dictionary-encoded floats, keep the
normalize-then-compare path.

* fix: split float comparison bitmaps with as_chunks for clippy 1.99

CI's rust-test job lints with the current stable toolchain, Rust 1.99.0,
whose clippy adds chunks_exact_to_as_chunks. It rejected the three
chunks_exact(64) calls in pack and pack_pairs under -D warnings, so the
job failed before any test ran. as_chunks::<64>() yields the same 64-value
chunks as arrays, which also drops the try_into().unwrap() conversions.

* perf: keep CASE WHEN eager when a branch compares floats

`is_infallible` in `case_when.rs` accepted the `BinaryExpr` of normalized
operands that a float comparison used to be, but not the `SparkComparison`
that replaced it. A CASE WHEN or IF with a float comparison in a later WHEN
or in a branch value therefore fell back from eager to lazy evaluation.

`SparkComparison::is_infallible` now reports a comparison of two operands of
the same type as unable to fail, which covers every flat Float32 or Float64
comparison. The nested comparators reject a dictionary-encoded leaf against
a plain one, which the logical type check in `spark_comparison` lets through,
so a nested comparison counts only when the types are equal.

The `is_infallible` test builds comparisons through `spark_comparison`, so a
change in what the planner builds shows up there. The contributor guide
describes comparing in place and lists the float comparison kernels.
apache#6715)

* perf: run Spark's null guard for a Scala UDF inside the codegen kernel

Spark's HandleNullInputsForUDF rule wraps a Scala UDF that has a primitive
parameter over a nullable input as
`if (isnull(a) or ...) null else f(knownnotnull(a), ...)`. The serde sent only
the UDF to the codegen dispatcher and ran the If natively, as a CASE that
filters the batch for each branch and merges the results, which cost more than
the call it protects.

CometIf now recognizes that exact shape, including the `false` form that
ReplaceNullWithFalseInPredicate leaves in predicates, and sends the whole If to
the dispatcher, so the null check is a branch in the kernel's loop. When the
dispatcher cannot take the whole guard, the If is converted natively as before
and no fallback reason is recorded against it.

* fix: keep a null guard native when an expression in it is disabled

Converting the guard natively checks spark.comet.expression.<name>.enabled
on the UDF and on every node of the predicate, and a disabled one takes the
projection to Spark. Dispatching the guard whole skipped those checks, so
with ScalaUDF, IsNull, Or, or an expression in a guarded argument disabled,
the projection stayed in Comet. emitNullGuardDispatch now declines the guard
in that case, and the native conversion records the same fallback reason as
before.

* test: bind the dispatched null guard in the Scala UDF dispatcher benchmark

CometScalaUDFDispatchBenchmark, added by apache#6709, said that Spark's null
check around a UDF with a primitive parameter runs natively around the
dispatched call, and its JVM layer cases bound the ScalaUDF alone as the
subtree the serde would dispatch. The check now runs in the kernel with
the call. The scaladoc says so, and the JVM layer cases bind the
projected expression, which for a primitive parameter is the check
together with the call.

* perf: check a repeated argument once when matching a UDF's null guard

BooleanSimplification drops a repeated isnull from the predicate that
HandleNullInputsForUDF builds, so with a column at two primitive
positions, f(a, a) optimizes to if (isnull(a)) null else
f(knownnotnull(a), knownnotnull(a)). The predicate rebuilt for the match
kept every repeat, so those guards stayed native. It is now rebuilt from
an ExpressionSet of the guarded arguments. ExpressionSet keeps every
nondeterministic argument, so a guard over one still stays native.

The first null guard test also covers addBoth(a, a), addThree(a, b, a)
and addBoth(a, 1L), the last with an argument that cannot be null.
…el (apache#6354)

* fix: truncate per-row date_trunc formats with the literal-format kernel

* fix: pass the SECOND/MILLISECOND overflow policy through the format-column path

* fix: handle a NULL format and a literal timestamp with a format column in date_trunc
…-point mode (apache#6735)

* feat: run nullable nested float sort keys natively in strict floating-point mode

Strict floating-point mode declined a sort or window key that nests floats
in an array or struct whose type can hold a null element or field, because
the native sort placed such a null by the key's null order (apache#6476) and a
RANGE frame ordered it above every other value (apache#6477). apache#6683 and apache#6684 now
decline exactly those shapes in every mode: ASC NULLS LAST or DESC NULLS
FIRST on such a key, and a RANGE frame that has to find a row's peers over
one. What strict mode still declined beyond them, the default null orders,
TopK, ranks, rank limits, ROWS frames and RANGE frames bounded only by the
partition, already matches Spark, so the strict-only decline goes.

The strict fixture now runs those shapes natively under strict mode, with a
null element, a -0.0 and a NaN with the sign bit set among the keys, and
keeps expect_fallback for the shapes the general guards decline.
CometWindowExecSuite expects a native rank limit in strict mode, and the
nested strict sort test in CometExpressionSuite adds a key that can hold a
null. The floating-point compatibility page, the operator tuning page and
the spark.comet.exec.strictFloatingPoint doc no longer describe a strict
exception.

* test: expect native strict sort over nullable nested float keys

* test: sort a real null element in the strict nested float sort test

The IF key in the strict nested sort test never produced a null, because
id runs from 0 to 4. It now nulls row 4's element, so the test checks
that a null element sorts first in strict mode, ahead of the signed
zeros and NaN payloads, with that row's bits check skipped.

Also update two comments that still described the strict-only decline:
nested_float_order_keys.sql and CometWindowGroupLimitExec now say that
strict mode runs these keys natively, and that CometSortOrder declines
ASC NULLS LAST and DESC NULLS FIRST on them in every mode.
* perf: project cached batches by buffer selection, prune on collated strings

Follow-up to apache#5051, applying items from apache#5487.

Replace the per-column Arrow IPC stream layout of `CometCachedBatch` with a
single encapsulated IPC record batch message per cached batch, carrying no
Schema message and no end-of-stream marker. The reader rebuilds the schema
from the cached relation's attributes, so a wide relation no longer repeats
the same schema bytes once per cached batch.

Compression moves from a whole-payload Spark codec to Arrow's per-buffer IPC
compression. That is what makes projection cheap: the message metadata records
every buffer's offset and length in the body, so `CachedBatchIpc.readProjected`
copies out only the byte ranges of the columns a scan selected and decompresses
just those. This subsumes the separate "drop the schema message" item, since
there is no longer a per-column stream to frame.

Dictionary-encoded columns are decoded before being stored: a payload with no
schema message cannot describe a dictionary encoding.

The codec defaults to zstd, and lz4 is deliberately not offered. Arrow's lz4 is
commons-compress's pure-Java implementation, unrelated to the JNI-accelerated
lz4-java behind `spark.io.compression.codec`. Over a 200k-row six-column
relation it measured 205s to write against 347ms for zstd, while also producing
larger output, so no workload prefers it. zstd also beats storing batches
uncompressed on both axes (347ms and 2 MiB against 1743ms and 13 MiB), because
the bytes it saves cost more to copy and store than compressing them costs.

Decompression is done here rather than left to `VectorLoader`, which leaks:
`VectorLoader.loadBuffers` collects a field's decompressed buffers into a local
list and releases them only after the whole field loads, so a buffer that fails
to decompress strands every buffer of that field decompressed before it. A
string column reaches this, its offsets buffer decompressing before its data
buffer throws.

Also track statistics bounds for collated string columns, comparing with the
collation's own ordering through a new `CometTypeShim.compareStrings`. Matching
the bare `StringType` object excluded collated columns, which then got null
bounds and no pruning.

Benchmark over a 5M-row six-column relation, keeping the cached scan native
against falling back to a Spark cache scan and converting: 1.3x on a repeated
scan, 1.3x on a narrow projection and 2.3x on a full projection.

* refactor: use Spark's interpreted ordering for bounds, hoist projection layout

Cleanup pass over the cache format change. No behaviour change.

Drop the `compareStrings` shim in favour of `TypeUtils.getInterpretedOrdering`.
That method is public with the same signature on every supported Spark version,
and on Spark 4 it resolves a `StringType` through
`CollationFactory.fetchCollation(collationId).comparator` -- the comparison the
shim was reaching for. So the collation awareness comes from Spark itself and
the shim, its Spark 3.x stub and the hand-rolled per-type `compare` all go.
The ordering is now resolved once per column per partition rather than being
re-dispatched on the `DataType` twice per row.

Build the projection's index layout once per partition instead of per batch.
The node, buffer and variadic index arithmetic is a pure function of the cached
schema and the selected columns, but it walks every field of the relation, so
recomputing it per batch made the bookkeeping O(total columns) against O(selected
columns) of useful work -- worst in the wide-relation, narrow-projection case the
format exists for. `CachedBatchIpc.Projection` now holds that layout and the
projected schema, and owns the whole decode; `ProjectedBatch` is left with
ownership only. This also puts the projected schema next to the code that packs
buffers in the same order, an invariant that previously spanned two files
unstated.

Smaller cleanups: use Arrow's `DataSizeRoundingUtil.roundUpTo8Multiple` rather
than open-coding IPC body alignment; size the serialization buffer from the
record batch's known body length instead of growing from 32 bytes; resolve
decompressors once instead of per batch; share the dictionary lookup guard
between `Utils.combineDictionaryProviders` and the cache writer; read the codec
config through one helper carrying the driver-vs-executor rationale; and collapse
the duplicated compressed-buffer predicate and scramble loop in the test helper.

Corrects two `Utils` scaladocs that still described the per-column stream format
this change replaced. Benchmark and codec figures in the docs re-measured against
the current code.

* fix: relocate the arrow-compression service file when shading

arrow-compression ships
META-INF/services/org.apache.arrow.vector.compression.CompressionCodec$Factory.
The shade plugin copies it verbatim without a ServicesResourceTransformer, so
the jar declared a provider for Spark's own unshaded Arrow interface while
naming a class that exists here only under the relocated package. Every
ServiceLoader lookup Spark's Arrow made then failed with a
ServiceConfigurationError, which took CompressionCodec.Factory's static
initializer down with it and broke unrelated Arrow IPC reads, including
mapInArrow.

Add ServicesResourceTransformer so the service file name and its contents are
both relocated. arrow-compression is the only bundled artifact that ships one.

Also drop an unused NonFatal import that scalafix flagged.

* test: drop the cache leak test that depends on zstd corruption detection

"releases its vectors when a column fails part way through" zeroed the last
16 bytes of a compressed buffer and required the read to fail. Whether that
fails is a property of the zstd runtime, not of Comet: the cached payload is
byte-identical across Spark versions, but Comet takes zstd-jni from Spark
rather than from arrow-compression, and 1.5.5 (Spark 3.4, 3.5) decodes that
frame while 1.5.7 (Spark 4.x) reports it corrupt. So the test passed on 4.x
and failed on 3.4 and 3.5.

The scenario it claimed to cover is also unreachable: CachedBatchIpc
decompresses every selected buffer before VectorLoader runs, so no content
corruption can fail part way through the load. The two remaining leak tests
corrupt a frame from its header onwards, which every zstd release rejects,
and already cover a failure at a column's first buffer and a failure after an
earlier buffer of the same column decoded.

Records the constraint on scramble so a future test does not reach for a
tail-only corruption again, and drops the now unused truncateColumn helper
and the dictionary fixture's payload argument.

* feat: enable Comet's in-memory cache by default

Flip spark.comet.exec.inMemoryCache.enabled to true so cached tables are
stored and scanned in Comet's Arrow format without an opt-in.

CometDriverPlugin.maybeSetCacheSerializer read the config out of SparkConf
with a hardcoded false default, so flipping the ConfigEntry alone would have
left the serializer uninstalled unless the user set the key explicitly. It
now falls back to the entry's own default, matching how the plugin reads
spark.comet.metrics.enabled.

Stacked on apache#5543.

* test: cover nested columns in the cached-batch projection tests and benchmark

Addresses review feedback asking whether nested data should be tested and
benchmarked.

Nested columns were already round-tripped, but only under a full projection,
which cannot see the part of the format that is nontrivial for them. A flat
column always owns one field node and two or three buffers; a nested one owns a
run as long as its subtree, and selecting every column covers the whole
sequence however it is partitioned. So the buffer-span arithmetic was only
exercised in the one shape where getting it wrong does not show.

Adds two tests over a six-column relation whose middle four columns are a
struct, an array, a map and a struct wrapping an array:

- Each column takes its turn as the sole projection while the other five are
  corrupted, so a run computed short or long is caught by reaching into a
  corrupted neighbour.
- Values are compared against the uncached query across single-column,
  paired and out-of-order projections. Row counts cannot catch a window that
  is misaligned but still decompresses, and out-of-order is the case a full
  projection cannot stand in for.

The per-column statistics test now runs over the nested relation too, since a
nested column's recorded size is the sum of its whole subtree.

Both new tests fail if fieldNodeCount stops recursing into children.

In the benchmark, adds the three projection widths over a relation of struct
columns, and asserts the width each case claims. That assertion caught the
existing "full projection (6 of 6 columns)" case reading three: count() over a
non-nullable column is rewritten to count(1) by NullPropagation, which prunes
the column out of the scan, and only k, s1 and s2 were nullable -- and those
only incidentally, because Remainder can divide by zero. Every column of both
relations is now nullable so count(c) genuinely reads c, and the documented
numbers are regenerated.

Array and map columns are left out of the benchmark deliberately: the baseline
arm needs Spark's cache scan to bridge into Comet operators, and
CometSparkToColumnarExec declines ArrayType and MapType, so for those the arm
does not exist and the two cases stop measuring the same boundary. The docs say
so rather than leaving it to be rediscovered.

* fix: drop a redundant string interpolator flagged by scalafix RedundantSyntax

* review: check the cached layout, and address the rest of the review

Reader-side: a cached payload carries no schema, so `Projection` derived
every node and buffer window from `Utils.toArrowSchema(cacheAttributes)`
with nothing checking the writer had produced that layout. `load` now
compares `nodesLength()`/`buffersLength()` against the totals
`selectedRange` already computes, before any unchecked `batch.buffers(j)`.

Writer-side: `isArrowBacked` accepts a `FixedSizeBinaryVector` for a
`BinaryType` column, which is two buffers where the reader rebuilds three,
and it answers for the top-level vector only -- so a struct of large
strings passes it and is stored with 64-bit offsets. `matchesReaderLayout`
compares the batch's Arrow types against the reader's recursively, and a
batch that disagrees takes the conversion path instead. A dictionary
column's field carries the index type, so the dictionary's field is what
is compared.

Also: an unrecognized body-compression byte is rejected rather than read
as plain bytes, `fieldVariadicCount` and the variadic plumbing are gone
(the length check covers view vectors, which the counts would not have),
`columnSizes` no longer re-walks each column's subtree, the write codec is
a case class rather than a bare tuple, the per-partition `Projection` is
lazy so a row-count-only read never builds it, `hydrateDictionaries` is
`decodeDictionaries`, `Projection` takes an `IndexedSeq`, and the stale
`readProjected` links and some over-long comments are fixed.

Tests: the two projection tests become one parameterized over both
relations, caching once and restoring the payload between columns instead
of re-caching; the two leak tests become one with two corruption points.
New tests cover the reader's layout check and the writer declining a
fixed-size-binary batch.

* review: own the write-side compression buffers, fix the activation example

Compressing through VectorUnloader leaks on the failure path: appendNodes
retains each input buffer and accumulates the compressed ones into a list
local to getRecordBatch, so a buffer that fails to compress strands that
retain and leaves every buffer compressed before it reachable from nothing.
Closing the input batch afterwards undoes neither. Unload plain and compress
in CachedBatchIpc.compressed instead, mirroring what decompressed already
does on the read side, so every allocation stays reachable from an error
path that owns it.

The docs enabled the cache with spark.conf.set, which cannot work: the
driver plugin picks spark.sql.cache.serializer while the SparkContext is
initializing. Show it as a startup --conf.

Also drops a redundant s interpolator that the scalafix lint rejected.

* fix: write cached batches to the schema width, not the batch width

ArrowWriter.writeColumns drove its loop from the input ColumnarBatch's width while indexing the writer's fields, which come from the schema the batch is written under. That assumed every producer hands over a batch exactly as wide as the schema.

Iceberg's vectorized reader does not. BatchDeleteFilter.filterBatch reads with the delete filter's requiredSchema, which carries _pos after the projected columns when a data file has position deletes, and trims the extras back only when the file also has equality deletes. A merge-on-read UPDATE writes position deletes and no equality deletes, so the extra column survives into the batch, and caching such a relation failed with ArrayIndexOutOfBoundsException inside the write loop.

Drive the loop from the writer's fields instead, which writes exactly the columns the schema describes: the extras are trailing, the same prefix Iceberg keeps when it does trim. A batch narrower than the schema is a genuine contract violation and is now refused with a message naming both widths.

Closes apache#6087.

* fix: report a cached relation's decoded size to the planner, not its compressed size

Comet's cache format reported each batch's sizeInBytes as its stored,
compressed payload. Spark's planner reads the sum of those as the size of a
materialized cached relation, for the broadcast threshold and the shuffled
hash join build side among others, and both of Spark's own cache formats
report decoded sizes there. With the default zstd codec a cached relation
could look several times smaller than in Spark's format and be broadcast
where Spark would shuffle it.

Record each column's decoded Arrow size, measured before compression as
Spark's ArrowCachedBatchSerializer does, and let CometCachedBatch inherit
sizeInBytes from SimpleMetricsCachedBatch as the sum of those.

* fix: keep Spark's cache scan for a relation whose cached plan records observed metrics

Spark collects Dataset.observe metrics after a query with
CollectMetricsExec.collect, which reaches the metrics recorded inside a cached
plan only through an InMemoryTableScanExec over it. With Comet's native cache
scan in its place those metrics were lost: QueryExecution.observedMetrics came
back empty, Observation.get returned an empty map on Spark 3.5 and later, and
on Spark 3.4 it never returned.

Keep Spark's scan for such a relation, with a fallback reason. The data stays
in Comet's format and is read through the existing fallback path. The check
walks the cached plan the way CollectMetricsExec.collect does, through
subqueries, adaptive plans and nested caches.

* test: install Comet's cache serializer in the Spark SQL test sessions

The Spark SQL diffs turn Comet on through SharedSparkSession and TestHive
rather than by loading CometPlugin, and the plugin is what installs Comet's
cache serializer. So even with spark.comet.exec.inMemoryCache.enabled on by
default, every Spark SQL suite cached in Spark's own format and none of them
exercised Comet's.

Set spark.sql.cache.serializer in both places when Comet is enabled, as the
plugin would, and adapt the tests that assume Spark's cache scan:

- plan checks that only need a cache scan to be present also accept
  CometInMemoryTableScanExec, reading its originalPlan where the test uses
  the scan's relation;
- PartitionBatchPruningSuite checks its answers as before, but reads the
  test-only accumulators only from Spark's scan, which Comet's lacks;
- CacheTableInKryoSuite registers Comet's classes with
  CometKryoRegistrator, as Comet asks of any application that sets
  spark.kryo.registrationRequired;
- tests of Spark internals that Comet's scan replaces are tagged
  IgnoreComet: exact cache size estimates, the union's columnar support,
  subquery reuse through the scan's predicates, and AQE coalescing under a
  union (apache#6454).

Each diff was regenerated from a clone of its Spark tag with the existing
diff applied, after checking that it round-tripped unchanged.

* fix: coalesce shuffle partitions under Comet unions the way Spark does

Spark's CoalesceShufflePartitions coalesces each child of a UnionExec as its
own group, and from Spark 4.0 each child of a CartesianProductExec,
BroadcastHashJoinExec or BroadcastNestedLoopJoinExec too. It matches those
classes, so a CometUnionExec fell through to the case that coalesces only when
every leaf below it is an exchange stage. A union with a scan or a table-cache
stage in one branch kept every partition of the shuffles in the others.

Add CometCoalesceShufflePartitions, a query-stage optimizer rule that runs
after Spark's. For a Comet operator whose Spark original is one of those, and
whose shuffle stages no AQE rule has touched, it rebuilds the Spark operators
over the Comet children, runs Spark's own rule over that, and swaps the Comet
operators back in. It extends AQEShuffleReadRule, so AQE gates and validates
it the way it does Spark's rule. Spark 3.4 has no hook for query-stage
optimizer rules, so the fix applies from Spark 3.5.

Closes apache#6454.

* test: fix three Spark SQL test adaptations for Comet's cache format

- CachedBatchSerializerNoUnwrapSuite sets its own cache serializer, but
  Spark keeps the first one it loads for the rest of the JVM, so the
  suite ran with Comet's. Clear it before and after the suite, as
  CachedBatchSerializerSuite does (4.1.3, 4.2.0).
- The SPARK-19993 helper counted the subqueries in a Comet cache scan's
  original plan, which subquery reuse never reaches, on top of the same
  subqueries in the filter above the scan.
- SPARK-35332's AQE check reads the cached plan from the SQL UI's plan
  tree, which shows it only under Spark's own cache scan (apache#6463). Skip
  it under Comet (4.0.4 and later).

* fix: keep Spark's cache format when Kryo would reject Comet's or Comet disables itself

CometDriverPlugin installed ArrowCachedBatchSerializer whenever Comet, its
native execution and the cache config were enabled at startup. Two more
startup settings decide whether that format can work:

- With spark.kryo.registrationRequired=true and no CometKryoRegistrator,
  Kryo rejects CometCachedBatch, so caching failed with "Class is not
  registered" as soon as Spark serialized a cached block, where Spark's
  own format works. The plugin only warned. It now keeps Spark's format,
  and the warning says so.
- With Comet shuffle enabled but neither of Comet's shuffle managers
  configured, Comet disables itself, so every cache was stored in Comet's
  format with only Spark operators to read it. The plugin now keeps
  Spark's format there too.

The plugin's boolean config reads also honor a deprecated alternative
key, such as spark.comet.exec.shuffle.enabled, as a session does.

* docs: describe the in-memory cache default in the upgrade guide

Add a 1.2.0 section to the upgrade guide for the new default of
spark.comet.exec.inMemoryCache.enabled: what changes, how to keep Spark's
cache format, and when Comet keeps it without being asked. The operator
and compatibility pages still called the feature disabled by default,
and the cache guide still said an application that started with the
default kept Spark's format.

* test: benchmark the in-memory cache against Spark's format with AQE on

The published comparisons either read the same Comet-written cache both
ways, with spark.comet.sparkToColumnar.enabled turned on, or read both
formats with Comet off. Neither is what an application gets from the
feature's default. Add cases with Comet and AQE on and Comet's other
settings at their defaults, reading a relation cached in Spark's format
and in Comet's, with Comet operators above the cache scan and with a
Spark operator above it.

* feat: record why Spark scans a relation cached in Comet's format when Comet is off

spark.sql.cache.serializer is static, so a relation cached in Comet's
format keeps it for the life of the application. A session that turns
Comet or its native execution off then reads it with Spark's
InMemoryTableScanExec, which is slower than reading Spark's own format
(apache#5485). CometExecRule records a fallback reason for the other ways Spark
ends up scanning a cache, but it returned before looking at the plan in
these two cases, so nothing explained them.

Record a reason on each such scan, naming the cause and the startup
setting that keeps caches in Spark's format.

* docs: say an application keeps the cache format it started with

The sentence described a session started with the default, which keeps
Spark's format only while the default is off.

* test: format the in-memory cache benchmark

* fix: bind the fallback reason result for the strict Scala warnings build

-Ywarn-value-discard rejects the node that withFallbackReason returns,
discarded as the last expression of a case.

* docs: compare the in-memory cache with Spark's format as an application gets it

Add the benchmark's adaptive results to the cache guide: with Comet and
AQE on and Comet's other settings at their defaults, Comet's format is as
fast or faster than Spark's in every shape but a read of three long
columns, which zstd decompression makes about 10% slower. That includes
a Spark operator above the native scan.

The guide said that, without the feature, a CometSparkColumnarToColumnar
converts each batch of Spark's cache scan for Comet. That takes
spark.comet.sparkToColumnar.enabled, which is off by default, so by
default the operators above Spark's scan run on Spark. The limitation
that Spark operators read Comet's format slowly applies to Spark's own
cache scan, not to Spark operators above Comet's.

* fix: count Kryo registrations however the application made them

The gate kept Spark's cache format whenever spark.kryo.registrator did
not list CometKryoRegistrator, even where the application had registered
Comet's cached batch another way, such as spark.kryo.classesToRegister.
Comet's format works there, while Spark's does not before 4.1, which
registers DefaultCachedBatch itself only from then on, so the gate broke
caches that worked.

Ask a Kryo instance built from the application's conf which of Comet's
classes it has registered, and keep Spark's format only if Comet's
cached batch is not among them. If Kryo cannot be built, fall back to
looking for CometKryoRegistrator in spark.kryo.registrator. The startup
warning uses the same check and names the missing classes.

* docs: describe the Kryo condition by registration in the upgrade guide

The plugin now keeps Spark's cache format when Kryo has not registered
Comet's cached batch, however the application registers classes, rather
than when spark.kryo.registrator does not list CometKryoRegistrator.

* docs: name spark.comet.convert.inMemoryCache.enabled for converting Spark's cache scan

spark.comet.sparkToColumnar.enabled is deprecated for in-memory cached tables
since apache#6602, which gave the conversion its own config.

* test: accept Comet's cache scan in BroadcastJoinSuite's cached-relation tests

Since apache#6415, suites that build their own SparkSession run Comet, and with the
cache on by default BroadcastJoinSuite's cached relations are read by
CometInMemoryTableScanExec. SPARK-23214 and SPARK-37742 count Spark's cache
scan and broadcast hash join, so count Comet's as well.

* fix: coalesce with the whole stage in view and keep partitionings above Comet unions

Address review of the CometCoalesceShufflePartitions rule:

- Stand in only for Comet operators whose output partitioning is unknown.
  From Spark 4.1 a union of co-partitioned children reports their
  partitioning, and an aggregate above reads it without a shuffle. Coalescing
  one branch made Comet's aggregate, which states no required distribution,
  return each key twice.
- Hand Spark's rule the whole stage rather than the subtree below each Comet
  operator, so it sees a Cartesian product or nested loop join above a union
  and uses its smaller target size there, and divides the minimum partition
  count over every group. Leave a stage that already has a read over a
  shuffle, which Spark's rule would coalesce again.
- Copy the Spark original when withNewChildren hands it back, so a union that
  AQE plans again over its stages is coalesced too.
- Drop the CartesianProductExec case, which has no Comet counterpart, and
  document the rule in CometSparkSessionExtensions.

* test: run Spark's SPARK-42101 union coalescing test with Comet again

The test failed with Comet's cache format because AQE did not coalesce the
shuffled branch of a union that Comet ran (apache#6454), which apache#6459 fixes.

* fix: coalesce Comet union branches in stages that already hold an AQE shuffle read

CometCoalesceShufflePartitions returned a stage unchanged when it held
any AQEShuffleReadExec, because Spark's rule asserts on reads that are
not skew splits. Below a Cartesian product, Spark's rule coalesces the
shuffle on the other side by itself first, so the read it leaves there
kept the shuffled branch of a Comet union at every partition.

Hide each read behind a leaf while Spark's rule runs, and put it back
afterwards. Spark's rule then leaves alone the shuffles it would
coalesce together with a read one, and coalesces the rest of the
stage.

* docs: say Spark's cache scan can read Comet's format more slowly

The fused reader from apache#5859 reads Comet's format about as fast as
Spark's own, and faster for the narrowest reads, so the upgrade note
should not say Spark's scan is always slower.

* test: leave Comet's cache scan out of the DPP suite's subquery check

Since apache#6577, CometInMemoryTableScanExec exposes Spark's own
InMemoryTableScanExec as its one subquery, so that Spark's UI draws the
cached plan below it. With the cache on by default, the DPP suite's
'filtering ratio policy fallback' caches its dimension table, and
checkPartitionPruningPredicate requires every subquery of an adaptive
plan to contain an AdaptiveSparkPlanExec, which Spark's scan does not.
Skip it there in every diff, since the query never runs it as a
subquery.

* feat: keep Comet's cache format opt-in on Spark 3.4

Spark 3.4 has no hook for query-stage optimizer rules, so
CometCoalesceShufflePartitions cannot run there, and AQE cannot
coalesce the shuffled branch of a Comet union whose other branch reads
a cache in Comet's format (apache#6454). spark.comet.exec.inMemoryCache.enabled
now defaults to true only from Spark 3.5, and the plugin, which follows
the entry's default, keeps Spark's format on 3.4 unless it is set.

The 3.4 Spark SQL diff goes back to main's: its sessions install
Comet's serializer only as the plugin would, which on 3.4 it no longer
does by default, so its cache test adaptations are not needed there.

* docs: rewrap the Spark 3.4 cache default sentences

* test: run Spark's SPARK-35332 AQE cache test with Comet again

The diffs skipped CachedTableSuite's 'SPARK-35332: Make cache plan
disable configs configurable - check AQE' on Spark 4.0 and later,
because SparkPlanInfo drew the cached plan only below Spark's own
InMemoryTableScanExec. Since apache#6577 it is drawn below
CometInMemoryTableScanExec too, so the test finds the cached plan.

The test's check that the cached plan was coalesced, which the diffs
already adapt, looked for AQEShuffleRead only below a columnar-to-row
transition, where it is when Comet's plan is cached in Spark's format.
In Comet's columnar format the cached plan ends in the read itself, as
in Spark, so the check accepts it there too. The test passes on Spark
4.0, 4.1 and 4.2 with either format, and with Comet off.

* docs: stop describing the in-memory cache as experimental

It is enabled by default from Spark 3.5. The operator table now marks
InMemoryTableScanExec as supported, and the plan-node table no longer says
the cache scan is disabled by default.

* test: skip only the format-dependent checks in the Spark SQL diffs

SPARK-36120, SPARK-33687, SPARK-22673 and SPARK-43376 now run with Comet's
cache format and skip only the size or subquery-reuse assertions that depend
on it. The union test in CoalesceShufflePartitionsSuite runs its coalescing
check again on Spark 3.5 and later, where CometCoalesceShufflePartitions
coalesces the shuffles on the union's join side.

From Spark 3.5, CometTestSettings also makes Comet's cache serializer the
default of the sql and hive test JVMs, next to the shuffle manager. Spark
keeps the first cache serializer it loads for the rest of the JVM, so a suite
that caches first through a session of its own left the suites after it in
Spark's format.

* test: time building the cache in Spark's format in the cache benchmark

runBuildBenchmark caches the same relation in Spark's format and in Comet's,
with each codec, from Comet operators and from Spark operators, and reports
the footprint of each. The cache guide has the results.

* test: skip only the columnar checks of SPARK-37371 under Comet

Comet runs the union of two cached relations as a CometUnion over its own
cache scans, so the checks on Spark's scans and union are skipped there. The
union with a local relation stays Spark's non-columnar UnionExec, and both
answers are checked, under Comet as well.

---------

Co-authored-by: test <a@b.c>
apache#6599)

* feat: run array_contains on float elements natively with Spark's equality

A Comet kernel compares float elements as Spark's genEqual does, with
-0.0 equal to 0.0 and all NaNs equal, at any depth, so these calls no
longer go through the codegen dispatcher.

Closes apache#6520

* fix: keep nested floats and eagerly evaluated values on the dispatcher

Route only flat FLOAT and DOUBLE arrays to spark_array_contains. Nested
float elements, and a value other than a literal or column read over a
nullable array, stay on the codegen dispatcher, which is faster for
nested elements, keeps string collations, and skips the value for a null
array as Spark does.

* test: import EDGE_VALUES from float_semantics in array_contains tests

Main moved the constant there, so the crate's unit tests failed to
compile.

---------

Co-authored-by: Andy Grove <agrove@apache.org>
…ceberg writes (apache#6808)

apache#6441 makes a native Iceberg write to S3 fall back when the Hadoop
configuration has an fs.s3a.* setting the native writer doesn't support.
Spark 4.1 sets fs.s3a.committer.magic.enabled and fs.s3a.committer.name
for every application when spark-hadoop-cloud is on the classpath
(SPARK-47618), and many deployments set fs.s3a.experimental.input.fadvise,
fs.s3a.bulk.delete.page.size or fs.s3a.committer.threads cluster-wide. Any
of them makes every native S3 write fall back.

None of these settings affects an Iceberg data-file write: the native
writer doesn't go through S3A, fadvise is a read hint, and Iceberg commits
through table metadata rather than a Hadoop output committer. Ignore them
like the Spark-seeded read settings already in IgnoredHadoopS3Keys.
…Spark 3.x native writer (apache#6821)

* fix: run InsertIntoHadoopFsRelationCommand's post-write steps in the Spark 3.x native writer

On Spark 3.x CometNativeWriteExec replaces the whole InsertIntoHadoopFsRelationCommand, but
only ran the job/task commit lifecycle. After the job commits it now refreshes the table's
file index, recaches cached plans that read the output path, and updates or clears the
table stats, as the command does. It also rejects duplicate output column names up front,
which the command checks before writing.

Co-authored-by: Isaac <no-reply@databricks.com>

* test: assert the duplicate-column write is native

Spark's own writer rejects the same columns, so a fallback would keep the test green
without exercising the native writer's check. Capture the plan of the failed write as
well, and assert it carried a native write.

Co-authored-by: Isaac <no-reply@databricks.com>

---------

Co-authored-by: Isaac <no-reply@databricks.com>
…#6695)

* refactor: move the plan entry point logic into core functions

Separate the native logic of createPlan, setShufflePartitionPusher,
executePlan and releasePlan from their JNI argument handling:

- create_execution_context builds the execution context from plain values
  (PlanInputs) and a memory pool factory. prepare_plan_settings parses the
  Spark configs and initializes the Tokio runtime.
- register_shuffle_partition_pusher checks the plan state before building
  the pusher.
- execute_plan returns the plan's next batch; the export still exports it.
- release_plan stops and drops the plan.

These plans call back into the JVM, so the JVM objects stay JNI global
references in the execution context (PlanJvmRefs), and the steps that need
an Env are passed in as callbacks: the memory pool factory, the pusher
factory, and PublishMetrics. ExecutionContext::metrics becomes an Option,
which is always set by createPlan, so a plan can be created and run without
a JVM in unit tests.

Exception classes and messages are unchanged.

* Test that release_plan returns the memory of a plan holding a reservation

The lifecycle test ran a RangeScan, which never reserves memory, so its
check that nothing stays reserved after release_plan could not fail. Run
a descending sort over the range instead, release it after its first batch
while it still holds its buffered input, and check that the plan holds a
reservation before the release and none after. Do the same when the final
metrics publish fails. Draining a plan to the end stays covered by a
separate test.
* refactor: remove unused cast compatibility flag

* refactor: drop unused parquet allow_incompat option

The flag has no remaining reader after the cast-option cleanup, and the unused CometConf import fails scalafix.

* fix: preserve DPP filters when reverting native scans

* Fix SparkCastOptions constructor calls

* refactor: drop the DPP scan reversion from the cast cleanup

* fix: drop the removed cast-option argument from case-when calls

* fix: drop the removed parquet-option argument from the page-index test

* fix: drop the removed cast-option argument from the cast_nested bench

apache#6648 added this bench after the last merge of main into this branch, and it still passed the third argument that this PR removes from SparkCastOptions::new.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andy Grove <agrove@apache.org>
…'s entry (apache#6310)

Spark removes a task's execution pool entry when its balance reaches zero,
and an acquire that was parked in the pool then throws NoSuchElementException
when it wakes up. Releases take no task monitor, so another native thread or
a JVM consumer of the same task can empty the balance while a native acquire
waits. The error is not a memory refusal, so the operator could not spill
and the task failed.

CometTaskMemoryManager now recognises that one exception and calls Spark
again. A missing entry means the task held nothing from Spark, so the failed
call has no grant to reconcile, and the retry registers the task again and
waits for its share as the first call would have. After three attempts it
returns a zero grant, which the native side treats as a refusal and spills.

Closes apache#6224.
…conversion (apache#6824)

* Builder appending refactoring

* Formatting, small refactoring and adding docstrings

* Added docstring regarding alignment

* Fixed the fallback in an unaligned but NULLABLE case, that was failing the CI

* Changes fixed

* Fixed cargo formatting

* Remade the building process using arrow buffers with no hot-spots in Vec<> loops

* Formatting fix

* Added docstring about runtime check of alignment

* Addressed the review comments

* Fixed formatting

* Added docstring regarding endianess

* test: cover a timestamp array with a timezone in the append round trip

The round-trip test built every builder with its type's default data type, so the
timestamp case never had a timezone. append_array panics when the array's data type,
timezone included, differs from the builder's. The test now builds the builder with the
column's data type and adds a timestamp column with a timezone, which fails if the
bitmap path drops the timezone.

* perf: append a nullable array with nulls in bulk only from 64 elements

Building the validity and value buffers takes four allocations per array, which cost as
much as appending 40 to 55 elements one by one, so the bulk path made lists of shorter
arrays with a null up to 4.8 times slower than the per-element loop.
MIN_BULK_NULLABLE_APPEND_ELEMENTS keeps those arrays on the loop. No array length is
slower than on main now, and arrays from 64 elements keep the speedup.

- The timestamp appender takes the timezone by reference, so the list path doesn't clone
  it for every row.
- process_primitive_lists is main's single arm again, with an optional argument list.
- The SAFETY comments are back, and the alignment comment points at SparkUnsafeObject.
- The partial-byte, all-null and all-valid tests run at lengths on both sides of the
  cutoff, from an 8-byte-aligned copy. The round trip covers 63 and 64 elements with
  nulls, and its "no nulls and long enough" case now has 8 elements. A new test appends
  several arrays into one builder, so a validity buffer lands at an offset that isn't a
  whole byte.

* test: cover the list path's timezone and make the bulk cutoff measurable in-tree

- timestamp_list_column_keeps_its_timezone drives a Timestamp(Microsecond, Some("UTC"))
  list column through append_columns, so append_list_column_batch must forward the
  timezone. With &None in its place, append_array's data type assertion fails.
  CometColumnarShuffleSuite gets an array<timestamp> case with 70-element arrays that
  hold a null, which fails the same way end to end.
- The impl_append_to_builder doc states that `timezone` must be the builder's, and when a
  mismatch panics. The has_null() comment covers the validity buffer path too.
- array_element_append gets a list_with_one_null group: 8192 list rows of 32, 64 and 128
  elements with one null each, for i32 and i64, appended as the list path appends them.
  Its doc comment shows how to compare the bulk path with the per-element loop, and
  MIN_BULK_NULLABLE_APPEND_ELEMENTS cites it instead of an out-of-tree harness.

* test: read the bench's array words with as_chunks

Clippy 1.99 flags chunks_exact with a constant chunk size
(chunks_exact_to_as_chunks), so the rust-test job failed under -D warnings
before any Rust test ran. The buffer's length is a multiple of 8, so
as_chunks::<8>() leaves no remainder.

---------

Co-authored-by: sandudb <alexandrdomenti@gmail.com>
Co-authored-by: Matt Butrovich <mbutrovich@users.noreply.github.com>
…l arithmetic (apache#5216)

* perf: skip DecimalPrecision.promote's rewrite when there is no decimal arithmetic

QueryPlanSerde.exprToProto runs DecimalPrecision.promote over every expression it
converts. The rule is a transformUp, so it walks and rebuilds every node even when
nothing matches, which is the common case. Guard it with a cheap traversal that
looks for arithmetic producing a decimal, and only run the rewrite when one exists.

* ci: register DecimalPrecisionSuite in the PR build workflows

* docs: describe transformUp's real cost in the promote guard comment
… evaluation (apache#6777)

Registering a native UDF with a type Comet has no native representation
for, such as a user-defined type, succeeded, and every call then fell
back to Spark, which cannot evaluate it. CometNativeUDF.register now
refuses such a signature before it loads the library.

Spark also evaluates a call itself while planning a query: over local
data, in a filter on partition columns, and to sample the keys of a
global sort. The error a native UDF raises there said that Comet had not
taken the operator holding the call, which sent users to the explain
output for a fallback that never happened. The error, the scaladoc and
the Rust UDF guide now list those cases, and the guide says to sort on a
column holding a UDF's result rather than on the call.

Split out of apache#6697.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:aggregation area:expressions area:ffi area:Iceberg area:memory area:scan area:udf area:writer enhancement New feature or request run-all-spark-profiles Run Comet test suites against all supported Spark profiles run-spark-4.1-tests Run Spark 4.1 SQL tests across all modules

Projects

None yet

Development

Successfully merging this pull request may close these issues.