@@ -10,12 +10,14 @@ use delta_kernel_derive::ToSchema;
1010use roaring:: RoaringTreemap ;
1111use url:: Url ;
1212
13+ use crc:: { Crc , CRC_32_ISO_HDLC } ;
14+
1315use crate :: schema:: DataType ;
1416use crate :: utils:: require;
1517use crate :: { DeltaResult , Error , StorageHandler } ;
1618
1719#[ derive( Debug , PartialEq , Eq , Clone , Copy ) ]
18- #[ cfg_attr( test, derive( serde:: Serialize ) ) ]
20+ #[ cfg_attr( test, derive( serde:: Serialize , serde :: Deserialize ) ) ]
1921pub enum DeletionVectorStorageType {
2022 #[ cfg_attr( test, serde( rename = "u" ) ) ]
2123 PersistedRelative ,
@@ -57,8 +59,69 @@ impl ToDataType for DeletionVectorStorageType {
5759 }
5860}
5961
62+ /// Represents an abstract path to a deletion vector file.
63+ ///
64+ /// This is used in the public API to construct the path to a deletion vector file and
65+ /// has logic to convert [`crate::actions::deletion_vector_writer::DeletionVectorWriteResult`]
66+ /// to a [`DeletionVectorDescriptor`] with appropriate storage type and path.
67+ pub struct DeletionVectorPath {
68+ /// The base URL path to the Delta table
69+ table_path : Url ,
70+ /// Unique identifier for this deletion vector file
71+ uuid : uuid:: Uuid ,
72+ /// Optional directory prefix within the table path where the DV file will be located,
73+ /// this is to allow for randomizing reads/writes to avoid object store throttling.
74+ prefix : String ,
75+ }
76+
77+ impl DeletionVectorPath {
78+ pub ( crate ) fn new ( table_path : Url , prefix : String ) -> Self {
79+ Self {
80+ table_path,
81+ uuid : uuid:: Uuid :: new_v4 ( ) ,
82+ prefix,
83+ }
84+ }
85+
86+ #[ cfg( test) ]
87+ pub ( crate ) fn new_with_uuid ( table_path : Url , prefix : String , uuid : uuid:: Uuid ) -> Self {
88+ Self {
89+ table_path,
90+ uuid,
91+ prefix,
92+ }
93+ }
94+
95+ /// Helper method to construct the relative path to a deletion vector file
96+ /// from the prefix and UUID suffix.
97+ fn relative_path ( prefix : & str , uuid : & uuid:: Uuid ) -> String {
98+ if !prefix. is_empty ( ) {
99+ format ! ( "{prefix}/deletion_vector_{uuid}.bin" )
100+ } else {
101+ format ! ( "deletion_vector_{uuid}.bin" )
102+ }
103+ }
104+
105+ /// Returns the absolute path to the deletion vector file.
106+ pub fn absolute_path ( & self ) -> DeltaResult < Url > {
107+ let dv_suffix = Self :: relative_path ( & self . prefix , & self . uuid ) ;
108+ self . table_path
109+ . join ( & dv_suffix)
110+ . map_err ( |_| Error :: DeletionVector ( format ! ( "invalid path: {dv_suffix}" ) ) )
111+ }
112+
113+ /// Returns the compressed encoded path for use in descriptor (prefix + z85 encoded UUID).
114+ pub ( crate ) fn encoded_relative_path ( & self ) -> String {
115+ format ! ( "{}{}" , self . prefix, z85:: encode( self . uuid. as_bytes( ) ) )
116+ }
117+ }
118+
60119#[ derive( Debug , Clone , PartialEq , Eq , ToSchema ) ]
61- #[ cfg_attr( test, derive( serde:: Serialize ) , serde( rename_all = "camelCase" ) ) ]
120+ #[ cfg_attr(
121+ test,
122+ derive( serde:: Serialize , serde:: Deserialize ) ,
123+ serde( rename_all = "camelCase" )
124+ ) ]
62125pub struct DeletionVectorDescriptor {
63126 /// A single character to indicate how to access the DV. Legal options are: ['u', 'i', 'p'].
64127 pub storage_type : DeletionVectorStorageType ,
@@ -122,14 +185,8 @@ impl DeletionVectorDescriptor {
122185 . map_err ( |_| Error :: deletion_vector ( "Failed to decode DV uuid" ) ) ?;
123186 let uuid = uuid:: Uuid :: from_slice ( & decoded)
124187 . map_err ( |err| Error :: DeletionVector ( err. to_string ( ) ) ) ?;
125- let dv_suffix = if prefix_len > 0 {
126- format ! (
127- "{}/deletion_vector_{uuid}.bin" ,
128- & self . path_or_inline_dv[ ..prefix_len]
129- )
130- } else {
131- format ! ( "deletion_vector_{uuid}.bin" )
132- } ;
188+ let dv_suffix =
189+ DeletionVectorPath :: relative_path ( & self . path_or_inline_dv [ ..prefix_len] , & uuid) ;
133190 let dv_path = parent
134191 . join ( & dv_suffix)
135192 . map_err ( |_| Error :: DeletionVector ( format ! ( "invalid path: {dv_suffix}" ) ) ) ?;
@@ -170,54 +227,118 @@ impl DeletionVectorDescriptor {
170227 }
171228 }
172229 Some ( path) => {
173- let offset = self . offset ;
174- let size_in_bytes = self . size_in_bytes ;
230+ let size_in_bytes: u32 =
231+ self . size_in_bytes
232+ . try_into ( )
233+ . or ( Err ( Error :: DeletionVector ( format ! (
234+ "size_in_bytes doesn't fit in usize for {path}"
235+ ) ) ) ) ?;
175236
176237 let dv_data = storage
177- . read_files ( vec ! [ ( path, None ) ] ) ?
238+ . read_files ( vec ! [ ( path. clone ( ) , None ) ] ) ?
178239 . next ( )
179- . ok_or ( Error :: missing_data ( "No deletion vector data" ) ) ??;
240+ . ok_or ( Error :: missing_data ( format ! (
241+ "No deletion vector data for {path}"
242+ ) ) ) ??;
243+ let dv_data_len = dv_data. len ( ) ;
180244
181245 let mut cursor = Cursor :: new ( dv_data) ;
182246 let mut version_buf = [ 0 ; 1 ] ;
183- cursor
184- . read ( & mut version_buf )
185- . map_err ( |err| Error :: DeletionVector ( err . to_string ( ) ) ) ?;
247+ cursor. read ( & mut version_buf ) . map_err ( |err| {
248+ Error :: DeletionVector ( format ! ( "Failed to read version from {path}: {err}" ) )
249+ } ) ?;
186250 let version = u8:: from_be_bytes ( version_buf) ;
187251 require ! (
188252 version == 1 ,
189- Error :: DeletionVector ( format!( "Invalid version: {version}" ) )
253+ Error :: DeletionVector ( format!( "Invalid version {version} for {path }" ) )
190254 ) ;
191255
192- if let Some ( offset) = offset {
193- cursor. set_position ( offset as u64 ) ;
194- }
256+ // Deletion vector file format:
257+ // +---------------+-----------------+
258+ // | num bytes | value |
259+ // +===============+=================+
260+ // | 1 byte | version |
261+ // +---------------+-----------------+
262+ // | offset-1 | other dvs... |
263+ // +---------------+-----------------+ <- this_dv_start
264+ // | 4 bytes | dv_size |
265+ // +---------------+-----------------+
266+ // | 4 bytes | magic value |
267+ // +---------------+-----------------+ <- bitmap_start
268+ // | dv_size - 4 | bitmap |
269+ // +---------------+-----------------+ <- crc_start
270+ // | 4 bytes | CRC |
271+ // +---------------+-----------------+
272+
273+ let this_dv_start: usize =
274+ self . offset
275+ . unwrap_or ( 1 )
276+ . try_into ( )
277+ . or ( Err ( Error :: DeletionVector ( format ! (
278+ "Offset {:?} doesn't fit in usize for {path}" ,
279+ self . offset
280+ ) ) ) ) ?;
281+ let magic_start = this_dv_start + 4 ;
282+ // bitmap_start = this_dv_start + 4 (dv_size field) + 4 (magic field)
283+ let bitmap_start = this_dv_start + 8 ;
284+ // crc_start = this_dv_start + 4 (dv_size field) + dv_size (magic field + bitmap)
285+ // Safety: size_in_bytes is checked to fit in u32 which for all known platforms should
286+ // fix in usize range.
287+ let crc_start = this_dv_start + 4 + ( size_in_bytes as usize ) ;
288+ require ! (
289+ this_dv_start < dv_data_len,
290+ Error :: DeletionVector ( format!(
291+ "This DV start is out of bounds for {path} (Offset: {this_dv_start} >= Size: {dv_data_len})"
292+ ) )
293+ ) ;
294+
295+ cursor. set_position ( this_dv_start as u64 ) ;
195296 let dv_size = read_u32 ( & mut cursor, Endian :: Big ) ?;
196297 require ! (
197- dv_size == size_in_bytes as u32 ,
298+ dv_size == size_in_bytes,
198299 Error :: DeletionVector ( format!(
199- "DV size mismatch. Log indicates {size_in_bytes}, file says: {dv_size}"
300+ "DV size mismatch for {path} . Log indicates {size_in_bytes}, file says: {dv_size}"
200301 ) )
201302 ) ;
202303 let magic = read_u32 ( & mut cursor, Endian :: Little ) ?;
203304 require ! (
204305 magic == 1681511377 ,
205- Error :: DeletionVector ( format!( "Invalid magic: {magic}" ) )
306+ Error :: DeletionVector ( format!( "Invalid magic {magic} for {path }" ) )
206307 ) ;
207308
208- // get the Bytes back out and limit it to dv_size
209- let position = cursor. position ( ) ;
210- let mut bytes = cursor. into_inner ( ) ;
211- let truncate_pos = position + dv_size as u64 ;
212- assert ! (
213- truncate_pos <= usize :: MAX as u64 ,
214- "Can't truncate as truncate_pos is > usize::MAX"
309+ let bytes = cursor. into_inner ( ) ;
310+
311+ // +4 to account for CRC value
312+ require ! (
313+ bytes. len( ) >= crc_start + 4 ,
314+ Error :: DeletionVector ( format!(
315+ "Can't read deletion vector for {path} as there are not enough bytes. Expected {}, but got {}" ,
316+ crc_start + 4 ,
317+ bytes. len( )
318+ ) )
215319 ) ;
216- bytes. truncate ( truncate_pos as usize ) ;
217- let mut cursor = Cursor :: new ( bytes) ;
218- cursor. set_position ( position) ;
219- RoaringTreemap :: deserialize_from ( cursor)
220- . map_err ( |err| Error :: DeletionVector ( err. to_string ( ) ) )
320+
321+ let mut crc_cursor: Cursor < Bytes > =
322+ Cursor :: new ( bytes. slice ( crc_start..crc_start + 4 ) ) ;
323+ let crc = read_u32 ( & mut crc_cursor, Endian :: Big ) ?;
324+ let crc32 = create_dv_crc32 ( ) ;
325+ // CRC is calculated from magic field through end of bitmap
326+ // Safety: verified bytes is larger than crc_start + 4, above.
327+ let expected_crc = crc32. checksum ( & bytes. slice ( magic_start..crc_start) ) ;
328+ require ! (
329+ crc == expected_crc,
330+ Error :: DeletionVector ( format!(
331+ "CRC32 checksum mismatch for {path}. Got: {crc}, expected: {expected_crc}"
332+ ) )
333+ ) ;
334+ // Safety: verified bytes is larger than crc_start + 4, above.
335+ let dv_bytes = bytes. slice ( bitmap_start..crc_start) ;
336+ let cursor = Cursor :: new ( dv_bytes) ;
337+ RoaringTreemap :: deserialize_from ( cursor) . map_err ( |err| {
338+ Error :: DeletionVector ( format ! (
339+ "Failed to deserialize deletion vector for {path}: {err}"
340+ ) )
341+ } )
221342 }
222343 }
223344 }
@@ -238,6 +359,12 @@ enum Endian {
238359 Little ,
239360}
240361
362+ /// Factory function to create a CRC-32 instance using the ISO HDLC algorithm.
363+ /// This ensures consistent CRC algorithm usage for deletion vectors.
364+ pub ( crate ) fn create_dv_crc32 ( ) -> Crc < u32 > {
365+ Crc :: < u32 > :: new ( & CRC_32_ISO_HDLC )
366+ }
367+
241368/// small helper to read a big or little endian u32 from a cursor
242369fn read_u32 ( cursor : & mut Cursor < Bytes > , endian : Endian ) -> DeltaResult < u32 > {
243370 let mut buf = [ 0 ; 4 ] ;
@@ -437,7 +564,7 @@ mod tests {
437564 let storage = sync_engine. storage_handler ( ) ;
438565
439566 let example = dv_example ( ) ;
440- let tree_map = example. read ( storage, & parent) . unwrap ( ) ;
567+ let tree_map = example. read ( storage. clone ( ) , & parent) . unwrap ( ) ;
441568
442569 let expected: Vec < u64 > = vec ! [ 0 , 9 ] ;
443570 let found = tree_map. iter ( ) . collect :: < Vec < _ > > ( ) ;
@@ -562,4 +689,81 @@ mod tests {
562689 assert_eq ! ( variant, parsed) ;
563690 }
564691 }
692+
693+ #[ test]
694+ fn test_deletion_vector_path_uniqueness ( ) {
695+ // Verify that two DeletionVectorPath instances created with the same arguments
696+ // produce different absolute paths due to unique UUIDs
697+ let table_path = Url :: parse ( "file:///tmp/test_table/" ) . unwrap ( ) ;
698+ let prefix = String :: from ( "deletion_vectors" ) ;
699+
700+ let dv_path1 = DeletionVectorPath :: new ( table_path. clone ( ) , prefix. clone ( ) ) ;
701+ let dv_path2 = DeletionVectorPath :: new ( table_path. clone ( ) , prefix. clone ( ) ) ;
702+
703+ let abs_path1 = dv_path1. absolute_path ( ) . unwrap ( ) ;
704+ let abs_path2 = dv_path2. absolute_path ( ) . unwrap ( ) ;
705+
706+ // The absolute paths should be different because each DeletionVectorPath
707+ // gets a unique UUID
708+ assert_ne ! ( abs_path1, abs_path2) ;
709+ assert_ne ! (
710+ dv_path1. encoded_relative_path( ) ,
711+ dv_path2. encoded_relative_path( )
712+ ) ;
713+ }
714+
715+ #[ test]
716+ fn test_deletion_vector_path_absolute_path_with_prefix ( ) {
717+ let table_path = Url :: parse ( "file:///tmp/test_table/" ) . unwrap ( ) ;
718+ let prefix = String :: from ( "dv" ) ;
719+ let known_uuid = uuid:: Uuid :: parse_str ( "abcdef01-2345-6789-abcd-ef0123456789" ) . unwrap ( ) ;
720+
721+ let dv_path = DeletionVectorPath :: new_with_uuid ( table_path. clone ( ) , prefix, known_uuid) ;
722+ let abs_path = dv_path. absolute_path ( ) . unwrap ( ) ;
723+
724+ // Verify the exact path with known UUID
725+ let expected =
726+ "file:///tmp/test_table/dv/deletion_vector_abcdef01-2345-6789-abcd-ef0123456789.bin" ;
727+ assert_eq ! ( abs_path. as_str( ) , expected) ;
728+ }
729+
730+ #[ test]
731+ fn test_deletion_vector_path_absolute_path_with_known_uuid ( ) {
732+ // Test with a known UUID to verify exact path construction
733+ let table_path = Url :: parse ( "file:///tmp/test_table/" ) . unwrap ( ) ;
734+ let prefix = String :: from ( "dv" ) ;
735+ let known_uuid = uuid:: Uuid :: parse_str ( "550e8400-e29b-41d4-a716-446655440000" ) . unwrap ( ) ;
736+
737+ let dv_path = DeletionVectorPath :: new_with_uuid ( table_path, prefix, known_uuid) ;
738+ let abs_path = dv_path. absolute_path ( ) . unwrap ( ) ;
739+
740+ // Verify the exact path is constructed correctly
741+ let expected_path =
742+ "file:///tmp/test_table/dv/deletion_vector_550e8400-e29b-41d4-a716-446655440000.bin" ;
743+ assert_eq ! ( abs_path. as_str( ) , expected_path) ;
744+
745+ // Verify the encoded_relative_path is exactly as expected (prefix + z85 encoded UUID: 20 chars)
746+ let encoded = dv_path. encoded_relative_path ( ) ;
747+ assert_eq ! ( encoded, "dvrsTVZ&*Sl-RXRWjryu/!" ) ;
748+ }
749+
750+ #[ test]
751+ fn test_deletion_vector_path_absolute_path_with_known_uuid_empty_prefix ( ) {
752+ // Test with a known UUID and empty prefix
753+ let table_path = Url :: parse ( "file:///tmp/test_table/" ) . unwrap ( ) ;
754+ let prefix = String :: from ( "" ) ;
755+ let known_uuid = uuid:: Uuid :: parse_str ( "123e4567-e89b-12d3-a456-426614174000" ) . unwrap ( ) ;
756+
757+ let dv_path = DeletionVectorPath :: new_with_uuid ( table_path, prefix, known_uuid) ;
758+ let abs_path = dv_path. absolute_path ( ) . unwrap ( ) ;
759+
760+ // Verify the exact path is constructed correctly without prefix directory
761+ let expected_path =
762+ "file:///tmp/test_table/deletion_vector_123e4567-e89b-12d3-a456-426614174000.bin" ;
763+ assert_eq ! ( abs_path. as_str( ) , expected_path) ;
764+
765+ // Verify the encoded_relative_path is exactly as expected (z85 encoded UUID: 20 chars)
766+ let encoded = dv_path. encoded_relative_path ( ) ;
767+ assert_eq ! ( encoded, "5<w-%>:JjlQ/G/]6C<1m" ) ;
768+ }
565769}
0 commit comments