1414# KIND, either express or implied. See the License for the
1515# specific language governing permissions and limitations
1616# under the License.
17- from dataclasses import dataclass
1817from typing import Any
1918from unittest .mock import patch
2019
@@ -325,23 +324,13 @@ def test_concurrent_overwrite_overwrite_raises_validation_exception(catalog: Cat
325324 tbl2 .overwrite (pa .table ({"x" : [40 , 50 , 60 ]}), overwrite_filter = "x > 0" )
326325
327326
328- @dataclass (frozen = True )
329- class _FileOverwriteScenario :
330- identifier : str
331- deleting_table : Table
332- replacing_transaction : Transaction
333- target_file : DataFile
334- same_partition_file : DataFile
335- different_partition_file : DataFile
327+ _FILE_OVERWRITE_TABLE = "default.concurrent_file_delete"
336328
337329
338- def _prepare_file_overwrite_scenario (catalog : Catalog ) -> _FileOverwriteScenario :
339- """Prepare a file replacement and candidates for concurrent deletion."""
340- import uuid
341-
330+ def _create_file_overwrite_table (catalog : Catalog ) -> DataFile :
331+ """Create the file-overwrite test table and return the file to replace."""
342332 import pyarrow as pa
343333
344- from pyiceberg .io .pyarrow import _dataframe_to_data_files
345334 from pyiceberg .partitioning import PartitionField , PartitionSpec
346335 from pyiceberg .transforms import IdentityTransform
347336
@@ -351,85 +340,102 @@ def _prepare_file_overwrite_scenario(catalog: Catalog) -> _FileOverwriteScenario
351340 NestedField (2 , "value" , LongType (), required = False ),
352341 )
353342 spec = PartitionSpec (PartitionField (source_id = 1 , field_id = 1000 , transform = IdentityTransform (), name = "category" ))
354- identifier = "default.concurrent_file_delete"
355- table = catalog . create_table ( identifier , schema = schema , partition_spec = spec )
343+ table = catalog . create_table ( _FILE_OVERWRITE_TABLE , schema = schema , partition_spec = spec )
344+
356345 table .append (pa .table ({"category" : ["a" , "b" ], "value" : [0 , 1 ]}))
357- original_file = next (task .file for task in table .scan ().plan_files () if task .file .partition [0 ] == "a" )
346+ file_to_replace = next (task .file for task in table .scan ().plan_files () if task .file .partition [0 ] == "a" )
347+
358348 table .append (pa .table ({"category" : ["a" ], "value" : [3 ]}))
349+ return file_to_replace
350+
359351
360- deleting_table = catalog .load_table (identifier )
361- replacing_table = catalog .load_table (identifier )
362- active_files = [task .file for task in replacing_table .scan ().plan_files ()]
363- same_partition_file = next (
364- data_file for data_file in active_files if data_file .partition [0 ] == "a" and data_file != original_file
352+ def _data_file_in_partition (table : Table , partition : str , excluded_file : DataFile | None = None ) -> DataFile :
353+ """Return a data file in a partition, optionally excluding one file."""
354+ return next (
355+ task .file for task in table .scan ().plan_files () if task .file .partition [0 ] == partition and task .file != excluded_file
365356 )
366- different_partition_file = next (data_file for data_file in active_files if data_file .partition [0 ] == "b" )
357+
358+
359+ def _stage_file_replacement (table : Table , file_to_replace : DataFile ) -> Transaction :
360+ """Stage replacing one data file without committing the transaction."""
361+ import uuid
362+
363+ import pyarrow as pa
364+
365+ from pyiceberg .io .pyarrow import _dataframe_to_data_files
366+
367367 replacement_files = list (
368368 _dataframe_to_data_files (
369- table_metadata = replacing_table .metadata ,
369+ table_metadata = table .metadata ,
370370 df = pa .table ({"category" : ["a" ], "value" : [2 ]}),
371- io = replacing_table .io ,
371+ io = table .io ,
372372 write_uuid = uuid .uuid4 (),
373373 )
374374 )
375375
376- replacing_transaction = replacing_table .transaction ()
377- with replacing_transaction .update_snapshot ().overwrite () as overwrite :
378- overwrite .delete_data_file (original_file )
376+ transaction = table .transaction ()
377+ with transaction .update_snapshot ().overwrite () as overwrite :
378+ overwrite .delete_data_file (file_to_replace )
379379 for replacement_file in replacement_files :
380380 overwrite .append_data_file (replacement_file )
381381
382- return _FileOverwriteScenario (
383- identifier = identifier ,
384- deleting_table = deleting_table ,
385- replacing_transaction = replacing_transaction ,
386- target_file = original_file ,
387- same_partition_file = same_partition_file ,
388- different_partition_file = different_partition_file ,
389- )
382+ return transaction
383+
384+
385+ def _delete_data_file (table : Table , data_file : DataFile ) -> None :
386+ """Commit the deletion of one data file."""
387+ with table .transaction () as transaction :
388+ with transaction .update_snapshot ().overwrite () as overwrite :
389+ overwrite .delete_data_file (data_file )
390+
391+
392+ def _file_overwrite_values (catalog : Catalog ) -> list [int ]:
393+ """Return the sorted values in the file-overwrite test table."""
394+ result = catalog .load_table (_FILE_OVERWRITE_TABLE ).scan ().to_arrow ()
395+ return sorted (result ["value" ].to_pylist ())
390396
391397
392398def test_file_overwrite_fails_when_target_file_is_concurrently_deleted (catalog : Catalog ) -> None :
393399 """A file replacement must fail if the original file was concurrently deleted."""
394- scenario = _prepare_file_overwrite_scenario (catalog )
400+ file_to_replace = _create_file_overwrite_table (catalog )
401+ replacing_table = catalog .load_table (_FILE_OVERWRITE_TABLE )
402+ deleting_table = catalog .load_table (_FILE_OVERWRITE_TABLE )
395403
396- with scenario .deleting_table .transaction () as deleting_transaction :
397- with deleting_transaction .update_snapshot ().overwrite () as overwrite :
398- overwrite .delete_data_file (scenario .target_file )
404+ replacing_transaction = _stage_file_replacement (replacing_table , file_to_replace )
405+ _delete_data_file (deleting_table , file_to_replace )
399406
400407 with pytest .raises (ValidationException , match = "Data files were concurrently deleted" ):
401- scenario . replacing_transaction .commit_transaction ()
408+ replacing_transaction .commit_transaction ()
402409
403- result = catalog .load_table (scenario .identifier ).scan ().to_arrow ()
404- assert sorted (result ["value" ].to_pylist ()) == [1 , 3 ]
410+ assert _file_overwrite_values (catalog ) == [1 , 3 ]
405411
406412
407413def test_file_overwrite_allows_concurrent_delete_in_same_partition (catalog : Catalog ) -> None :
408414 """A file replacement must allow another file in its partition to be concurrently deleted."""
409- scenario = _prepare_file_overwrite_scenario (catalog )
415+ file_to_replace = _create_file_overwrite_table (catalog )
416+ replacing_table = catalog .load_table (_FILE_OVERWRITE_TABLE )
417+ deleting_table = catalog .load_table (_FILE_OVERWRITE_TABLE )
418+ file_to_delete = _data_file_in_partition (deleting_table , "a" , excluded_file = file_to_replace )
410419
411- with scenario . deleting_table . transaction () as deleting_transaction :
412- with deleting_transaction . update_snapshot (). overwrite () as overwrite :
413- overwrite . delete_data_file ( scenario . same_partition_file )
420+ replacing_transaction = _stage_file_replacement ( replacing_table , file_to_replace )
421+ _delete_data_file ( deleting_table , file_to_delete )
422+ replacing_transaction . commit_transaction ( )
414423
415- scenario .replacing_transaction .commit_transaction ()
416-
417- result = catalog .load_table (scenario .identifier ).scan ().to_arrow ()
418- assert sorted (result ["value" ].to_pylist ()) == [1 , 2 ]
424+ assert _file_overwrite_values (catalog ) == [1 , 2 ]
419425
420426
421427def test_file_overwrite_allows_concurrent_delete_in_different_partition (catalog : Catalog ) -> None :
422428 """A file replacement must allow a file in another partition to be concurrently deleted."""
423- scenario = _prepare_file_overwrite_scenario (catalog )
424-
425- with scenario .deleting_table .transaction () as deleting_transaction :
426- with deleting_transaction .update_snapshot ().overwrite () as overwrite :
427- overwrite .delete_data_file (scenario .different_partition_file )
429+ file_to_replace = _create_file_overwrite_table (catalog )
430+ replacing_table = catalog .load_table (_FILE_OVERWRITE_TABLE )
431+ deleting_table = catalog .load_table (_FILE_OVERWRITE_TABLE )
432+ file_to_delete = _data_file_in_partition (deleting_table , "b" )
428433
429- scenario .replacing_transaction .commit_transaction ()
434+ replacing_transaction = _stage_file_replacement (replacing_table , file_to_replace )
435+ _delete_data_file (deleting_table , file_to_delete )
436+ replacing_transaction .commit_transaction ()
430437
431- result = catalog .load_table (scenario .identifier ).scan ().to_arrow ()
432- assert sorted (result ["value" ].to_pylist ()) == [2 , 3 ]
438+ assert _file_overwrite_values (catalog ) == [2 , 3 ]
433439
434440
435441def test_concurrent_overwrite_append_retries_successfully (catalog : Catalog ) -> None :
0 commit comments