Use when reviewing a DataFusion Comet pull request that adds or changes a Spark expression, its Scala serde, its protobuf message, or its native Rust implementation. Load alongside review-comet-pr, which covers the parts of the review that apply to every PR.
<!--
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.
-->
Expression-specific review for Comet PR #$ARGUMENTS.
REQUIRED BACKGROUND: Use review-comet-pr for PR metadata, existing comments, CI, the review
bar, and the output format. This skill only covers what is specific to expressions.
The benchmark workflow and the no-regression rule, for PRs that change an existing kernel
docs/source/contributor-guide/timezones.md
The timestamp label rule, where the session timezone comes from, and how to test it, for datetime expressions, casts, and anything that returns a timestamp
Hold the diff against these. If the PR does something a guide says to do differently, either the PR
is wrong or the guide is out of date, and you need to say which.
1. Read the Spark Source
For any PR that adds or modifies an expression, you must read the Spark source to understand the
canonical behavior. This is the authoritative reference for what Comet must match. Do not review
an expression PR from the diff alone.
Clone Spark into /tmp/spark if it is not already there, using a shallow clone, then find the
expression class under sql/catalyst/src/main/scala by searching for its case class declaration.
Read the Spark implementation carefully. Pay attention to:
eval and doGenCode / nullSafeEval. These define the exact behavior.
inputTypes and dataType. These define which types Spark accepts and what it returns.
Null handling. Does it use nullable = true? Does nullSafeEval handle nulls implicitly?
Special cases, guards, and require assertions.
ANSI mode branches, usually SQLConf.get.ansiEnabled or failOnError.
Behavior differences across the Spark versions Comet supports. If the expression changed between
3.5 and 4.0, the serde needs a shim, not a single implementation.
Then read the Spark tests for the expression, under the test source trees in sql/.
Compare Spark's behavior against the PR and identify edge cases Spark tests that the PR does not,
types Spark supports that the PR does not handle, and behavioral differences that should be marked
Incompatible.
All child expressions converted via exprToProtoInternal
Return type correctly serialized
getSupportLevel reflects true compatibility rather than the happy path
Serde lives in the appropriate file (datetime.scala, strings.scala, arithmetic.scala, and so on)
ANSI and fail_on_error handling matches the constraints in adding_a_new_expression.md
A change that routes a whole class of expressions to the JVM codegen dispatcher lists the new
shapes it admits and tests one of each, including decimal results whose scale differs from the
declared type and boolean inputs from sliced batches (#6424, #6425)
A timezone-aware expression serializes CometTimeZone.nativeId(expr.timeZoneId), the
timezone Spark stamped on it in a form native code can parse, rather than
SQLConf.get.sessionLocalTimeZone or the JVM default, and returns
CometTimeZone.supportLevel from getSupportLevel when nativeId gives None
Registration in QueryPlanSerde.scala
Added to the correct map (temporal, string, arithmetic, and so on)
No duplicate registrations
Import statement added
Return type collisions with DataFusion built-ins
If the PR registers a Spark function whose name is also defined by datafusion-functions, such as
levenshtein, concat, coalesce, sha2, or regexp_replace, check that the serde sets the
return type explicitly via scalarFunctionExprToProtoWithReturnType rather than
scalarFunctionExprToProto or the bare CometScalarFunction(name) shortcut.
Without an explicit return type the native planner consults DataFusion's UDF registry first for
type resolution, and any arity or input-type difference between the Spark and DataFusion versions
fails native execution with Error from DataFusion: Function 'X' expects N arguments but received M. The Comet UDF is only swapped in after DataFusion's signature validation passes. See "When to
set the return type explicitly" in adding_a_new_expression.md.
3. Rust Implementation
Location: native/spark-expr/src/, registered in comet_scalar_funcs.rs.
Matches DataFusion and Arrow conventions
Null handling is correct, including nulls inside nested types
No panics. Use Result.
Batch operations rather than row-by-row where a kernel exists
Invalid UTF-8 going into a native StringType goes through decode_utf8_spark_lossy
An aggregate merges partial states in Spark's order of operations, not just with Spark's
buffer layout. A different floating-point order fails Spark's exact checks, such as m2 == 0,
when a constant is merged from several partitions (#6423).
Comparisons, grouping and set operations treat -0.0 and 0.0, and NaN payloads, as Spark
does, including inside arrays and structs. Where a DataFusion kernel treats them differently,
the serde falls back (#5507, #5701).
A TimestampType result is labelled "UTC", in return_type() or data_type() and in the
arrays the kernel builds. Not the session timezone, and not None.
Local-time work uses the session timezone from the proto. DataFusion's own datetime functions
take the timezone from the input's label, so wiring one in for a timezone-aware Spark
expression evaluates it in UTC unless the session timezone is passed explicitly.
A UTC fast path matches a fixed list of UTC aliases, as is_utc_timezone in
extract_date_part.rs does, and sends every other timezone ID down the general path
Before accepting a hand-written kernel, ask whether the function already exists upstream in
DataFusion or the datafusion-spark crate. Comet prefers wiring an upstream function over carrying
its own copy.
4. Tests
Expression tests should use Comet SQL Tests (CometSqlFileTestSuite) where possible. The
framework runs each query through both Spark and Comet and compares results, with no Scala code.
Fall back to Comet Scala Tests in CometExpressionSuite only when the SQL framework cannot express
the test, for example complex DataFrame setup or programmatic data generation.
Test location: spark/src/test/resources/sql-tests/expressions/<category>/, with categories
aggregate/, array/, string/, math/, struct/, map/, datetime/, hash/, and others.
statement
CREATE TABLE test_crc32(col string, a int, b float) USING parquet
statement
INSERT INTO test_crc32 VALUES ('Spark', 10, 1.5), (NULL, NULL, NULL), ('', 0, 0.0)
-- default mode: verifies native Comet execution and that the result matches Spark
query
SELECT crc32(col) FROM test_crc32
-- compares results without requiring native execution
query spark_answer_only
SELECT crc32(cast(a as string)) FROM test_crc32
-- allows numeric variance for floating-point results
query tolerance=0.0001
SELECT cos(v) FROM test_trig
-- asserts fallback to Spark occurs
query expect_fallback(unsupported expression)
SELECT unsupported_func(v) FROM test_table
-- verifies both engines throw matching exceptions
query expect_error(ARITHMETIC_OVERFLOW)
SELECT 2147483647 + 1
-- skip a query with a known bug, with the issue link
query ignore(https://github.com/apache/datafusion-comet/issues/NNNN)
SELECT known_buggy_expr(v) FROM test_table
Run the whole suite with -Dsuites="org.apache.comet.CometSqlFileTestSuite" -Dtest=none, or a
single file by appending a substring of its name to the suite argument.
Verify all test requirements, whichever framework is used:
Basic functionality tested against column data, not only literals
Both literal and column arguments tested, in every combination for multi-argument
expressions. They take different code paths.
Timestamp and datetime expressions are tested as "Testing timezone-sensitive code" in
timezones.md describes. That means several session timezones through ConfigMatrix,
including Etc/UTC and a zone with DST, both TIMESTAMP and TIMESTAMP_NTZ inputs, and a
result that is compared or fed to another expression rather than only projected.
SQL syntax gated with MinSparkVersion when it only parses on newer Spark
For a function from another project, such as an Iceberg transform, the expected values come
from that project's Java implementation, which is what Spark runs, not from iceberg-rust
(#6426)
A query ignore(...) for a result that differs from Spark comes with a fallback for that
case, so the divergence doesn't ship natively by default (#5701)
expect_error patterns substring-match what both Spark and Comet actually throw
For PRs that add a new native expression, performance is not optional. The point of a native
implementation is to be faster than Spark's codegen. If it is not faster, it may not be worth
carrying.
Does the PR description contain benchmark numbers comparing Comet against Spark? If not, ask for
them.
Does the PR add a benchmark under spark/src/test/scala/org/apache/spark/sql/benchmark/?
Are the benchmarks representative, with realistic data sizes and the data shapes that break
optimizations, such as nulls, dictionaries, and wide strings?
Does the Rust implementation allocate or copy more than it needs to, process row by row where a
kernel exists, redo type conversions, or revalidate UTF-8 repeatedly?
If a benchmark shows Comet slower than Spark, flag it. The PR needs to explain why that is
acceptable or include a plan to fix it.
For PRs optimizing an existing kernel, optimizing_expressions.md defines the no-regression rule
and the requirement to record a performance audit. Hold the PR to both.
6. Documentation
Generated, review the source instead
The compatibility pages under docs/source/user-guide/latest/compatibility/expressions/ and
docs/source/user-guide/latest/configs.md are produced by GenerateDocs and regenerated by CI on
merge. Never ask the contributor to hand-edit them. Check what feeds them:
Expression marked Incompatible(Some("...")) but getIncompatibleReasons() is empty, so the
compat page shows it as supported with no caveats.
Unsupported(Some("...")) for specific types or argument shapes with no
getUnsupportedReasons() to surface the limitation.
Reason strings that drift from the notes passed to Compatible / Incompatible /
Unsupported. They need not match exactly, but consistency helps.
Reason strings too terse to be useful in user-facing docs: a single word, no context, no link to
a tracking issue when behavior is known to differ.
Hand-edited, the PR should update these
docs/source/user-guide/latest/expressions.md, the supported-expressions list. A new expression
belongs here.
Other latest/compatibility/ pages such as floating-point.md, regex.md, operators.md.
Does the PR make the contributor guide stale?
Check adding_a_new_expression.md against the diff. It goes stale when a PR:
Changes the CometExpressionSerde interface, the support-level API, or the
getIncompatibleReasons / getUnsupportedReasons contract
Adds or renames an expression map in QueryPlanSerde.scala, since the guide names them
Changes how the return type is resolved, which would invalidate the "when to set the return type
explicitly" section
Changes the shimming pattern or adds a Spark version to the set the guide lists
Changes how scalar functions are registered in comet_scalar_funcs.rs
Check sql-file-tests.md when the PR adds, renames, or changes a test directive. The directive
reference in that doc is the only place they are documented.
Common Expression Review Findings
Incomplete type support, Spark supports types the PR does not handle
Tests in the wrong framework, Scala tests where a SQL file test would do
Missing getSupportLevel, divergences left undeclared rather than marked Incompatible
Version-specific Spark behavior implemented once, with no shim
Name collides with a DataFusion built-in and no explicit return type
Timestamp result mislabelled, with the session timezone or no timezone instead of "UTC".
It only shows once the result is compared or fed to another expression.
Audit an existing Comet expression for correctness and test coverage. Studies the Spark implementation across versions 3.4.3, 3.5.8, 4.0.1, and 4.1.1, reviews the Comet and DataFusion implementations, identifies missing test coverage, and offers to implement additional tests.
Triage open Comet issues marked `requires-triage` per the project bug triage guide. Classifies each issue as a bug or an enhancement, applies the recommended type (`bug`/`enhancement`), priority, and area labels, removes `requires-triage`, and files a dated summary issue listing what was done. A human reviews the summary issue and closes it when satisfied.
Use when implementing a new Spark expression in DataFusion Comet. Walks through cloning latest Spark master to study the canonical implementation, checking the upstream datafusion-spark crate before writing native code, building the Comet serde and Rust wire-up from the contributor guide, then running audit-comet-expression to drive a test-coverage iteration loop.
Use when optimizing the performance of an existing native scalar expression in the datafusion-comet-spark-expr crate (native/spark-expr/) — casts, string/JSON/array/math kernels that run per-row or per-batch. Covers benchmarking, keeping output bit-identical, and the no-regression gate. Not for adding new expressions (use implement-comet-expression) or wiring upstream functions (use wire-datafusion-function).
Use when reviewing a DataFusion Comet pull request that crosses the JVM/native boundary, touching Arrow C Data or C Stream interface code, batch export and import, CometExecIterator, ScanExec, NativeUtil, CometVector subclasses, or jni_api. Load alongside review-comet-pr.
Use when reviewing a DataFusion Comet pull request that touches Iceberg writes, the split-operator write plan (IcebergWriteStrategy, IcebergWriteExec, IcebergCommitExec), the native Iceberg writer (CometIcebergWriteExec, iceberg_write.rs, iceberg_partition_path.rs), its eligibility gate (CometIcebergNativeWrite), or the iceberg-rust pin. Load alongside review-comet-pr.