`count()` and `to_arrow()` disagree for `!=` on a nullable identity partition
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 25/100
Hướng nghiên cứu
The mismatch comes from the residual evaluator in pyiceberg/expressions/visitors.py, which count() in pyiceberg/table/__init__.py relies on, while to_arrow() re-applies the row filter in pyiceberg/io/pyarrow.py. Start by reproducing the snippet and comparing both paths. Choosing whether NULL matches != is an open design question, and PR #4278 is already open against this issue, so check it first. Done means count() equals len(to_arrow()) for the same scan, with tests covering NULL partitions.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Apache Iceberg version
0.12.0 (latest release)
Please describe the bug 🐞
For a table identity-partitioned on a nullable string column p, filtering with p != 'a' gives scan.count() == 9 but len(scan.to_arrow()) == 4. The table has 3 rows with p = 'a', 4 with p = 'b', and 5 with p = NULL. count() includes the NULL rows, while to_arrow() excludes them. to_arrow_batch_reader() also returns 4 rows.
I reproduced this on PyIceberg 0.12.0 and main at e3564a54c10960307147377c578680276865ed18, with PyArrow 26.0.1 on Linux/amd64.
Install pyiceberg[pyarrow,sql-sqlite]==0.12.0 and pyarrow==26.0.1. The reproduction uses an in-process SQLite catalog and a temporary warehouse:
import tempfile
import pyarrow as pa
from pyiceberg.catalog.sql import SqlCatalog
from pyiceberg.expressions import NotEqualTo
from pyiceberg.partitioning import PartitionField, PartitionSpec
from pyiceberg.schema import Schema
from pyiceberg.transforms import IdentityTransform
from pyiceberg.types import LongType, NestedField, StringType
warehouse = tempfile.mkdtemp()
catalog = SqlCatalog(
"default", uri=f"sqlite:///{warehouse}/catalog.db", warehouse=f"file://{warehouse}"
)
catalog.create_namespace("ns")
schema = Schema(
NestedField(1, "id", LongType(), required=False),
NestedField(2, "p", StringType(), required=False),
NestedField(3, "v", LongType(), required=False),
)
spec = PartitionSpec(
PartitionField(source_id=2, field_id=1000, transform=IdentityTransform(), name="p")
)
table = catalog.create_table("ns.t", schema=schema, partition_spec=spec)
table.append(
pa.table(
{
"id": list(range(1, 13)),
"p": ["a"] * 3 + ["b"] * 4 + [None] * 5,
"v": [1, 2, None, 1, 2, None, 3, 1, None, 2, 3, None],
},
schema=pa.schema([("id", pa.int64()), ("p", pa.string()), ("v", pa.int64())]),
)
)
for field, value in [("p", "a"), ("v", 1)]:
scan = table.scan(row_filter=NotEqualTo(field, value))
print(
f"{field}: count()={scan.count()} to_arrow()={len(scan.to_arrow())} batch_reader={len(scan.to_arrow_batch_reader().read_all())}"
)
Output on both 0.12.0 and main at e3564a54:
p: count()=9 to_arrow()=4 batch_reader=4
v: count()=5 to_arrow()=5 batch_reader=5
The NULL-partition task has residual=AlwaysTrue() because the residual evaluator compares None != 'a', so count() adds the file's record_count of 5 without reading the file. to_arrow() does not use the residual: it passes the scan's full row filter to ArrowScan and re-applies that filter to each file. Arrow evaluates NULL != 'a' to null, and the scanner drops those rows.
Two null conventions are involved. PyIceberg's row evaluator, Java's Evaluator.notEq, and Java's ResidualEvaluator.notEq treat NULL as matching !=, agreeing with count() here. Java uses a nulls-first string comparator. Arrow treats NULL as not matching, agreeing with to_arrow(). When count() must read a file, it follows Arrow too: for the non-partition column v, both methods return 5 for NotEqualTo("v", 1), while the row evaluator matches 9.
Not(EqualTo("p", "a")) and NotStartsWith("p", "a") also give 9 and 4. A two-value NotIn("p", {"a", "c"}) returns 9 from both methods, because Arrow's ~isin keeps NULL rows. The table in https://github.com/apache/iceberg-python/pull/3918 shows the same difference.
Expected: count() returns len(to_arrow()) for the same scan. Whether NULL rows match != is the choice left open in https://github.com/apache/iceberg-python/pull/3918. That draft PR described the two conventions and their implications for delete() and overwrite(), and it was closed as stale.
Related:
- https://github.com/apache/iceberg-python/issues/3498 covers other residual and evaluator null cases, rather than this
count()versusto_arrow()mismatch. - The open https://github.com/apache/iceberg-python/pull/4219 addresses https://github.com/apache/iceberg-python/issues/4184 by making
delete()remove exactly the rows returned byscan(row_filter=f), including!=on a column containing NULL. That tiesdelete()to Arrow's convention. - https://github.com/apache/iceberg-python/issues/4197 compares the same read paths for an expired snapshot ID, rather than a nullable partition filter. The merged https://github.com/apache/iceberg-python/pull/4218 made
count()raise liketo_arrow()for that case.
Willingness to contribute
- I can contribute a fix for this bug independently
- I would be willing to contribute a fix for this bug with guidance from the Iceberg community
- I cannot contribute a fix for this bug at this time
- Ngôn ngữ chính
- Python
- Star
- 1.2k
- Fork
- 654
- Merge trung bình
- 2 ngày 11 giờ
- Pull request đã merge (30 ngày)
- 122
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của apache/iceberg-python
-
`inspect.files()` and the other file metadata tables raise `ArrowInvalid` when a snapshot has no manifestsCó thể đã có người làm @YuangGao đã nhận 10 ngày trước. Đang mởkind:bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
apache/iceberg-python#4269 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`inspect.entries()` raises TypeError when a data file has no column_sizesCó thể đã có người làm @Rahul-cs50 đã nhận 14 ngày trước. Đang mởkind:bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
apache/iceberg-python#4186 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`older_than` reads a naive datetime as UTC and expires snapshots it should keepCó thể đã có người làm @Rahul-cs50 đã nhận 13 ngày trước. Đang mởkind:bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
apache/iceberg-python#4182 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
REST: Send empty first-page pageToken when rest-page-size is setCó thể đã có người làm @ehddnr301 đã nhận 32 ngày trước. Đang mởkind:bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
apache/iceberg-python#4096 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
PyArrowFileIO: every small S3 write is a 3-request multipart upload; expose allow_delayed_openCó thể đã có người làm @David-Banquet đã nhận 29 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
apache/iceberg-python#4093 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của apache/iceberg-python
Issue tương tự
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 80/100
glossyio/traffic-monitor#312 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
local devenv exampleĐang mởdocumentation enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
es-ude/elastic-ai.creator#1046 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
ENH: permit cachetools >= 7Đang mởfeature request
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
confident-ai/deepteam#306 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
SslLibrary crashes with unhandled OSError: port/proto not found on non-standard SSL portsCó thể đã có người làm @himanchalkaushale đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
OWASP/Nettacker#1884 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày