Skip to content

Commit 9906259

Browse files
Merge branch 'main' into feat/pub-as-record-batch
2 parents 4a9d1c9 + 98bfbc6 commit 9906259

10 files changed

Lines changed: 1397 additions & 53 deletions

File tree

kernel/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pre-release-hook = [
4242
delta_kernel_derive = { path = "../derive-macros", version = "0.16.0" }
4343
bytes = "1.10"
4444
chrono = "0.4.41"
45+
crc = "3.2.2"
4546
indexmap = "2.10.0"
4647
itertools = "0.14"
4748
roaring = "0.11.2"

kernel/src/actions/deletion_vector.rs

Lines changed: 241 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ use delta_kernel_derive::ToSchema;
1010
use roaring::RoaringTreemap;
1111
use url::Url;
1212

13+
use crc::{Crc, CRC_32_ISO_HDLC};
14+
1315
use crate::schema::DataType;
1416
use crate::utils::require;
1517
use 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))]
1921
pub 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+
)]
62125
pub 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
242369
fn 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

Comments
 (0)