| # 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. | |
| from typing import Any | |
| import pytest | |
| from pyiceberg.partitioning import PartitionField, PartitionFieldValue, PartitionKey, PartitionSpec | |
| from pyiceberg.schema import Schema | |
| from pyiceberg.table import TableProperties | |
| from pyiceberg.table.locations import LocationProvider, load_location_provider | |
| from pyiceberg.transforms import IdentityTransform | |
| from pyiceberg.typedef import EMPTY_DICT | |
| from pyiceberg.types import NestedField, StringType | |
| PARTITION_FIELD = PartitionField(source_id=1, field_id=1002, transform=IdentityTransform(), name="string_field") | |
| PARTITION_KEY = PartitionKey( | |
| field_values=[PartitionFieldValue(PARTITION_FIELD, "example_string")], | |
| partition_spec=PartitionSpec(PARTITION_FIELD), | |
| schema=Schema(NestedField(field_id=1, name="string_field", field_type=StringType(), required=False)), | |
| ) | |
| class CustomLocationProvider(LocationProvider): | |
| def new_data_location(self, data_file_name: str, partition_key: PartitionKey | None = None) -> str: | |
| return f"custom_location_provider/{data_file_name}" | |
| def test_simple_location_provider_no_partition() -> None: | |
| provider = load_location_provider(table_location="table_location", table_properties={"write.object-storage.enabled": "false"}) | |
| assert provider.new_data_location("my_file") == "table_location/data/my_file" | |
| def test_simple_location_provider_with_partition() -> None: | |
| provider = load_location_provider(table_location="table_location", table_properties={"write.object-storage.enabled": "false"}) | |
| assert provider.new_data_location("my_file", PARTITION_KEY) == "table_location/data/string_field=example_string/my_file" | |
| def test_custom_location_provider() -> None: | |
| qualified_name = CustomLocationProvider.__module__ + "." + CustomLocationProvider.__name__ | |
| provider = load_location_provider( | |
| table_location="table_location", table_properties={"write.py-location-provider.impl": qualified_name} | |
| ) | |
| assert provider.new_data_location("my_file") == "custom_location_provider/my_file" | |
| def test_custom_location_provider_single_path() -> None: | |
| with pytest.raises(ValueError, match=r"write\.py-location-provider\.impl should be full path"): | |
| load_location_provider(table_location="table_location", table_properties={"write.py-location-provider.impl": "not_found"}) | |
| def test_custom_location_provider_not_found(caplog: Any) -> None: | |
| import logging | |
| caplog.set_level(logging.DEBUG) | |
| with pytest.raises(ValueError, match=r"Could not initialize LocationProvider"): | |
| load_location_provider( | |
| table_location="table_location", table_properties={"write.py-location-provider.impl": "module.not_found"} | |
| ) | |
| assert "Could not initialize LocationProvider: module.not_found" in caplog.text | |
| assert "ModuleNotFoundError: No module named 'module'" in caplog.text | |
| def test_object_storage_no_partition() -> None: | |
| provider = load_location_provider(table_location="table_location", table_properties={"write.object-storage.enabled": "true"}) | |
| location = provider.new_data_location("test.parquet") | |
| parts = location.split("/") | |
| assert len(parts) == 7 | |
| assert parts[0] == "table_location" | |
| assert parts[1] == "data" | |
| assert parts[-1] == "test.parquet" | |
| # Entropy directories in the middle | |
| for dir_name in parts[2:-1]: | |
| assert dir_name | |
| assert all(c in "01" for c in dir_name) | |
| def test_object_storage_with_partition() -> None: | |
| provider = load_location_provider( | |
| table_location="table_location", | |
| table_properties={"write.object-storage.enabled": "true"}, | |
| ) | |
| location = provider.new_data_location("test.parquet", PARTITION_KEY) | |
| # Partition values AND entropy included in the path. Entropy differs to that in the test below because the partition | |
| # key AND the data file name are used as the hash input. This matches Java behaviour; the hash below is what the | |
| # Java implementation produces for this input too. | |
| assert location == "table_location/data/0001/0010/1001/00000011/string_field=example_string/test.parquet" | |
| # NB: We test here with None partition key too because disabling partitioned paths still replaces final / with - even in | |
| # paths of un-partitioned files. This matches the behaviour of the Java implementation. | |
| @pytest.mark.parametrize("partition_key", [PARTITION_KEY, None]) | |
| def test_object_storage_partitioned_paths_disabled(partition_key: PartitionKey | None) -> None: | |
| provider = load_location_provider( | |
| table_location="table_location", | |
| table_properties={ | |
| "write.object-storage.enabled": "true", | |
| "write.object-storage.partitioned-paths": "false", | |
| }, | |
| ) | |
| location = provider.new_data_location("test.parquet", partition_key) | |
| # No partition values included in the path and last part of entropy is separated with "-" | |
| assert location == "table_location/data/0110/1010/0011/11101000-test.parquet" | |
| @pytest.mark.parametrize( | |
| ["data_file_name", "expected_hash"], | |
| [ | |
| ("a", "0101/0110/1001/10110010"), | |
| ("b", "1110/0111/1110/00000011"), | |
| ("c", "0010/1101/0110/01011111"), | |
| ("d", "1001/0001/0100/01110011"), | |
| ], | |
| ) | |
| def test_hash_injection(data_file_name: str, expected_hash: str) -> None: | |
| provider = load_location_provider(table_location="table_location", table_properties={"write.object-storage.enabled": "true"}) | |
| assert provider.new_data_location(data_file_name) == f"table_location/data/{expected_hash}/{data_file_name}" | |
| def test_object_location_provider_write_data_path() -> None: | |
| provider = load_location_provider( | |
| table_location="s3://table-location/table", | |
| table_properties={ | |
| "write.object-storage.enabled": "true", | |
| TableProperties.WRITE_DATA_PATH: "s3://table-location/custom/data/path", | |
| }, | |
| ) | |
| assert ( | |
| provider.new_data_location("file.parquet") == "s3://table-location/custom/data/path/0010/1111/0101/11011101/file.parquet" | |
| ) | |
| def test_simple_location_provider_write_data_path() -> None: | |
| provider = load_location_provider( | |
| table_location="table_location", | |
| table_properties={ | |
| TableProperties.WRITE_DATA_PATH: "s3://table-location/custom/data/path", | |
| "write.object-storage.enabled": "false", | |
| }, | |
| ) | |
| assert provider.new_data_location("file.parquet") == "s3://table-location/custom/data/path/file.parquet" | |
| def test_location_provider_metadata_default_location() -> None: | |
| provider = load_location_provider(table_location="table_location", table_properties=EMPTY_DICT) | |
| assert provider.new_metadata_location("manifest.avro") == "table_location/metadata/manifest.avro" | |
| def test_location_provider_metadata_location_with_custom_path() -> None: | |
| provider = load_location_provider( | |
| table_location="table_location", | |
| table_properties={TableProperties.WRITE_METADATA_PATH: "s3://table-location/custom/path"}, | |
| ) | |
| assert provider.new_metadata_location("metadata.json") == "s3://table-location/custom/path/metadata.json" | |
| def test_metadata_location_with_trailing_slash() -> None: | |
| provider = load_location_provider( | |
| table_location="table_location", | |
| table_properties={TableProperties.WRITE_METADATA_PATH: "s3://table-location/custom/path/"}, | |
| ) | |
| assert provider.new_metadata_location("metadata.json") == "s3://table-location/custom/path/metadata.json" |