Skip to content

Match NaN partition values with IsNaN in partition predicates - #4281

Draft
hcrosse wants to merge 2 commits into
apache:mainfrom
hcrosse:nan-partition-predicate
Draft

hcrosse wants to merge 2 commits into
apache:mainfrom
hcrosse:nan-partition-predicate

Conversation

@hcrosse

@hcrosse hcrosse commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Closes #4277

Rationale for this change

delete(), overwrite(), upsert() and dynamic_partition_overwrite() raise ValueError: Cannot create expression literal from NaN when they touch a NaN identity partition. _build_partition_predicate builds EqualTo for each non-NULL partition value, and expression literals reject NaN. I changed _build_partition_predicate to emit IsNaN for NaN partition values next to the existing IsNull branch.

This PR depends on #4280. On main, Record compares two NaN partition values as unequal, so commit validation misses a concurrent position delete in a NaN partition. Today the replacement raises before validation runs. With only this change, a stale file replacement would commit over that delete and bring the deleted row back. This PR includes both commits, and only the second one belongs to it.

Dynamic overwrites with NaN keys used to raise. They can now reach two existing bugs, both of which need a file written under an older partition spec.

upsert() with NaN in a join column still raises, because create_match_filter builds literals from the join keys. That path is separate from this change.

Are these changes tested?

Yes. I added three tests.

  • test_replace_files_in_nan_identity_partition covers whole-file deletion, partial rewriting, overwrite and dynamic overwrite in a NaN identity partition, on the memory and SQL catalogs.
  • test_build_partition_predicate_with_nan_and_null checks the predicate for NaN, NULL and finite values.
  • test_file_overwrite_validates_concurrent_position_delete_in_nan_partition stages a file replacement, then commits a partition-scoped position delete from another table handle. It checks that the replacement fails validation and the deleted row stays gone, on the memory and SQL catalogs. PyIceberg cannot write delete files, so the helper commits the delete through overwrite() with _OverwriteFiles.new_manifest_writer patched to write a delete manifest.

All three fail on main with the NaN literal error and pass with both commits. Without the dependency, the concurrent-delete test fails because the stale replacement commits.

I also ran upsert() on a row in the NaN partition and delete(AlwaysTrue()). Both raise on main and succeed with the fix. These cases are not in the committed tests.

The full unit suite with both commits gives 4482 passed, 4 failed and 5 skipped, versus 4472 passed, 4 failed and 5 skipped on main. Three of the ten new passes come from the dependency and seven from this PR. The same four Hive Kerberos tests fail on both because the hive-kerberos extra cannot be installed in the test environment. prek run -a passed.

Are there any user-facing changes?

Yes. delete(), overwrite(), upsert() and dynamic_partition_overwrite() now work on NaN identity partitions, apart from the cases above. There is no API change. Please add the changelog label.

`Record.__eq__` compared the underlying lists, so two separately decoded
NaN values compared unequal while their `str`-based hashes matched.
`DeleteFileIndex` keys partition-scoped position deletes by partition
`Record`, so those deletes never applied to data files in a NaN identity
partition, and scans returned deleted rows. Commit validation missed
concurrently deleted files in NaN partitions for the same reason.

Compare elementwise when the lists differ, counting NaN floats at the
same position as equal. The hash is unchanged.

Closes apache#4279
_build_partition_predicate emitted EqualTo for every non-null partition
value. Expression literals reject NaN, so deleting, overwriting, or
upserting a file in a NaN identity partition raised, as did a dynamic
partition overwrite with NaN in the incoming data.

Closes apache#4277
@hcrosse
hcrosse marked this pull request as ready for review November 4, 2026 01:00
Copilot AI balanced review requested due to automatic review settings November 4, 2026 01:00
@hcrosse
hcrosse marked this pull request as draft November 4, 2026 01:00

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.

🟡 Changes recommended

The equality fast path repeats comparisons for unequal nested Records, causing exponential work with nesting depth instead of a single linear pass.

1 open issue
What changed in this PR

This PR fixes partition predicate construction for NaN identity partitions, allowing affected delete and replacement operations to proceed. It includes the dependency that makes NaN partition records compare equal for delete lookups and conflict validation.

Changes:

  • Emit IsNaN for NaN partition values while preserving NULL and finite-value handling.
  • Treat corresponding NaN floats as equal in Record.
  • Add regression coverage for file replacement, partition lookups, and concurrent-delete validation.
File Description
pyiceberg/​table/​__init__.py Build NaN-aware partition predicates.
pyiceberg/​typedef.py Add NaN-aware record equality.
tests/​test_typedef.py Cover NaN record equality and set deduplication.
tests/​table/​test_validate.py Check concurrent deletion conflicts in NaN partitions.
tests/​table/​test_init.py Verify predicates for NaN, NULL, and finite values.
tests/​table/​test_delete_file_index.py Verify delete lookup using NaN partition records.
tests/​table/​test_commit_retry.py Reject replacements conflicting with concurrent position deletes.
tests/​catalog/​test_catalog_behaviors.py Exercise deletes and overwrites in NaN identity partitions.

Review effort: Balanced

Comment thread pyiceberg/typedef.py
Comment on lines +207 to +211
if self._data == other._data:
return True
if len(self._data) != len(other._data):
return False
for a, b in zip(self._data, other._data, strict=True):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This comes from the commit this PR includes from #4280 and is fixed there. This PR picks up the fix when it merges main after that PR lands.

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.

delete() and overwrite() raise Cannot create expression literal from NaN on a NaN identity partition

2 participants