| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| |
| import uuid |
| from datetime import date, datetime |
| from pathlib import Path |
| |
| import datafusion |
| import pyarrow as pa |
| import pytest |
| from datafusion import SessionContext |
| from packaging.version import Version |
| from pyiceberg.catalog import Catalog, load_catalog |
| from pyiceberg_core.datafusion import IcebergDataFusionTable |
| |
| if Version(datafusion.__version__) < Version("53.0.0"): |
| pytest.skip( |
| "Iceberg table provider requires datafusion>=53 for FFI compatibility", |
| allow_module_level=True, |
| ) |
| |
| |
| @pytest.fixture(scope="session") |
| def warehouse(tmp_path_factory: pytest.TempPathFactory) -> Path: |
| return tmp_path_factory.mktemp("warehouse") |
| |
| |
| @pytest.fixture(scope="session") |
| def catalog(warehouse: Path): |
| catalog = load_catalog( |
| "default", |
| uri=f"sqlite:///{warehouse}/pyiceberg_catalog.db", |
| warehouse=f"file://{warehouse}", |
| ) |
| yield catalog |
| catalog.close() |
| |
| |
| @pytest.fixture(scope="session") |
| def arrow_table_with_null() -> "pa.Table": |
| """Pyarrow table with all kinds of columns.""" |
| import pyarrow as pa |
| |
| return pa.Table.from_pydict( |
| { |
| "bool": [False, None, True], |
| "string": ["a", None, "z"], |
| # Go over the 16 bytes to kick in truncation |
| "string_long": ["a" * 22, None, "z" * 22], |
| "int": [1, None, 9], |
| "long": [1, None, 9], |
| "float": [0.0, None, 0.9], |
| "double": [0.0, None, 0.9], |
| # 'time': [1_000_000, None, 3_000_000], # Example times: 1s, none, and 3s past midnight #Spark does not support time fields |
| "timestamp": [ |
| datetime(2023, 1, 1, 19, 25, 00), # noqa: DTZ001 |
| None, |
| datetime(2023, 3, 1, 19, 25, 00), # noqa: DTZ001 |
| ], |
| # "timestamptz": [ |
| # datetime(2023, 1, 1, 19, 25, 00, tzinfo=timezone.utc), |
| # None, |
| # datetime(2023, 3, 1, 19, 25, 00, tzinfo=timezone.utc), |
| # ], |
| "date": [date(2023, 1, 1), None, date(2023, 3, 1)], |
| # Not supported by Spark |
| # 'time': [time(1, 22, 0), None, time(19, 25, 0)], |
| # Not natively supported by Arrow |
| # 'uuid': [uuid.UUID('00000000-0000-0000-0000-000000000000').bytes, None, uuid.UUID('11111111-1111-1111-1111-111111111111').bytes], |
| "binary": [b"\01", None, b"\22"], |
| "fixed": [ |
| uuid.UUID("00000000-0000-0000-0000-000000000000").bytes, |
| None, |
| uuid.UUID("11111111-1111-1111-1111-111111111111").bytes, |
| ], |
| }, |
| ) |
| |
| |
| def test_register_iceberg_table_provider( |
| catalog: Catalog, arrow_table_with_null: pa.Table |
| ) -> None: |
| catalog.create_namespace_if_not_exists("default") |
| iceberg_table = catalog.create_table_if_not_exists( |
| "default.dataset", |
| schema=arrow_table_with_null.schema, |
| ) |
| iceberg_table.append(arrow_table_with_null) |
| |
| iceberg_table_provider = IcebergDataFusionTable( |
| identifier=iceberg_table.name(), |
| metadata_location=iceberg_table.metadata_location, |
| file_io_properties=iceberg_table.io.properties, |
| ) |
| |
| ctx = SessionContext() |
| ctx.register_table("test", iceberg_table_provider) |
| |
| datafusion_table = ctx.table("test") |
| assert datafusion_table is not None |
| |
| # check that the schema is the same |
| from pyiceberg.io.pyarrow import _pyarrow_schema_ensure_small_types |
| |
| assert _pyarrow_schema_ensure_small_types( |
| datafusion_table.schema() |
| ) == _pyarrow_schema_ensure_small_types(iceberg_table.schema().as_arrow()) |
| # large/small type mismatches, fixed in pyiceberg 0.10.0 |
| # assert datafusion_table.schema() == iceberg_table.schema().as_arrow() |
| |
| # check that the data is the same |
| assert ( |
| datafusion_table.to_arrow_table().to_pylist() |
| == iceberg_table.scan().to_arrow().to_pylist() |
| ) |
| # large/small type mismatches, fixed in pyiceberg 0.10.0 |
| # assert datafusion_table.to_arrow_table() == iceberg_table.scan().to_arrow() |
| |
| |
| def test_register_pyiceberg_table( |
| catalog: Catalog, arrow_table_with_null: pa.Table |
| ) -> None: |
| from types import MethodType |
| |
| catalog.create_namespace_if_not_exists("default") |
| iceberg_table = catalog.create_table_if_not_exists( |
| "default.dataset", |
| schema=arrow_table_with_null.schema, |
| ) |
| iceberg_table.append(arrow_table_with_null) |
| |
| # monkey patch the __datafusion_table_provider__ method to the iceberg table |
| def __datafusion_table_provider__(self, session): |
| return IcebergDataFusionTable( |
| identifier=self.name(), |
| metadata_location=self.metadata_location, |
| file_io_properties=self.io.properties, |
| ).__datafusion_table_provider__(session) |
| |
| iceberg_table.__datafusion_table_provider__ = MethodType( |
| __datafusion_table_provider__, iceberg_table |
| ) |
| |
| ctx = SessionContext() |
| ctx.register_table("test", iceberg_table) |
| |
| datafusion_table = ctx.table("test") |
| assert datafusion_table is not None |