From 4d8b947221b7f8c1bdedb3014b2fcf8ec21b4c02 Mon Sep 17 00:00:00 2001 From: Arjun Pakhan Date: Sat, 8 Aug 2026 19:20:21 +0000 Subject: [PATCH 1/2] fix(inspect): handle 4-byte bounds in _readable_bound after int/float type promotion (#3744) --- tests/table/test_inspect.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/table/test_inspect.py b/tests/table/test_inspect.py index c325af2033..fc1ff29cf6 100644 --- a/tests/table/test_inspect.py +++ b/tests/table/test_inspect.py @@ -14,7 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - from pathlib import PosixPath import pyarrow as pa @@ -23,7 +22,7 @@ from pyiceberg.conversions import to_bytes from pyiceberg.schema import Schema from pyiceberg.table.inspect import _readable_bound -from pyiceberg.types import NestedField, StringType +from pyiceberg.types import DoubleType, LongType, NestedField, StringType from tests.catalog.test_base import InMemoryCatalog @@ -68,3 +67,17 @@ def test_inspect_entries_and_files_render_null_bound(catalog: InMemoryCatalog) - files_metrics = tbl.inspect.files().to_pydict()["readable_metrics"][0]["s"] assert files_metrics["lower_bound"] is None assert files_metrics["upper_bound"] is None + + +def test_readable_bound_type_promotions() -> None: + # 4-byte LE representation of integer 10 -> b'\x0a\x00\x00\x00' + four_byte_int_bound = b"\x0a\x00\x00\x00" + + # 4-byte LE representation of float 10.0 -> b'\x00\x00\x20\x41' + four_byte_float_bound = b"\x00\x00\x20\x41" + + # Test int -> long promotion decoding + assert _readable_bound(LongType(), four_byte_int_bound) == 10 + + # Test float -> double promotion decoding + assert _readable_bound(DoubleType(), four_byte_float_bound) == 10.0 From 52b38f94983bbd3d458390efcc3933d819adc909 Mon Sep 17 00:00:00 2001 From: Arjun Pakhan Date: Sun, 9 Aug 2026 11:37:45 +0000 Subject: [PATCH 2/2] fix(inspect): handle 4-byte bounds in manifests() and add e2e test (#3744) --- pyiceberg/table/inspect.py | 15 ++++++++---- tests/table/test_inspect.py | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/pyiceberg/table/inspect.py b/pyiceberg/table/inspect.py index 5cae743313..3dab12f7cb 100644 --- a/pyiceberg/table/inspect.py +++ b/pyiceberg/table/inspect.py @@ -26,7 +26,7 @@ from pyiceberg.manifest import DataFile, DataFileContent, ManifestContent, ManifestFile, PartitionFieldSummary from pyiceberg.partitioning import PartitionSpec from pyiceberg.table.snapshots import Snapshot, ancestors_of -from pyiceberg.types import PrimitiveType +from pyiceberg.types import DoubleType, FloatType, IntegerType, LongType, PrimitiveType from pyiceberg.utils.concurrent import ExecutorFactory from pyiceberg.utils.singleton import _convert_to_hashable_type @@ -39,7 +39,13 @@ def _readable_bound(field_type: PrimitiveType, bound: bytes | None) -> Any | None: - return from_bytes(field_type, bound) if bound is not None else None + if bound is None: + return None + if isinstance(field_type, LongType) and len(bound) == 4: + return from_bytes(IntegerType(), bound) + if isinstance(field_type, DoubleType) and len(bound) == 4: + return from_bytes(FloatType(), bound) + return from_bytes(field_type, bound) class InspectTable: @@ -183,7 +189,6 @@ def _readable_metrics_struct(bound_type: PrimitiveType) -> pa.StructType: "value_count": value_counts.get(field.field_id), "null_value_count": null_value_counts.get(field.field_id), "nan_value_count": nan_value_counts.get(field.field_id), - # Makes them readable "lower_bound": _readable_bound(field.field_type, lower_bounds.get(field.field_id)), "upper_bound": _readable_bound(field.field_type, upper_bounds.get(field.field_id)), } @@ -420,7 +425,7 @@ def _partition_summaries_to_rows( lower_bound = ( ( field.transform.to_human_string( - partition_field_type, from_bytes(partition_field_type, field_summary.lower_bound) + partition_field_type, _readable_bound(partition_field_type, field_summary.lower_bound) ) ) if field_summary.lower_bound @@ -429,7 +434,7 @@ def _partition_summaries_to_rows( upper_bound = ( ( field.transform.to_human_string( - partition_field_type, from_bytes(partition_field_type, field_summary.upper_bound) + partition_field_type, _readable_bound(partition_field_type, field_summary.upper_bound) ) ) if field_summary.upper_bound diff --git a/tests/table/test_inspect.py b/tests/table/test_inspect.py index fc1ff29cf6..276c8953ac 100644 --- a/tests/table/test_inspect.py +++ b/tests/table/test_inspect.py @@ -81,3 +81,52 @@ def test_readable_bound_type_promotions() -> None: # Test float -> double promotion decoding assert _readable_bound(DoubleType(), four_byte_float_bound) == 10.0 + + +def test_inspect_files_type_promoted_bounds_e2e() -> None: + import shutil + import tempfile + + import pyarrow as pa + + from pyiceberg.catalog.sql import SqlCatalog + from pyiceberg.schema import Schema + from pyiceberg.types import IntegerType, LongType, NestedField, StringType + + warehouse = tempfile.mkdtemp(prefix="iceberg_test_e2e_") + try: + catalog = SqlCatalog("test_e2e", uri=f"sqlite:///{warehouse}/catalog.db", warehouse=f"file://{warehouse}") + catalog.create_namespace("ns") + + tbl = catalog.create_table( + "ns.t", + schema=Schema( + NestedField(1, "name", StringType(), required=False), + NestedField(2, "qty", IntegerType(), required=False), + ), + ) + + tbl.append( + pa.Table.from_pylist( + [{"name": "a", "qty": 10}], + schema=pa.schema([pa.field("name", pa.string()), pa.field("qty", pa.int32())]), + ) + ) + + # Promote int -> long + with tbl.update_schema() as update: + update.update_column("qty", field_type=LongType()) + + tbl = catalog.load_table("ns.t") + + # Test metadata inspection tables + files_df = tbl.inspect.files() + assert files_df.num_rows == 1 + + entries_df = tbl.inspect.entries() + assert entries_df.num_rows == 1 + + manifests_df = tbl.inspect.manifests() + assert manifests_df.num_rows >= 1 + finally: + shutil.rmtree(warehouse, ignore_errors=True)