Skip to content

[core] Negate predicates like Java so NULL rows stay excluded - #1145

Open
JeonDaehong wants to merge 1 commit into
apache:mainfrom
JeonDaehong:fix-negate-null
Open

JeonDaehong wants to merge 1 commit into
apache:mainfrom
JeonDaehong:fix-negate-null

Conversation

@JeonDaehong

Copy link
Copy Markdown

Purpose

Linked issue: close #1143

Predicate::negate wrapped leaves in Not, and Not was evaluated as !inner, so a NULL input made NOT(p = 'a') true. Reads returned those rows and delete_by_predicate deleted them. Java flips the leaf instead (Equal.negate() is NotEqual), which is false for NULL.

Brief change log

  • Predicate::negate now does what Java Predicate.negate does: flip the leaf operator (=/!=, </>=, <=/>, IS NULL/IS NOT NULL, IN/NOT IN, BETWEEN/NOT BETWEEN) and push through AND/OR with De Morgan.
  • DataFusion: the NOT BETWEEN coverage check in scan.rs now matches the new shape of the negated filter (x < lo OR x > hi instead of NOT(x >= lo AND x <= hi)).
  • Leaves with no negated operator here (STARTS_WITH, ENDS_WITH, CONTAINS, LIKE, array ops) stay as Not(leaf). eval_row and the residual row filter now return false for those when the input is NULL, same as Java NotStartsWith/NotLike/... .

Tests

  • negated_predicates_keep_null_rows and negated_data_predicate_keeps_null_rows cover the cases from the issue on the partition and DV delete paths and on reads. Both fail on main.
  • Unit tests for negate and for NULL evaluation in eval_row and the residual filter.
  • Changed existing tests:
    • test_file_index_nested_not_with_added_column_falls_back now expects [1] instead of [1, 2]. added is NULL in the old file, so for id 2 the predicate is NOT(true AND unknown). Java drops that row too.
    • test_not / test_display_not, three DataFusion pushdown tests, the global index exactness test and the stats fail-open test asserted the old Not shape. The last two now build Predicate::Not directly so they keep testing the Not branch.

fmt and clippy (1.98.0, -D warnings) pass. cargo test --no-fail-fast -p paimon -p paimon-datafusion -p paimon-c: 5804 passed. The 39 failures are all TableNotExist / table not found for the fixture tables CI provisions, plus 2 that need the lumina library. The ones I checked fail the same way on main.

Not covered here: a Predicate::Not around AND/OR built by hand (not through negate) is still evaluated as !inner. Nothing in core builds that today, but I can add it if you'd like.

API and Format

No API change. Predicate::negate returns a different but equivalent tree for non-NULL inputs.

Documentation

None.

Predicate::negate wrapped leaves in Not, which was evaluated as !inner, so
NOT(p = 'a') selected rows where p is NULL. Reads returned them and
delete_by_predicate deleted them.

Flip the leaf operator and push through AND/OR like Java Predicate.negate.
Leaves without a negated operator stay as Not, which now evaluates to false
for NULL input.

Closes apache#1143
@JeonDaehong

Copy link
Copy Markdown
Author

@JingsongLi @leaves12138 could you take a look when you have time? Fixes #1143: delete_by_predicate with a negated predicate was also deleting rows where the column is NULL.

@JeonDaehong
JeonDaehong marked this pull request as draft October 17, 2026 06:24
@JeonDaehong

Copy link
Copy Markdown
Author

Moved to draft until CI is green. I couldn't run the docker fixtures and the Go/Python suites locally. Will mark it ready once they pass.

@JeonDaehong
JeonDaehong marked this pull request as ready for review October 17, 2026 06:42
@JeonDaehong

Copy link
Copy Markdown
Author

CI is green, ready for review.

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.

[Bug] Negated predicates match NULL rows, so delete_by_predicate deletes them

1 participant