@@ -46,6 +46,10 @@ def setUpClass(cls):
4646 def tearDownClass (cls ):
4747 shutil .rmtree (cls .tempdir , ignore_errors = True )
4848
49+ @staticmethod
50+ def _file_path (file_meta ):
51+ return file_meta .external_path if file_meta .external_path else file_meta .file_path
52+
4953 # ------------------------------------------------------------------
5054 # Parquet-format data evolution
5155 # ------------------------------------------------------------------
@@ -236,6 +240,41 @@ def test_blob_write_and_read(self):
236240 self .assertEqual (actual .column ('id' ).to_pylist (), [1 , 2 , 3 ])
237241 self .assertEqual (actual .column ('payload' ).to_pylist (), blobs )
238242
243+ def test_blob_abort_deletes_uncommitted_files (self ):
244+ pa_schema = pa .schema ([
245+ ('id' , pa .int32 ()),
246+ ('payload' , pa .large_binary ()),
247+ ])
248+ schema = Schema .from_pyarrow_schema (pa_schema , options = {
249+ 'row-tracking.enabled' : 'true' ,
250+ 'data-evolution.enabled' : 'true' ,
251+ })
252+ self .catalog .create_table ('default.fmt_blob_abort_cleanup' , schema , False )
253+ table = self .catalog .get_table ('default.fmt_blob_abort_cleanup' )
254+
255+ writer = table .new_batch_write_builder ().new_write ()
256+ writer .write_arrow (pa .Table .from_pydict ({
257+ 'id' : [1 , 2 , 3 ],
258+ 'payload' : [b'a' , b'b' , b'c' ],
259+ }, schema = pa_schema ))
260+ commit_messages = writer .prepare_commit ()
261+
262+ all_files = [nf for msg in commit_messages for nf in msg .new_files ]
263+ parquet_files = [f for f in all_files if f .file_name .endswith ('.parquet' )]
264+ blob_files = [f for f in all_files if f .file_name .endswith ('.blob' )]
265+ self .assertGreater (len (parquet_files ), 0 )
266+ self .assertGreater (len (blob_files ), 0 )
267+ for file_meta in all_files :
268+ self .assertTrue (table .file_io .exists (self ._file_path (file_meta )))
269+
270+ writer .abort ()
271+
272+ for file_meta in all_files :
273+ self .assertFalse (
274+ table .file_io .exists (self ._file_path (file_meta )),
275+ f"Expected abort to delete { file_meta .file_name } " ,
276+ )
277+
239278 def test_blob_column_subset_evolution (self ):
240279 """Write normal+blob cols in one commit, overwrite normal col in another, merge-read."""
241280 pa_schema = pa .schema ([
@@ -563,6 +602,87 @@ def test_vortex_with_row_id_and_filter(self):
563602 # Vector (vortex) file format for embedding columns
564603 # ------------------------------------------------------------------
565604
605+ def test_vector_abort_deletes_uncommitted_files (self ):
606+ pa_schema = pa .schema ([
607+ ('id' , pa .int64 ()),
608+ ('embed' , pa .list_ (pa .float32 (), 3 )),
609+ ])
610+ schema = Schema .from_pyarrow_schema (pa_schema , options = {
611+ 'row-tracking.enabled' : 'true' ,
612+ 'data-evolution.enabled' : 'true' ,
613+ 'vector.file.format' : 'parquet' ,
614+ })
615+ self .catalog .create_table ('default.fmt_vector_abort_cleanup' , schema , False )
616+ table = self .catalog .get_table ('default.fmt_vector_abort_cleanup' )
617+
618+ writer = table .new_batch_write_builder ().new_write ()
619+ writer .write_arrow (pa .table ({
620+ 'id' : pa .array ([1 , 2 , 3 ], type = pa .int64 ()),
621+ 'embed' : pa .FixedSizeListArray .from_arrays (
622+ pa .array ([0.1 , 0.2 , 0.3 ,
623+ 0.4 , 0.5 , 0.6 ,
624+ 0.7 , 0.8 , 0.9 ], type = pa .float32 ()), 3 ),
625+ }))
626+ commit_messages = writer .prepare_commit ()
627+
628+ all_files = [nf for msg in commit_messages for nf in msg .new_files ]
629+ normal_files = [f for f in all_files if not DataFileMeta .is_vector_file (f .file_name )]
630+ vector_files = [f for f in all_files if DataFileMeta .is_vector_file (f .file_name )]
631+ self .assertGreater (len (normal_files ), 0 )
632+ self .assertGreater (len (vector_files ), 0 )
633+ for file_meta in all_files :
634+ self .assertTrue (table .file_io .exists (self ._file_path (file_meta )))
635+
636+ writer .abort ()
637+
638+ for file_meta in all_files :
639+ self .assertFalse (
640+ table .file_io .exists (self ._file_path (file_meta )),
641+ f"Expected abort to delete { file_meta .file_name } " ,
642+ )
643+
644+ def test_vector_close_failure_after_prepare_raises (self ):
645+ from unittest .mock import patch
646+
647+ pa_schema = pa .schema ([
648+ ('id' , pa .int64 ()),
649+ ('embed' , pa .list_ (pa .float32 (), 3 )),
650+ ])
651+ schema = Schema .from_pyarrow_schema (pa_schema , options = {
652+ 'row-tracking.enabled' : 'true' ,
653+ 'data-evolution.enabled' : 'true' ,
654+ 'vector.file.format' : 'parquet' ,
655+ })
656+ self .catalog .create_table ('default.fmt_vector_close_failure' , schema , False )
657+ table = self .catalog .get_table ('default.fmt_vector_close_failure' )
658+
659+ writer = table .new_batch_write_builder ().new_write ()
660+ writer .write_arrow (pa .table ({
661+ 'id' : pa .array ([1 , 2 , 3 ], type = pa .int64 ()),
662+ 'embed' : pa .FixedSizeListArray .from_arrays (
663+ pa .array ([0.1 , 0.2 , 0.3 ,
664+ 0.4 , 0.5 , 0.6 ,
665+ 0.7 , 0.8 , 0.9 ], type = pa .float32 ()), 3 ),
666+ }))
667+ commit_messages = writer .prepare_commit ()
668+
669+ all_files = [nf for msg in commit_messages for nf in msg .new_files ]
670+ for file_meta in all_files :
671+ self .assertTrue (table .file_io .exists (self ._file_path (file_meta )))
672+
673+ data_writer = next (iter (writer .file_store_write .data_writers .values ()))
674+ with patch .object (
675+ data_writer , '_close_current_writers' ,
676+ side_effect = RuntimeError ("Close error" )):
677+ with self .assertRaisesRegex (RuntimeError , "Close error" ):
678+ writer .close ()
679+
680+ for file_meta in all_files :
681+ self .assertFalse (
682+ table .file_io .exists (self ._file_path (file_meta )),
683+ f"Expected abort to delete { file_meta .file_name } " ,
684+ )
685+
566686 @unittest .skipIf (sys .version_info < (3 , 11 ), "vortex-data requires Python >= 3.11" )
567687 @unittest .skipUnless (
568688 __import__ ('importlib' ).util .find_spec ('vortex' ) is not None ,
0 commit comments