blob: d0a5d7cee3747af85eef04bb4cd21bc59b330b6e [file]
# 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