Skip to content

Keep rows where a delete filter evaluates to null - #4219

Open
Rahul-cs50 wants to merge 1 commit into
apache:mainfrom
Rahul-cs50:fix/4184-delete-null-or-filter
Open

Rahul-cs50 wants to merge 1 commit into
apache:mainfrom
Rahul-cs50:fix/4184-delete-null-or-filter

Conversation

@Rahul-cs50

Copy link
Copy Markdown

Closes #4184

Rationale for this change

When delete() (and overwrite() with an overwrite_filter) has to rewrite a data file, it keeps the rows that _expression_to_complementary_pyarrow selects. That filter was NOT f, OR'd with c IS NULL for every column c that f reads without a null check. Under pyarrow's three-valued logic that goes wrong both ways when a column is null:

  • NOT f is null where f is null, so the row is dropped even though f did not match it: x=2 in the issue (p == 2, v null) is deleted by (p == 1 AND v IS NULL) OR (p == 2 AND v == 'c').
  • c IS NULL keeps a row whenever c is null, even where f is still true: p == 1 OR v == 'c' keeps x=1 (p == 1, v null).

A row should be deleted exactly where f is true, which are the rows scan(row_filter=f) returns. The complement is now NOT coalesce(f, false). The NaN handling (OR is_nan(c) for columns the filter does not NaN-check) is left as it was.

Are these changes tested?

Yes. test_delete_and_overwrite_filter_with_null in tests/catalog/test_catalog_behaviors.py runs the issue's two filters plus v != 'c' and NOT (v == 'c') through delete() and overwrite() on every local catalog, and checks that the rows left are the ones scan(row_filter=...) does not return. It fails on main and passes with this change. test_expression_to_complementary_pyarrow is updated for the new expression. Unit tests in tests/io, tests/table, tests/catalog and tests/expressions: 2658 passed (the only failures are the four Kerberos tests in test_hive.py, which need the hive-kerberos extra I did not install). ruff check and ruff format --check are clean.

Are there any user-facing changes?

delete() and overwrite(..., overwrite_filter=...) no longer delete rows the filter does not match, or keep rows it does, when a column the filter reads is null.

delete() and overwrite() rewrite a data file with the rows the delete
filter does not select. The complementary filter was Not(filter) OR'd
with IS NULL for each column the filter does not null-check, so a null
kept rows the filter matches (p == 1 OR v == 'c' with a null v) and
dropped rows it does not match, where Not(filter) is null.

Coalesce the filter to false before inverting it, so exactly the rows
where the filter is true are deleted, the rows scan(row_filter=filter)
returns.

Closes apache#4184
Copilot AI balanced review requested due to automatic review settings October 31, 2026 07:01

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@developer-rpai developer-rpai left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice writeup, and checking against scan(row_filter=...) is the right oracle. Wrapping in coalesce(..., False) works well with Arrow's kleene and/or.

  1. I think the NaN branch in _expression_to_complementary_pyarrow (pyiceberg/io/pyarrow.py around L1125) now has the same problem the null branch had. OR is_nan(col) keeps a row whenever col is NaN, even if the filter is true through another column. Take p == 1 OR d > 0 with p = 1 and d = NaN: Arrow gives false for NaN > 0 (not null), so the filter is true and scan returns the row, but the complement still keeps it. d != 5 is the same, since NaN != 5 is true in Arrow. Could you add a float column with a NaN to test_delete_and_overwrite_filter_with_null and check whether the NaN terms are still needed at all? Fine as a follow-up if you'd rather keep this PR to nulls.
  2. Minor: _NullNaNUnmentionedTermsCollector still fills null_unmentioned_bound_terms, but nothing reads it anymore. You could drop that half or leave a note.
  3. The updated expected repr in tests/io/test_pyarrow_visitor.py is tied to pyarrow's expression formatting. A behavior assertion would be less brittle across pyarrow versions, but that's a nit.

This branch has not been deployed

No deployments
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.

Table.delete() with an OR filter deletes the wrong rows when a column is NULL

3 participants