Skip to content

Fix nullability report for spark expression #3362

Open

Description

@hsiang-c

Describe the bug

As apache/datafusion#19144 indicates, most of DF's Spark expression do not report the nullability correctly.

Spark 4.0 has a nullIntolerant method defined in Expression and the comment is

  /**
   * When an expression inherits this, meaning the expression is null intolerant (i.e. any null
   * input will result in null output). We will use this information during constructing IsNotNull
   * constraints.
   */

I think Comet should have similar fix.

Steps to reproduce

No response

Expected behavior

No response

Additional context

No response

Activity

  1. andygrove commented on Jun 30, 2026

    @andygrove
    Member

    Analysis: how this applies to Comet

    Worth noting that Comet's situation is the inverse of the DataFusion EPIC (apache/datafusion#19144), which makes the fix here cheaper.

    DataFusion has to reimplement Spark's per-expression nullability rules in Rust (via return_field_from_args) because it has no Spark to ask. Comet does have Spark: expr.nullable is already computed correctly on the JVM side, including nullIntolerant propagation, before serialization. So the natural fix for Comet is to propagate that value through the proto rather than re-deriving it natively.

    Current state

    • Most native expressions conservatively report nullable() -> true.
    • The only expression that propagates Spark's real nullability today is JvmScalarUdf, via the return_nullable field on its proto message (expr.proto), consumed in jvm_udf/mod.rs.
    • A version-agnostic isNullIntolerant shim already exists (CometExprTraitShim, handling the 3.x trait and the 4.x boolean method), but it is currently only used by the codegen dispatcher's null short-circuit, not for reporting native output nullability.

    Why it matters

    The risk is directional:

    • Over-reporting (nullable=true when actually non-null) is safe. It only misses optimizations. This is Comet's current behavior.
    • Under-reporting (nullable=false, then producing a null) is a hard failure: Arrow RecordBatch validation throws "declared as non-nullable but contains null values" downstream in shuffle/sort.

    That second failure mode has already been fixed one expression at a time, for example #4523 (GetStructField), #4533 (CreateArray), #4237 (array_except), and #4554 (codegen dispatcher honoring ev.isNull for nullable NullIntolerant roots). A systematic mechanism would retire this recurring class of bugs.

    Suggested approach

    1. Generalize the JvmScalarUdf pattern into a return_nullable channel for general expressions (cleanest as a generic wrapper on the Expr proto message so every serde benefits, rather than per-message fields).
    2. Set it from expr.nullable in the serde layer.
    3. Consume it on the Rust side in nullable() and when building output Fields in planner.rs.

    One caveat

    Propagating nullable=false is only safe where Comet's native implementation provably matches Spark, otherwise a latent value divergence becomes a hard Arrow validation error the moment the field is marked non-nullable. Comet's current nullable=true default is the safe floor, so this likely wants to roll out incrementally and per-expression rather than as a global flip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:expressionsExpression evaluationbugSomething isn't workingpriority:mediumFunctional bugs, performance regressions, broken features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions