Skip to content

[CALCITE-7817] Merge ANTI JOIN should preserve unmatched NULL keys - #5242

Open
bvolpato wants to merge 2 commits into
apache:mainfrom
bvolpato:bvolpato/fix-merge-anti-null-keys
Open

bvolpato wants to merge 2 commits into
apache:mainfrom
bvolpato:bvolpato/fix-merge-anti-null-keys

Conversation

@bvolpato

@bvolpato bvolpato commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Jira Link

CALCITE-7817

Changes Proposed

Enumerable merge ANTI joins currently stop processing left rows when they reach a NULL join key. Under strict equality, NULL keys do not match and those left rows must survive the ANTI join.

Preserve left NULL keys in all three merge-join advancement paths.

The existing ANTI expectations in EnumerablesTest encoded the bug, so this PR corrects them. For example, [3] is now [3, null], and "Both empty" with NULL keys is now [null] instead of [].

The ANTI section of EnumerablesTest.testMergeJoin3 has two new cases with repeated NULL keys. One case reaches the NULL keys after the right input runs out. The other case reaches them inside advanceLeft, immediately after a match. The tests also cover empty inputs, NULL keys on the right, and the overload with an additional predicate.

EnumerableJoinTest.testMergeJoinAntiWithSingleKeyAndNullValues covers the fix end to end. The existing end-to-end ANTI test uses a composite key, which already kept NULL keys.

Reproduction

With ascending, NULLS LAST inputs, an equality ANTI join of left [1, 2, NULL] and right [1] returns [2]. The correct result is [2, NULL].

./gradlew :core:test --tests 'org.apache.calcite.runtime.EnumerablesTest'

Validation

Both runs used Gradle in a JDK 21 container.

./gradlew :core:test --tests 'org.apache.calcite.runtime.EnumerablesTest' --tests 'org.apache.calcite.test.enumerable.EnumerableJoinTest'
  • With the fix, all tests pass: EnumerablesTest runs 53 tests with 1 skipped, and EnumerableJoinTest runs 13 tests.
  • With EnumerableDefaults.java from the base commit, 4 tests fail: 3 methods in EnumerablesTest, and testMergeJoinAntiWithSingleKeyAndNullValues with was "empid=100\nempid=110".
  • :linq4j:autostyleCheck, :core:autostyleCheck, :linq4j:checkstyleMain, :core:checkstyleTest, and git diff --check pass.
  • The full Gradle build has not been validated locally.

@bvolpato
bvolpato force-pushed the bvolpato/fix-merge-anti-null-keys branch from ca084e2 to 0c3d7fe Compare September 4, 2026 16:37
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@bvolpato bvolpato changed the title Merge ANTI JOIN should preserve unmatched NULL keys [CALCITE-7817] Merge ANTI JOIN should preserve unmatched NULL keys Sep 24, 2026
@bvolpato
bvolpato marked this pull request as ready for review September 24, 2026 01:59
Comment thread core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java
@rubenada

Copy link
Copy Markdown
Contributor

LGTM.
Looking at this with fresh eyes, it does look like a bug on the original code of EnumerableMerge AntiJoin

Comment thread core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java Outdated
@mihaibudiu

Copy link
Copy Markdown
Contributor

@rubenada you have LGTM but not approved officially. Still waiting for the nit?

@mihaibudiu mihaibudiu added the LGTM-will-merge-soon Overall PR looks OK. Only minor things left. label Sep 24, 2026
@rubenada

Copy link
Copy Markdown
Contributor

@rubenada you have LGTM but not approved officially. Still waiting for the nit?

It's not a blocking issue.

@vlsi vlsi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestions, none blocking:

  1. Please add an end-to-end test next to EnumerableJoinTest.testMergeJoinAntiWithCompositeKeyAndNullValues. That test covers only the composite key, which already kept NULL keys, so nothing at the SQL level pins this fix. This test fails on the base commit (was "empid=100\nempid=110") and passes with the PR:

    /** Test case for
     * <a href="https://issues.apache.org/jira/browse/CALCITE-7817">[CALCITE-7817]
     * Enumerable merge ANTI join drops left rows with NULL keys</a>. */
    @Test void testMergeJoinAntiWithSingleKeyAndNullValues() {
      tester(false, new HrSchema())
          .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
            planner.addRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
            planner.removeRule(EnumerableRules.ENUMERABLE_JOIN_RULE);
          })
          .withRel(builder -> builder
              .scan("s", "emps").as("e1")
              .sort(builder.field("commission"))
              .scan("s", "emps").as("e2")
              .filter(builder.equals(builder.field("deptno"), builder.literal(20)))
              .sort(builder.field("commission"))
              .antiJoin(
                  builder.equals(builder.field(2, 0, "commission"),
                      builder.field(2, 1, "commission")))
              .project(builder.field("empid"))
              .build())
          .explainHookContains("EnumerableMergeJoin")
          .returnsUnordered("empid=100", "empid=110", "empid=150");
    }
  2. The commit subject lacks the [CALCITE-7817] prefix. It would also help to state in the commit or PR description that the existing ANTI expectations in EnumerablesTest (for example [3] → [3, null], and [] → [null] for "Both empty") encoded the bug.

Comment thread core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java Outdated
Comment thread linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java Outdated
Add an EnumerableMergeJoin ANTI join test with a single nullable key.
The existing end-to-end test uses a composite key, which already kept
NULL keys.

Move the repeated NULL key cases into the ANTI section of
EnumerablesTest.testMergeJoin3. Add a comment to each case that names
the code path it exercises.

Correct two comments in EnumerableDefaults that said a NULL key always
ends the merge join. LEFT and ANTI joins continue to process left rows.

The previous ANTI expectations in EnumerablesTest encoded the bug. For
example, "[3]" is now "[3, null]", and "Both empty" with NULL keys is
now "[null]" instead of "[]".
@bvolpato

bvolpato commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review suggestions in 03486d7.

  • Added EnumerableJoinTest.testMergeJoinAntiWithSingleKeyAndNullValues. It fails with the base EnumerableDefaults.java (was "empid=100\nempid=110") and passes with the fix.
  • Moved the repeated NULL key cases into the ANTI section of testMergeJoin3, with a comment on each case.
  • Corrected the two comments in EnumerableDefaults.
  • The PR description now states that the previous ANTI expectations in EnumerablesTest encoded the bug.

The subject of the first commit still lacks the [CALCITE-7817] prefix. I added a new commit instead of a force-push, to keep the review history. I can squash both commits under the PR title when the review is complete.

@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

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

Labels

LGTM-will-merge-soon Overall PR looks OK. Only minor things left.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants