This is an automated email from the ASF dual-hosted git repository.
kevinjqliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git
The following commit(s) were added to refs/heads/main by this push:
new f507dbd1 Leverage Iceberg-Rust for all the transforms (#1833)
f507dbd1 is described below
commit f507dbd1c29c7d21d24c6f2f7b4479cde2be16de
Author: Fokko Driesprong <[email protected]>
AuthorDate: Sat Jun 7 20:40:26 2025 +0200
Leverage Iceberg-Rust for all the transforms (#1833)
# Rationale for this change
Testing out to use Iceberg Rust for all of the transforms. I think we
have some rounding error in
https://github.com/apache/iceberg-rust/pull/1128/
Closes https://github.com/apache/iceberg-python/issues/1591
# Are these changes tested?
# Are there any user-facing changes?
<!-- In the case of user-facing changes, please add the changelog label.
-->
---------
Co-authored-by: Kevin Liu <[email protected]>
---
poetry.lock | 54 ++++++++---------
pyiceberg/transforms.py | 128 ++++++++++++---------------------------
pyproject.toml | 4 +-
tests/table/test_partitioning.py | 12 +---
tests/test_transforms.py | 35 +----------
5 files changed, 73 insertions(+), 160 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 09ea4104..f41ef7d8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -58,7 +58,7 @@ description = "Happy Eyeballs for asyncio"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\""
+markers = "extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\""
files = [
{file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash =
"sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"},
{file = "aiohappyeyeballs-2.6.1.tar.gz", hash =
"sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"},
@@ -71,7 +71,7 @@ description = "Async http client/server framework (asyncio)"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\""
+markers = "extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\""
files = [
{file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_universal2.whl", hash =
"sha256:e2bc827c01f75803de77b134afdbf74fa74b62970eafdf190f3244931d7a5c0d"},
{file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_x86_64.whl", hash =
"sha256:e365034c5cf6cf74f57420b57682ea79e19eb29033399dd3f40de4d0171998fa"},
@@ -196,7 +196,7 @@ description = "aiosignal: a list of registered asynchronous
callbacks"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "(extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\") and
(extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\" or extra ==
\"ray\")"
+markers = "(extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\") and
(extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\" or extra ==
\"ray\")"
files = [
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash =
"sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
{file = "aiosignal-1.3.2.tar.gz", hash =
"sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
@@ -248,7 +248,7 @@ description = "Timeout context manager for asyncio programs"
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "(extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\") and
python_version <= \"3.10\""
+markers = "(extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\") and
python_version <= \"3.10\""
files = [
{file = "async_timeout-5.0.1-py3-none-any.whl", hash =
"sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
{file = "async_timeout-5.0.1.tar.gz", hash =
"sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
@@ -265,7 +265,7 @@ files = [
{file = "attrs-25.3.0-py3-none-any.whl", hash =
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
{file = "attrs-25.3.0.tar.gz", hash =
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
]
-markers = {main = "(extra == \"adlfs\" or extra == \"gcsfs\" or extra ==
\"s3fs\") and (extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\" or
extra == \"ray\")"}
+markers = {main = "(extra == \"s3fs\" or extra == \"adlfs\" or extra ==
\"gcsfs\") and (extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\"
or extra == \"ray\")"}
[package.extras]
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"",
"hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\"
and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)",
"pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation ==
\"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
@@ -467,7 +467,7 @@ files = [
{file = "boto3-1.37.3-py3-none-any.whl", hash =
"sha256:2063b40af99fd02f6228ff52397b552ff3353831edaf8d25cc04801827ab9794"},
{file = "boto3-1.37.3.tar.gz", hash =
"sha256:21f3ce0ef111297e63a6eb998a25197b8c10982970c320d4c6e8db08be2157be"},
]
-markers = {main = "extra == \"dynamodb\" or extra == \"glue\" or extra ==
\"rest-sigv4\""}
+markers = {main = "extra == \"glue\" or extra == \"dynamodb\" or extra ==
\"rest-sigv4\""}
[package.dependencies]
botocore = ">=1.37.3,<1.38.0"
@@ -488,7 +488,7 @@ files = [
{file = "botocore-1.37.3-py3-none-any.whl", hash =
"sha256:d01bd3bf4c80e61fa88d636ad9f5c9f60a551d71549b481386c6b4efe0bb2b2e"},
{file = "botocore-1.37.3.tar.gz", hash =
"sha256:fe8403eb55a88faf9b0f9da6615e5bee7be056d75e17af66c3c8f0a3b0648da4"},
]
-markers = {main = "extra == \"dynamodb\" or extra == \"glue\" or extra ==
\"rest-sigv4\" or extra == \"s3fs\""}
+markers = {main = "extra == \"glue\" or extra == \"dynamodb\" or extra ==
\"rest-sigv4\" or extra == \"s3fs\""}
[package.dependencies]
jmespath = ">=0.7.1,<2.0.0"
@@ -1449,7 +1449,7 @@ description = "A list-like structure which implements
collections.abc.MutableSeq
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "(extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\") and
(extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\" or extra ==
\"ray\")"
+markers = "(extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\") and
(extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\" or extra ==
\"ray\")"
files = [
{file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash =
"sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"},
{file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash =
"sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"},
@@ -2162,7 +2162,7 @@ files = [
{file = "jmespath-1.0.1-py3-none-any.whl", hash =
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
{file = "jmespath-1.0.1.tar.gz", hash =
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
-markers = {main = "extra == \"dynamodb\" or extra == \"glue\" or extra ==
\"rest-sigv4\" or extra == \"s3fs\""}
+markers = {main = "extra == \"glue\" or extra == \"dynamodb\" or extra ==
\"rest-sigv4\" or extra == \"s3fs\""}
[[package]]
name = "joserfc"
@@ -2980,7 +2980,7 @@ description = "multidict implementation"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\""
+markers = "extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\""
files = [
{file = "multidict-6.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash =
"sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1"},
{file = "multidict-6.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash =
"sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2"},
@@ -3574,7 +3574,7 @@ description = "Accelerated property cache"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\""
+markers = "extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\""
files = [
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash =
"sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"},
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash =
"sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"},
@@ -3882,7 +3882,7 @@ files = [
{file = "pyarrow-20.0.0-cp39-cp39-win_amd64.whl", hash =
"sha256:9965a050048ab02409fb7cbbefeedba04d3d67f2cc899eff505cc084345959ca"},
{file = "pyarrow-20.0.0.tar.gz", hash =
"sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1"},
]
-markers = {main = "extra == \"daft\" or extra == \"duckdb\" or extra ==
\"pandas\" or extra == \"pyarrow\" or extra == \"ray\""}
+markers = {main = "extra == \"pyarrow\" or extra == \"pandas\" or extra ==
\"duckdb\" or extra == \"ray\" or extra == \"daft\""}
[package.extras]
test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"]
@@ -4080,19 +4080,19 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyiceberg-core"
-version = "0.4.0"
+version = "0.5.1"
description = ""
optional = true
-python-versions = "*"
+python-versions = "~=3.9"
groups = ["main"]
-markers = "extra == \"pyiceberg-core\""
+markers = "extra == \"pyarrow\" or extra == \"pyiceberg-core\""
files = [
- {file =
"pyiceberg_core-0.4.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl",
hash =
"sha256:5aec569271c96e18428d542f9b7007117a7232c06017f95cb239d42e952ad3b4"},
- {file =
"pyiceberg_core-0.4.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
hash =
"sha256:5e74773e58efa4df83aba6f6265cdd41e446fa66fa4e343ca86395fed9f209ae"},
- {file =
"pyiceberg_core-0.4.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",
hash =
"sha256:7675d21a54bf3753c740d8df78ad7efe33f438096844e479d4f3493f84830925"},
- {file =
"pyiceberg_core-0.4.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
hash =
"sha256:7058ad935a40b1838e4cdc5febd768878c1a51f83dca005d5a52a7fa280a2489"},
- {file = "pyiceberg_core-0.4.0-cp39-abi3-win_amd64.whl", hash =
"sha256:a83eb4c2307ae3dd321a9360828fb043a4add2cc9797bef0bafa20894488fb07"},
- {file = "pyiceberg_core-0.4.0.tar.gz", hash =
"sha256:d2e6138707868477b806ed354aee9c476e437913a331cb9ad9ad46b4054cd11f"},
+ {file =
"pyiceberg_core-0.5.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl",
hash =
"sha256:f5bffe70f036d4409ab5f8a2a52353635dfa9afa665e84eb5fb0415d12171aaa"},
+ {file =
"pyiceberg_core-0.5.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",
hash =
"sha256:aa5d0d00efeb7c03ad48b17577452f42398f398b41034d2695a294c872365fcd"},
+ {file =
"pyiceberg_core-0.5.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
hash =
"sha256:6ae88f65f68898a680dde4d83179903d14b9ef2ca8f7b8715fca5d91a2aa24cd"},
+ {file = "pyiceberg_core-0.5.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash
= "sha256:9d02f82292d2d01cc6b43f79eadf1209e84457cf0a2b945a2ed834245d6a45f6"},
+ {file = "pyiceberg_core-0.5.1-cp39-abi3-win_amd64.whl", hash =
"sha256:aad22d3ea743fcf941336542fcf565e16b28b9131087d70e1b03924005334af3"},
+ {file = "pyiceberg_core-0.5.1.tar.gz", hash =
"sha256:5359750e690169e06e4c20b74854f0048abbb5fe74b75919247d87cf2af2fcf4"},
]
[[package]]
@@ -4392,7 +4392,7 @@ description = "World timezone definitions, modern and
historical"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"gcsfs\" or extra == \"pandas\" or extra == \"ray\""
+markers = "extra == \"pandas\" or extra == \"ray\" or extra == \"gcsfs\""
files = [
{file = "pytz-2025.2-py2.py3-none-any.whl", hash =
"sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
{file = "pytz-2025.2.tar.gz", hash =
"sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
@@ -4992,7 +4992,7 @@ files = [
{file = "s3transfer-0.11.3-py3-none-any.whl", hash =
"sha256:ca855bdeb885174b5ffa95b9913622459d4ad8e331fc98eb01e6d5eb6a30655d"},
{file = "s3transfer-0.11.3.tar.gz", hash =
"sha256:edae4977e3a122445660c7c114bba949f9d191bae3b34a096f18a1c8c354527a"},
]
-markers = {main = "extra == \"dynamodb\" or extra == \"glue\" or extra ==
\"rest-sigv4\""}
+markers = {main = "extra == \"glue\" or extra == \"dynamodb\" or extra ==
\"rest-sigv4\""}
[package.dependencies]
botocore = ">=1.36.0,<2.0a.0"
@@ -5426,7 +5426,7 @@ description = "Fast, Extensible Progress Meter"
optional = true
python-versions = ">=3.7"
groups = ["main"]
-markers = "extra == \"daft\" or extra == \"hf\""
+markers = "extra == \"hf\" or extra == \"daft\""
files = [
{file = "tqdm-4.67.1-py3-none-any.whl", hash =
"sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
{file = "tqdm-4.67.1.tar.gz", hash =
"sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
@@ -5726,7 +5726,7 @@ description = "Yet another URL library"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"adlfs\" or extra == \"gcsfs\" or extra == \"s3fs\""
+markers = "extra == \"s3fs\" or extra == \"adlfs\" or extra == \"gcsfs\""
files = [
{file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash =
"sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"},
{file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash =
"sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"},
@@ -5964,7 +5964,7 @@ hive = ["thrift"]
hive-kerberos = ["kerberos", "thrift", "thrift-sasl"]
pandas = ["pandas", "pyarrow"]
polars = ["polars"]
-pyarrow = ["pyarrow"]
+pyarrow = ["pyarrow", "pyiceberg-core"]
pyiceberg-core = ["pyiceberg-core"]
ray = ["pandas", "pyarrow", "ray", "ray"]
rest-sigv4 = ["boto3"]
@@ -5977,4 +5977,4 @@ zstandard = ["zstandard"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9.2, !=3.9.7"
-content-hash =
"fb2fa0b2764e8528206e2e401ad35a12b8edf31350d130796d5bb279d8785b58"
+content-hash =
"93eda79de3991bea0b9520f69372d146f7fd3d128cd982ea5afb78e8ec5d94f2"
diff --git a/pyiceberg/transforms.py b/pyiceberg/transforms.py
index 19889a98..8dda85a4 100644
--- a/pyiceberg/transforms.py
+++ b/pyiceberg/transforms.py
@@ -111,6 +111,36 @@ def _transform_literal(func: Callable[[L], L], lit:
Literal[L]) -> Literal[L]:
return literal(func(lit.value))
+def _pyiceberg_transform_wrapper(
+ transform_func: Callable[["ArrayLike", Any], "ArrayLike"],
+ *args: Any,
+ expected_type: Optional["pa.DataType"] = None,
+) -> Callable[["ArrayLike"], "ArrayLike"]:
+ try:
+ import pyarrow as pa
+ except ModuleNotFoundError as e:
+ raise ModuleNotFoundError("For partition transforms, PyArrow needs to
be installed") from e
+
+ def _transform(array: "ArrayLike") -> "ArrayLike":
+ def _cast_if_needed(arr: "ArrayLike") -> "ArrayLike":
+ if expected_type is not None:
+ return arr.cast(expected_type)
+ else:
+ return arr
+
+ if isinstance(array, pa.Array):
+ return _cast_if_needed(transform_func(array, *args))
+ elif isinstance(array, pa.ChunkedArray):
+ result_chunks = []
+ for arr in array.iterchunks():
+ result_chunks.append(_cast_if_needed(transform_func(arr,
*args)))
+ return pa.chunked_array(result_chunks)
+ else:
+ raise ValueError(f"PyArrow array can only be of type pa.Array or
pa.ChunkedArray, but found {type(array)}")
+
+ return _transform
+
+
class Transform(IcebergRootModel[str], ABC, Generic[S, T]):
"""Transform base class for concrete transforms.
@@ -175,27 +205,6 @@ class Transform(IcebergRootModel[str], ABC, Generic[S, T]):
@abstractmethod
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]": ...
- def _pyiceberg_transform_wrapper(
- self, transform_func: Callable[["ArrayLike", Any], "ArrayLike"],
*args: Any
- ) -> Callable[["ArrayLike"], "ArrayLike"]:
- try:
- import pyarrow as pa
- except ModuleNotFoundError as e:
- raise ModuleNotFoundError("For bucket/truncate transforms, PyArrow
needs to be installed") from e
-
- def _transform(array: "ArrayLike") -> "ArrayLike":
- if isinstance(array, pa.Array):
- return transform_func(array, *args)
- elif isinstance(array, pa.ChunkedArray):
- result_chunks = []
- for arr in array.iterchunks():
- result_chunks.append(transform_func(arr, *args))
- return pa.chunked_array(result_chunks)
- else:
- raise ValueError(f"PyArrow array can only be of type pa.Array
or pa.ChunkedArray, but found {type(array)}")
-
- return _transform
-
def parse_transform(v: Any) -> Transform[Any, Any]:
if isinstance(v, str):
@@ -375,7 +384,7 @@ class BucketTransform(Transform[S, int]):
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]":
from pyiceberg_core import transform as pyiceberg_core_transform
- return
self._pyiceberg_transform_wrapper(pyiceberg_core_transform.bucket,
self._num_buckets)
+ return _pyiceberg_transform_wrapper(pyiceberg_core_transform.bucket,
self._num_buckets)
@property
def supports_pyarrow_transform(self) -> bool:
@@ -501,22 +510,9 @@ class YearTransform(TimeTransform[S]):
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]":
import pyarrow as pa
- import pyarrow.compute as pc
-
- if isinstance(source, DateType):
- epoch = pa.scalar(datetime.EPOCH_DATE)
- elif isinstance(source, TimestampType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMP)
- elif isinstance(source, TimestamptzType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMPTZ)
- elif isinstance(source, TimestampNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMP).cast(pa.timestamp("ns"))
- elif isinstance(source, TimestamptzNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMPTZ).cast(pa.timestamp("ns"))
- else:
- raise ValueError(f"Cannot apply year transform for type: {source}")
+ from pyiceberg_core import transform as pyiceberg_core_transform
- return lambda v: pc.years_between(epoch, v) if v is not None else None
+ return _pyiceberg_transform_wrapper(pyiceberg_core_transform.year,
expected_type=pa.int32())
class MonthTransform(TimeTransform[S]):
@@ -575,28 +571,9 @@ class MonthTransform(TimeTransform[S]):
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]":
import pyarrow as pa
- import pyarrow.compute as pc
-
- if isinstance(source, DateType):
- epoch = pa.scalar(datetime.EPOCH_DATE)
- elif isinstance(source, TimestampType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMP)
- elif isinstance(source, TimestamptzType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMPTZ)
- elif isinstance(source, TimestampNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMP).cast(pa.timestamp("ns"))
- elif isinstance(source, TimestamptzNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMPTZ).cast(pa.timestamp("ns"))
- else:
- raise ValueError(f"Cannot apply month transform for type:
{source}")
-
- def month_func(v: pa.Array) -> pa.Array:
- return pc.add(
- pc.multiply(pc.years_between(epoch, v), pa.scalar(12)),
- pc.add(pc.month(v), pa.scalar(-1)),
- )
+ from pyiceberg_core import transform as pyiceberg_core_transform
- return lambda v: month_func(v) if v is not None else None
+ return _pyiceberg_transform_wrapper(pyiceberg_core_transform.month,
expected_type=pa.int32())
class DayTransform(TimeTransform[S]):
@@ -663,22 +640,9 @@ class DayTransform(TimeTransform[S]):
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]":
import pyarrow as pa
- import pyarrow.compute as pc
-
- if isinstance(source, DateType):
- epoch = pa.scalar(datetime.EPOCH_DATE)
- elif isinstance(source, TimestampType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMP)
- elif isinstance(source, TimestamptzType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMPTZ)
- elif isinstance(source, TimestampNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMP).cast(pa.timestamp("ns"))
- elif isinstance(source, TimestamptzNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMPTZ).cast(pa.timestamp("ns"))
- else:
- raise ValueError(f"Cannot apply day transform for type: {source}")
+ from pyiceberg_core import transform as pyiceberg_core_transform
- return lambda v: pc.days_between(epoch, v) if v is not None else None
+ return _pyiceberg_transform_wrapper(pyiceberg_core_transform.day,
expected_type=pa.int32())
class HourTransform(TimeTransform[S]):
@@ -728,21 +692,9 @@ class HourTransform(TimeTransform[S]):
return "HourTransform()"
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]":
- import pyarrow as pa
- import pyarrow.compute as pc
-
- if isinstance(source, TimestampType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMP)
- elif isinstance(source, TimestamptzType):
- epoch = pa.scalar(datetime.EPOCH_TIMESTAMPTZ)
- elif isinstance(source, TimestampNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMP).cast(pa.timestamp("ns"))
- elif isinstance(source, TimestamptzNanoType):
- epoch =
pa.scalar(datetime.EPOCH_TIMESTAMPTZ).cast(pa.timestamp("ns"))
- else:
- raise ValueError(f"Cannot apply hour transform for type: {source}")
+ from pyiceberg_core import transform as pyiceberg_core_transform
- return lambda v: pc.hours_between(epoch, v) if v is not None else None
+ return _pyiceberg_transform_wrapper(pyiceberg_core_transform.hour)
def _base64encode(buffer: bytes) -> str:
@@ -965,7 +917,7 @@ class TruncateTransform(Transform[S, S]):
def pyarrow_transform(self, source: IcebergType) -> "Callable[[pa.Array],
pa.Array]":
from pyiceberg_core import transform as pyiceberg_core_transform
- return
self._pyiceberg_transform_wrapper(pyiceberg_core_transform.truncate,
self._width)
+ return _pyiceberg_transform_wrapper(pyiceberg_core_transform.truncate,
self._width)
@property
def supports_pyarrow_transform(self) -> bool:
diff --git a/pyproject.toml b/pyproject.toml
index d906dff6..9699f285 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -81,7 +81,7 @@ psycopg2-binary = { version = ">=2.9.6", optional = true }
sqlalchemy = { version = "^2.0.18", optional = true }
getdaft = { version = ">=0.2.12", optional = true }
cachetools = ">=5.5,<7.0"
-pyiceberg-core = { version = "^0.4.0", optional = true }
+pyiceberg-core = { version = "^0.5.1", optional = true }
polars = { version = "^1.21.0", optional = true }
thrift-sasl = { version = ">=0.4.3", optional = true }
kerberos = {version = "^1.3.1", optional = true}
@@ -289,7 +289,7 @@ generate-setup-file = false
script = "build-module.py"
[tool.poetry.extras]
-pyarrow = ["pyarrow"]
+pyarrow = ["pyarrow", "pyiceberg-core"]
pandas = ["pandas", "pyarrow"]
duckdb = ["duckdb", "pyarrow"]
ray = ["ray", "pyarrow", "pandas"]
diff --git a/tests/table/test_partitioning.py b/tests/table/test_partitioning.py
index 57ab3e32..0fe22391 100644
--- a/tests/table/test_partitioning.py
+++ b/tests/table/test_partitioning.py
@@ -186,8 +186,8 @@ def test_partition_type(table_schema_simple: Schema) ->
None:
(DecimalType(5, 9), Decimal(19.25)),
(DateType(), datetime.date(1925, 5, 22)),
(TimeType(), datetime.time(19, 25, 00)),
- (TimestampType(), datetime.datetime(19, 5, 1, 22, 1, 1)),
- (TimestamptzType(), datetime.datetime(19, 5, 1, 22, 1, 1,
tzinfo=datetime.timezone.utc)),
+ (TimestampType(), datetime.datetime(2022, 5, 1, 22, 1, 1)),
+ (TimestamptzType(), datetime.datetime(2022, 5, 1, 22, 1, 1,
tzinfo=datetime.timezone.utc)),
(StringType(), "abc"),
(UUIDType(), UUID("12345678-1234-5678-1234-567812345678").bytes),
(FixedType(5), 'b"\x8e\xd1\x87\x01"'),
@@ -208,13 +208,7 @@ def
test_transform_consistency_with_pyarrow_transform(source_type: PrimitiveType
]
for t in all_transforms:
if t.can_transform(source_type):
- try:
- assert t.transform(source_type)(value) ==
t.pyarrow_transform(source_type)(pa.array([value])).to_pylist()[0]
- except ValueError as e:
- # Skipping unsupported feature
- if "FeatureUnsupported => Unsupported data type for truncate
transform" in str(e):
- continue
- raise
+ assert t.transform(source_type)(value) ==
t.pyarrow_transform(source_type)(pa.array([value])).to_pylist()[0]
def test_deserialize_partition_field_v2() -> None:
diff --git a/tests/test_transforms.py b/tests/test_transforms.py
index d22c94cc..f8d3ac9d 100644
--- a/tests/test_transforms.py
+++ b/tests/test_transforms.py
@@ -16,7 +16,7 @@
# specific language governing permissions and limitations
# under the License.
# pylint: disable=eval-used,protected-access,redefined-outer-name
-from datetime import date, datetime
+from datetime import date
from decimal import Decimal
from typing import Annotated, Any, Callable, Optional, Union
from uuid import UUID
@@ -24,7 +24,6 @@ from uuid import UUID
import mmh3 as mmh3
import pyarrow as pa
import pytest
-import pytz
from pydantic import (
BeforeValidator,
PlainSerializer,
@@ -1654,38 +1653,6 @@ def test_bucket_pyarrow_transforms(
assert expected == transform.pyarrow_transform(source_type)(input_arr)
-# pyiceberg_core currently does not support bucket transform on timestamp_ns
and timestamptz_ns
-# https://github.com/apache/iceberg-rust/issues/1110
[email protected](
- "source_type, input_arr, num_buckets",
- [
- (
- TimestampNanoType(),
- pa.array([datetime(1970, 1, 1, 0, 0, 0), datetime(2025, 2, 26, 1,
2, 3)], type=pa.timestamp(unit="ns")),
- 10,
- ),
- (
- TimestamptzNanoType(),
- pa.array(
- [datetime(1970, 1, 1, 0, 0, 0), datetime(2025, 2, 26, 1, 2,
3)],
- type=pa.timestamp(unit="ns", tz=pytz.timezone("Etc/GMT+10")),
- ),
- 10,
- ),
- ],
-)
-def test_unsupported_bucket_pyarrow_transform(
- source_type: PrimitiveType,
- input_arr: Union[pa.Array, pa.ChunkedArray],
- num_buckets: int,
-) -> None:
- transform: Transform[Any, Any] = BucketTransform(num_buckets=num_buckets)
- with pytest.raises(ValueError) as exc_info:
- transform.pyarrow_transform(source_type)(input_arr)
-
- assert "FeatureUnsupported => Unsupported data type for bucket transform"
in str(exc_info.value)
-
-
@pytest.mark.parametrize(
"source_type, input_arr, expected, width",
[