Skip to content

Commit 4b86925

Browse files
committed
Fix writes with time bucket partitions
Preserve integer bucket values during time partition conversion to avoid an AttributeError.
1 parent 068aae5 commit 4b86925

3 files changed

Lines changed: 57 additions & 5 deletions

File tree

‎pyiceberg/partitioning.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,8 +546,10 @@ def _(type: IcebergType, value: int | date | None) -> int | None:
546546

547547

548548
@_to_partition_representation.register(TimeType)
549-
def _(type: IcebergType, value: time | None) -> int | None:
550-
return time_to_micros(value) if value is not None else None
549+
def _(type: IcebergType, value: int | time | None) -> int | None:
550+
if value is None or isinstance(value, int):
551+
return value
552+
return time_to_micros(value)
551553

552554

553555
@_to_partition_representation.register(UUIDType)

‎tests/io/test_pyarrow.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import uuid
2323
import warnings
2424
from collections.abc import Iterator
25-
from datetime import date, datetime, timezone
25+
from datetime import date, datetime, time, timezone
2626
from pathlib import Path
2727
from typing import Any
2828
from unittest.mock import MagicMock, patch
@@ -93,7 +93,7 @@
9393
from pyiceberg.table import FileScanTask, TableProperties, WriteTask
9494
from pyiceberg.table.metadata import TableMetadataV2
9595
from pyiceberg.table.name_mapping import create_mapping_from_schema
96-
from pyiceberg.transforms import HourTransform, IdentityTransform
96+
from pyiceberg.transforms import BucketTransform, HourTransform, IdentityTransform
9797
from pyiceberg.typedef import UTF8, Properties, Record, TableVersion
9898
from pyiceberg.types import (
9999
BinaryType,
@@ -2784,6 +2784,40 @@ def test_partition_for_demo() -> None:
27842784
)
27852785

27862786

2787+
@pytest.mark.parametrize("format_version", [1, 2])
2788+
def test_append_time_bucket_partition(tmp_path: Path, format_version: int) -> None:
2789+
schema = Schema(NestedField(1, "event_time", TimeType(), required=False))
2790+
spec = PartitionSpec(PartitionField(1, 1000, BucketTransform(16), "event_time_bucket"))
2791+
data = pa.table(
2792+
{"event_time": [time(0), time(12, 34, 56, 123456), time(23, 59, 59, 999999), None, time(0)]},
2793+
schema=schema.as_arrow(),
2794+
)
2795+
2796+
with InMemoryCatalog("test", warehouse=tmp_path.as_uri()) as catalog:
2797+
catalog.create_namespace("default")
2798+
table = catalog.create_table(
2799+
"default.events", schema=schema, partition_spec=spec, properties={"format-version": str(format_version)}
2800+
)
2801+
table.append(data)
2802+
2803+
rows = table.scan().to_arrow()["event_time"].to_pylist()
2804+
assert rows.count(None) == 1
2805+
assert sorted(value for value in rows if value is not None) == sorted(
2806+
value for value in data["event_time"].to_pylist() if value is not None
2807+
)
2808+
2809+
files = table.inspect.data_files().to_pylist()
2810+
assert {file["partition"]["event_time_bucket"]: file["record_count"] for file in files} == {
2811+
12: 2,
2812+
11: 1,
2813+
8: 1,
2814+
None: 1,
2815+
}
2816+
for file in files:
2817+
bucket = file["partition"]["event_time_bucket"]
2818+
assert f"/event_time_bucket={bucket if bucket is not None else 'null'}/" in file["file_path"]
2819+
2820+
27872821
def test_partition_for_nested_field() -> None:
27882822
schema = Schema(
27892823
NestedField(id=1, name="foo", field_type=StringType(), required=True),

‎tests/test_transforms.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# under the License.
1818
# pylint: disable=eval-used,protected-access,redefined-outer-name
1919
from collections.abc import Callable
20-
from datetime import date, datetime
20+
from datetime import date, datetime, time
2121
from decimal import Decimal
2222
from typing import Annotated, Any
2323
from uuid import UUID
@@ -1643,6 +1643,22 @@ def test_to_partition_representation_timestamps(source_type: PrimitiveType, valu
16431643
assert _to_partition_representation(source_type, value) == expected
16441644

16451645

1646+
@pytest.mark.parametrize(
1647+
"value, expected",
1648+
[
1649+
pytest.param(None, None, id="none"),
1650+
pytest.param(time(0), 0, id="midnight"),
1651+
pytest.param(time(12, 34, 56, 123456), 45_296_123_456, id="microseconds"),
1652+
pytest.param(time(23, 59, 59, 999999), 86_399_999_999, id="end_of_day"),
1653+
pytest.param(0, 0, id="zero_int_passthrough"),
1654+
pytest.param(12, 12, id="bucket_int_passthrough"),
1655+
pytest.param(86_399_999_999, 86_399_999_999, id="time_int_passthrough"),
1656+
],
1657+
)
1658+
def test_to_partition_representation_time(value: int | time | None, expected: int | None) -> None:
1659+
assert _to_partition_representation(TimeType(), value) == expected
1660+
1661+
16461662
def test_to_partition_representation_unrecognized_type_raises() -> None:
16471663
with pytest.raises(ValueError, match="Type not recognized"):
16481664
_to_partition_representation(TimestampType(), "not-a-datetime-or-int")

0 commit comments

Comments
 (0)