You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.*/
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
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).
Set it from expr.nullable in the serde layer.
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.
Activity
andygrove commentedon Jun 30, 2026
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.nullableis already computed correctly on the JVM side, includingnullIntolerantpropagation, before serialization. So the natural fix for Comet is to propagate that value through the proto rather than re-deriving it natively.Current state
nullable() -> true.JvmScalarUdf, via thereturn_nullablefield on its proto message (expr.proto), consumed injvm_udf/mod.rs.isNullIntolerantshim 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:
nullable=truewhen actually non-null) is safe. It only misses optimizations. This is Comet's current behavior.nullable=false, then producing a null) is a hard failure: ArrowRecordBatchvalidation 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.isNullfor nullable NullIntolerant roots). A systematic mechanism would retire this recurring class of bugs.Suggested approach
JvmScalarUdfpattern into areturn_nullablechannel for general expressions (cleanest as a generic wrapper on theExprproto message so every serde benefits, rather than per-message fields).expr.nullablein the serde layer.nullable()and when building outputFields inplanner.rs.One caveat
Propagating
nullable=falseis 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 currentnullable=truedefault is the safe floor, so this likely wants to roll out incrementally and per-expression rather than as a global flip.