Skip to content

Conversation

@Brijesh-Thakkar
Copy link

Which issue does this PR close?

Rationale for this change

ScalarValue::partial_cmp previously returned a concrete ordering when NULL
values were involved (e.g. comparing a non-NULL value with NULL, or NULL with
NULL). This behavior was unexpected and inconsistent with SQL three-valued
logic, where NULL values are not comparable.

The incorrect ordering could propagate to higher-level utilities such as
try_cmp and vector ordering, leading to subtle and hard-to-detect bugs.

What changes are included in this PR?

  • Updated ScalarValue::partial_cmp to return None whenever either operand
    is NULL
  • Ensured ScalarValue::try_cmp returns an error for NULL comparisons
  • Aligned vector ordering utilities to behave consistently with the updated
    NULL comparison semantics
  • Added tests to validate the corrected behavior

Are these changes tested?

Yes.

  • Added a new unit test covering NULL comparison behavior in
    ScalarValue::partial_cmp
  • All existing unit tests and doctests pass

Are there any user-facing changes?

Yes.

Comparisons involving NULL ScalarValues no longer produce an ordering and now
behave consistently with SQL semantics. This may affect users relying on the
previous (incorrect) ordering behavior.

Copilot AI review requested due to automatic review settings December 31, 2025 19:18
@github-actions github-actions bot added the common Related to common crate label Dec 31, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes NULL handling in ScalarValue::partial_cmp to align with SQL three-valued logic semantics. Previously, the method returned concrete orderings when NULL values were involved, which was incorrect and could lead to subtle bugs in higher-level operations.

Key changes:

  • Modified ScalarValue::partial_cmp to return None for any comparison involving NULL values
  • Updated ScalarValue::try_cmp to properly error on NULL comparisons
  • Fixed vector ordering tests to verify that NULL comparisons return None

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
datafusion/common/src/scalar/mod.rs Core NULL comparison logic: added early return for NULL values in partial_cmp, removed old NULL comparison code, updated try_cmp test to expect errors for NULL comparisons, added comprehensive test for NULL partial ordering
datafusion/common/src/utils/mod.rs Updated vector ordering tests to verify NULL comparisons return None instead of asserting ordering

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@2010YOUY01 2010YOUY01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, the implementation looks good to me.

Let's fix the tests first and then see what else pops up.

}
(Dictionary(_, _), _) => None,
(Null, Null) => Some(Ordering::Equal),
// Null is handled by the early return above, but we need this for exhaustiveness
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do something like

(Null, Null) | (Null, _) | (_, Null) => unreachable!("Nulls are already handled before entering this matching arm

to be more defensive

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After investigating this further, I aligned the behavior with existing ScalarValue::partial_cmp semantics:

ScalarValue::partial_cmp continues to return None for any NULL comparison (including NULL vs NULL), consistent with SQL tri-state logic and existing DataFusion tests.

The failure in vector_ord was due to an incorrect test expectation assuming vectors containing NULLs were orderable.

I’ve updated the test to explicitly assert that vector comparison returns None when NULLs are encountered, rather than expecting < to succeed.

Implementing PostgreSQL-style composite ordering (NULL == NULL inside arrays/structs) would require a separate, higher-level comparison strategy and a conscious semantic change. I’ve kept this PR scoped to fixing the inconsistency without altering scalar comparison behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to implement this pg-compatible array comparison in a separate PR, I'll file a ticket afterwards.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirming

Agreed on keeping PostgreSQL-style composite ordering as a separate follow-up PR.
I’ll keep an eye on CI and update if anything comes up.

@2010YOUY01
Copy link
Contributor

Thanks again, it should be good to go after CI passing.

Unfortunately, we don't have a script to reproduce CI locally yet. To reproduce individual CI steps, you can check the CI steps in https://github.com/apache/datafusion/blob/9a9ff8d6162b7391736b0b7c82c00cb35b0652a1/.github/workflows/rust.yml

@Brijesh-Thakkar
Copy link
Author

I ran the relevant checks locally and can confirm the following all pass:

cargo fmt --all

cargo clippy -p datafusion-common -- -D warnings

cargo test -p datafusion-common (including doctests)

The remaining failures I saw earlier were from --all-features / benchmark and FFI targets, which appear unrelated to this change and are not required for datafusion-common.

The PR now strictly preserves existing tri-state NULL semantics for ScalarValue::partial_cmp, and vector ordering correctly returns None when NULLs are encountered.

Thanks for the review

@Brijesh-Thakkar
Copy link
Author

@2010YOUY01 I’ve verified locally that the change behaves correctly and passes within scope:

cargo test -p datafusion-common

cargo clippy -p datafusion-common -- -D warnings

The remaining CI failures appear to be workspace-level (benchmarks / allocator / FFI) and unrelated to the ScalarValue::partial_cmp NULL semantics change.

@2010YOUY01
Copy link
Contributor

@2010YOUY01 I’ve verified locally that the change behaves correctly and passes within scope:

cargo test -p datafusion-common

cargo clippy -p datafusion-common -- -D warnings

The remaining CI failures appear to be workspace-level (benchmarks / allocator / FFI) and unrelated to the ScalarValue::partial_cmp NULL semantics change.

I think they're failing because this PR's change has propagated to other places, we have to make sure all of them are passing.

This failure looks like a behavior change propagation
https://github.com/apache/datafusion/actions/runs/20653578399/job/59303606434?pr=19587

@Brijesh-Thakkar
Copy link
Author

@2010YOUY01 I’m investigating the failing CI jobs now (tests + benchmarks) to identify where NULL comparison assumptions need to be updated, and will push follow-up fixes accordingly.

@Brijesh-Thakkar Brijesh-Thakkar force-pushed the fix-scalarvalue-null-partial-cmp branch from cc60f57 to 552a1e9 Compare January 2, 2026 17:32
@github-actions github-actions bot added the logical-expr Logical plan and expressions label Jan 2, 2026
@Brijesh-Thakkar
Copy link
Author

I’ve pushed follow-up fixes to address all downstream failures caused by the updated ScalarValue::partial_cmp NULL semantics:

Updated ORDER BY rewrite logic to correctly handle aggregate aliases and preserve casts

Fixed logical plan comparison and ordering assumptions impacted by NULL → None comparisons

Resolved all related clippy warnings under -D warnings

I’ve verified locally that the following now pass:

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test -p datafusion-expr --lib
cargo test -p datafusion-common

The changes are now consistent across scalar comparison, vector ordering, and logical plan rewrites, with no remaining behavior mismatches.

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

Labels

common Related to common crate logical-expr Logical plan and expressions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unexpected Null behavior for ScalarValue::partial_cmp()

2 participants