Skip to content

fix: [branch-1.0] honour Spark's legacy null IN () behavior (#5127) - #5134

Merged
andygrove merged 1 commit into
apache:branch-1.0from
andygrove:backport-5127-branch-1.0
Jul 29, 2026
Merged

fix: [branch-1.0] honour Spark's legacy null IN () behavior (#5127)#5134
andygrove merged 1 commit into
apache:branch-1.0from
andygrove:backport-5127-branch-1.0

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

Cherry-pick of #5127 (merged to main as 038fd6e25) onto branch-1.0. Closes #4786 for the 1.0.0 release branch.

Rationale for this change

Comet's native in kernel always returns false for any operand against an empty IN list. Spark only behaves that way under the non-legacy behavior; under the legacy behavior a NULL operand evaluates to NULL (SPARK-44550), so Comet returns false where Spark returns NULL.

That legacy behavior applies more often than the config name suggests:

  • Spark 3.4: no config exists and In/InSet always use the legacy behavior
  • Spark 3.5: spark.sql.legacy.nullInEmptyListBehavior defaults to true
  • Spark 4.0+: the config is optional and defaults to !spark.sql.ansi.enabled, so the legacy behavior is in effect whenever ANSI mode is disabled

This is a correctness divergence on every supported Spark version, so it is worth carrying into 1.0.0 rather than leaving it as a documented known issue.

What changes are included in this PR?

A straight git cherry-pick -x of 038fd6e25. The diff is byte-identical to the commit on main; there were no conflicts and no adaptation was needed.

  • CometIn / CometInSet return Unsupported for an empty list when the legacy behavior is in effect. Both serdes already mix in CodegenDispatchFallback, so these cases run Spark's own doGenCode inside the Comet pipeline instead of falling the whole operator back to Spark.
  • CometNot's Not(In(...)) fast path skips the same case so it defers to CometIn (otherwise NOT (null IN ()) returned true instead of NULL).
  • Removes the now-fixed divergence from the compatibility guide (the entry added to branch-1.0 by docs: [branch-1.0] document known correctness issues in the compatibility guide (#5085) #5117), and replaces CometIn's compatible note with an unsupported-case reason.

OptimizeIn rewrites empty IN lists away, so this only affects plans where that rule is excluded; non-empty IN lists are unaffected.

How are these changes tested?

By the test added in #5127: CometExpressionSuite covers a IN () and NOT (a IN ()) over a Parquet scan with a NULL operand, across spark.sql.legacy.nullInEmptyListBehavior set to true, false, and unset (which exercises the version- and ANSI-derived default) with ANSI mode both on and off.

Verified locally on this branch under both -Pspark-3.5 and -Pspark-4.0; the test passes on each, and fails without the serde change.

* fix: honour Spark's legacy `null IN ()` behavior

Comet's native `in` kernel always returns `false` for an operand against an
empty `IN` list. Spark only does that under the non-legacy behavior; under the
legacy behavior a `NULL` operand evaluates to `NULL` (SPARK-44550). That legacy
behavior is in effect more often than the config name suggests: always on Spark
3.4 (no config), by default on Spark 3.5, and whenever ANSI mode is disabled on
Spark 4.0+, where `spark.sql.legacy.nullInEmptyListBehavior` defaults to
`!spark.sql.ansi.enabled`.

Mark the empty-list case `Unsupported` in `CometIn` / `CometInSet` when the
legacy behavior applies. Both serdes mix in `CodegenDispatchFallback`, so these
cases run Spark's own `doGenCode` inside the Comet pipeline rather than falling
the whole operator back to Spark. `CometNot`'s `Not(In(...))` fast path is
excluded for the same case so it defers to `CometIn`.

`OptimizeIn` folds empty `IN` lists away, so this only affects plans where that
rule is excluded, and non-empty `IN` lists are unchanged.

Closes apache#4786

* refactor: dedup the In/InSet support gates

- Share the empty-list + collation gate between `CometIn` and `CometInSet` via
  `ComparisonUtils.inSupportLevel` / `inUnsupportedReasons` instead of two
  copies that must be edited in lockstep.
- Read the legacy config through the existing `CometConf.getBooleanConf` helper
  rather than hand-rolling the string-key read, and make the resolver private.
- Have `CometNot`'s fused fast paths consult the child serde's `getSupportLevel`
  instead of re-checking its conditions by hand, so gates added to `CometIn` or
  `CollationAwareBinaryPredicate` no longer need mirroring here.
- Test: use `Rule.ruleName` for the excluded optimizer rules so a Spark rename
  cannot silently stop exercising the case.

(cherry picked from commit 038fd6e)
@andygrove
andygrove merged commit 01313d7 into apache:branch-1.0 Jul 29, 2026
69 checks passed
@andygrove
andygrove deleted the backport-5127-branch-1.0 branch July 29, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant