Skip to content

Commit a46cbdf

Browse files
authored
fix: rank catalog configuration above table properties when constructing FileIO (#3956)
* Rank catalog configuration above table properties when constructing FileIO * Cover FileIO property precedence in StaticTable.from_metadata
1 parent 0d58407 commit a46cbdf

11 files changed

Lines changed: 83 additions & 17 deletions

File tree

‎pyiceberg/catalog/__init__.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,15 @@ def identifier_to_database_and_table(
833833

834834
return tuple_identifier[0], tuple_identifier[1]
835835

836-
def _load_file_io(self, properties: Properties = EMPTY_DICT, location: str | None = None) -> FileIO:
837-
return load_file_io({**self.properties, **properties}, location)
836+
def _load_file_io(
837+
self,
838+
properties: Properties = EMPTY_DICT,
839+
location: str | None = None,
840+
table_properties: Properties = EMPTY_DICT,
841+
) -> FileIO:
842+
# table_properties ranks lowest: a principal who can commit to a table must not
843+
# be able to redirect its readers.
844+
return load_file_io({**table_properties, **self.properties, **properties}, location)
838845

839846
@staticmethod
840847
def _convert_schema_if_needed(
@@ -1020,7 +1027,7 @@ def _create_staged_table(
10201027
metadata = new_table_metadata(
10211028
location=location, schema=schema, partition_spec=partition_spec, sort_order=sort_order, properties=properties
10221029
)
1023-
io = self._load_file_io(properties=properties, location=metadata_location)
1030+
io = self._load_file_io(location=metadata_location, table_properties=properties)
10241031
return StagedTable(
10251032
identifier=(database_name, table_name),
10261033
metadata=metadata,
@@ -1054,7 +1061,7 @@ def _update_and_stage_table(
10541061
identifier=table_identifier,
10551062
metadata=updated_metadata,
10561063
metadata_location=new_metadata_location,
1057-
io=self._load_file_io(properties=updated_metadata.properties, location=new_metadata_location),
1064+
io=self._load_file_io(location=new_metadata_location, table_properties=updated_metadata.properties),
10581065
catalog=self,
10591066
)
10601067

‎pyiceberg/catalog/bigquery_metastore.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def _convert_bigquery_table_to_iceberg_table(self, identifier: str | Identifier,
404404
identifier=(dataset_name, table_name),
405405
metadata=metadata,
406406
metadata_location=metadata_location,
407-
io=self._load_file_io(metadata.properties, metadata_location),
407+
io=self._load_file_io(location=metadata_location, table_properties=metadata.properties),
408408
catalog=self,
409409
)
410410

‎pyiceberg/catalog/dynamodb.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ def _convert_dynamo_table_item_to_iceberg_table(self, dynamo_table_item: dict[st
697697
identifier=(database_name, table_name),
698698
metadata=metadata,
699699
metadata_location=metadata_location,
700-
io=self._load_file_io(metadata.properties, metadata_location),
700+
io=self._load_file_io(location=metadata_location, table_properties=metadata.properties),
701701
catalog=self,
702702
)
703703

‎pyiceberg/catalog/glue.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def _convert_glue_to_iceberg(self, glue_table: "TableTypeDef") -> Table:
389389
identifier=(database_name, table_name),
390390
metadata=metadata,
391391
metadata_location=metadata_location,
392-
io=self._load_file_io(metadata.properties, metadata_location),
392+
io=self._load_file_io(location=metadata_location, table_properties=metadata.properties),
393393
catalog=self,
394394
)
395395

@@ -535,7 +535,7 @@ def _create_table_s3tables(
535535
identifier=self.identifier_to_tuple(identifier),
536536
metadata=staged_table.metadata,
537537
metadata_location=staged_table.metadata_location,
538-
io=self._load_file_io(staged_table.metadata.properties, staged_table.metadata_location),
538+
io=self._load_file_io(location=staged_table.metadata_location, table_properties=staged_table.metadata.properties),
539539
catalog=self,
540540
)
541541

@@ -599,7 +599,7 @@ def create_table(
599599
identifier=self.identifier_to_tuple(identifier),
600600
metadata=staged_table.metadata,
601601
metadata_location=staged_table.metadata_location,
602-
io=self._load_file_io(staged_table.metadata.properties, staged_table.metadata_location),
602+
io=self._load_file_io(location=staged_table.metadata_location, table_properties=staged_table.metadata.properties),
603603
catalog=self,
604604
)
605605

‎pyiceberg/catalog/hive.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def _convert_hive_into_iceberg(self, table: HiveTable) -> Table:
353353
identifier=(table.dbName, table.tableName),
354354
metadata=metadata,
355355
metadata_location=metadata_location,
356-
io=self._load_file_io(metadata.properties, metadata_location),
356+
io=self._load_file_io(location=metadata_location, table_properties=metadata.properties),
357357
catalog=self,
358358
)
359359

‎pyiceberg/catalog/rest/__init__.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,13 @@ def _resolve_storage_credentials(storage_credentials: list[StorageCredential], l
638638

639639
return best_match.config if best_match else {}
640640

641-
def _load_file_io(self, properties: Properties = EMPTY_DICT, location: str | None = None) -> FileIO:
642-
merged_properties = {**self.properties, **properties}
641+
def _load_file_io(
642+
self,
643+
properties: Properties = EMPTY_DICT,
644+
location: str | None = None,
645+
table_properties: Properties = EMPTY_DICT,
646+
) -> FileIO:
647+
merged_properties = {**table_properties, **self.properties, **properties}
643648
if self._auth_manager:
644649
merged_properties[AUTH_MANAGER] = self._auth_manager
645650
return load_file_io(merged_properties, location)
@@ -1152,8 +1157,9 @@ def _response_to_table(self, identifier_tuple: tuple[str, ...], table_response:
11521157
metadata_location=table_response.metadata_location, # type: ignore
11531158
metadata=table_response.metadata,
11541159
io=self._load_file_io(
1155-
{**table_response.metadata.properties, **table_response.config, **credential_config},
1160+
{**table_response.config, **credential_config},
11561161
table_response.metadata_location,
1162+
table_properties=table_response.metadata.properties,
11571163
),
11581164
catalog=self,
11591165
config=table_response.config,
@@ -1169,8 +1175,9 @@ def _response_to_staged_table(self, identifier_tuple: tuple[str, ...], table_res
11691175
metadata_location=table_response.metadata_location, # type: ignore
11701176
metadata=table_response.metadata,
11711177
io=self._load_file_io(
1172-
{**table_response.metadata.properties, **table_response.config, **credential_config},
1178+
{**table_response.config, **credential_config},
11731179
table_response.metadata_location,
1180+
table_properties=table_response.metadata.properties,
11741181
),
11751182
catalog=self,
11761183
)

‎pyiceberg/catalog/sql.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def _convert_orm_to_iceberg(self, orm_table: IcebergTables) -> Table:
237237
identifier=Catalog.identifier_to_tuple(table_namespace) + (table_name,),
238238
metadata=metadata,
239239
metadata_location=metadata_location,
240-
io=self._load_file_io(metadata.properties, metadata_location),
240+
io=self._load_file_io(location=metadata_location, table_properties=metadata.properties),
241241
catalog=self,
242242
)
243243

‎pyiceberg/table/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1995,7 +1995,7 @@ def from_metadata(cls, metadata_location: str, properties: Properties = EMPTY_DI
19951995
identifier=("static-table", metadata_location),
19961996
metadata_location=metadata_location,
19971997
metadata=metadata,
1998-
io=load_file_io({**properties, **metadata.properties}, location=metadata_location),
1998+
io=load_file_io({**metadata.properties, **properties}, location=metadata_location),
19991999
catalog=NoopCatalog("static-table"),
20002000
)
20012001

‎tests/catalog/test_rest.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
TableAlreadyExistsError,
6666
ViewAlreadyExistsError,
6767
)
68-
from pyiceberg.io import load_file_io
68+
from pyiceberg.io import ARROW_FILE_IO, FSSPEC_FILE_IO, PY_IO_IMPL, load_file_io
6969
from pyiceberg.partitioning import PartitionField, PartitionSpec
7070
from pyiceberg.schema import Schema
7171
from pyiceberg.table import Table
@@ -3415,6 +3415,32 @@ def test_load_table_with_storage_credentials(rest_mock: Mocker, example_table_me
34153415
assert table.io.properties["s3.session-token"] == "vended-token"
34163416

34173417

3418+
def test_load_table_catalog_config_outranks_table_properties(
3419+
rest_mock: Mocker, example_table_metadata_with_snapshot_v1: dict[str, Any]
3420+
) -> None:
3421+
metadata_location = "s3://warehouse/database/table/metadata/00001.metadata.json"
3422+
rest_mock.get(
3423+
f"{TEST_URI}v1/namespaces/fokko/tables/table",
3424+
json={
3425+
"metadata-location": metadata_location,
3426+
"metadata": {
3427+
**example_table_metadata_with_snapshot_v1,
3428+
"properties": {PY_IO_IMPL: FSSPEC_FILE_IO, "s3.proxy-uri": "http://table-only-proxy"},
3429+
},
3430+
"config": {"s3.region": "from-config"},
3431+
},
3432+
status_code=200,
3433+
request_headers=TEST_HEADERS,
3434+
)
3435+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN, **{PY_IO_IMPL: ARROW_FILE_IO, "s3.region": "from-catalog"})
3436+
table = catalog.load_table(("fokko", "table"))
3437+
3438+
assert table.io.properties[PY_IO_IMPL] == ARROW_FILE_IO
3439+
# Server config and a key the catalog leaves unset keep working.
3440+
assert table.io.properties["s3.region"] == "from-config"
3441+
assert table.io.properties["s3.proxy-uri"] == "http://table-only-proxy"
3442+
3443+
34183444
def test_load_credentials_with_longest_prefix(rest_mock: Mocker) -> None:
34193445
rest_mock.get(
34203446
f"{TEST_URI}v1/namespaces/fokko/tables/table/credentials",

‎tests/catalog/test_sql.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
NoSuchTableError,
3737
TableAlreadyExistsError,
3838
)
39+
from pyiceberg.io import ARROW_FILE_IO, FSSPEC_FILE_IO, PY_IO_IMPL
3940
from pyiceberg.schema import Schema
4041
from pyiceberg.types import NestedField, StringType, strtobool
4142

@@ -299,6 +300,25 @@ def test_idempotent_when_column_already_exists(warehouse: Path) -> None:
299300
assert "iceberg_type" in get_columns(catalog.engine)
300301

301302

303+
def test_load_table_ranks_catalog_config_above_table_properties(warehouse: Path) -> None:
304+
catalog = SqlCatalog(
305+
name="test",
306+
uri="sqlite:///:memory:",
307+
warehouse=f"file://{warehouse}",
308+
**{PY_IO_IMPL: ARROW_FILE_IO},
309+
)
310+
catalog.create_namespace("ns")
311+
catalog.create_table(
312+
("ns", "tbl"),
313+
Schema(NestedField(1, "id", StringType(), required=True)),
314+
properties={PY_IO_IMPL: FSSPEC_FILE_IO, "s3.proxy-uri": "http://table-only-proxy"},
315+
)
316+
317+
io = catalog.load_table(("ns", "tbl")).io
318+
assert io.properties[PY_IO_IMPL] == ARROW_FILE_IO
319+
assert io.properties["s3.proxy-uri"] == "http://table-only-proxy"
320+
321+
302322
def test_list_tables_filters_by_iceberg_type(warehouse: Path) -> None:
303323
catalog = SqlCatalog(
304324
name="test",

0 commit comments

Comments
 (0)