Skip to content

Commit ab2eb39

Browse files
feat: Binary searching utility function for timestamp to version conversion (delta-io#896)
## What changes are proposed in this pull request? This PR introduces the `binary_search_by_key_with_bounds` utility function and the `history_manager` module. `binary_search_by_key_with_bounds` can be used to search over slices to find the least upper or greatest lower bounds. It uses a fallible key function to get comparable values from the slice. This is useful for performing binary search over fallible operations such as reading the in-commit timestamp from a commit file. The search bounds are defined by `Bound::LeastUpper` and `Bound::GreatestLower`. ## How was this change tested? The following cases are checked: - An exact match is present - There are duplicate matches. Ensure that the search bounds are respected. - Edge cases for GreatestLower and LeastUpper - Cases where the search is out of range - Error propagation from the key_fn to the caller.
1 parent ee9c483 commit ab2eb39

3 files changed

Lines changed: 269 additions & 0 deletions

File tree

kernel/src/history_manager/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod search;
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
//! This module defines the [`binary_search_by_key_with_bounds`] utility method. This can be used to
2+
//! search over sorted slices of values to find the greatest lower or least upper bounds.
3+
use std::cmp::Ordering;
4+
use std::error::Error;
5+
use std::fmt::Debug;
6+
7+
/// Defines the type of bound to return from a binary search operation.
8+
///
9+
/// For a search operation over `values` using key `key`:
10+
/// * [`Bound::LeastUpper`] - Finds the smallest index `i` such that `values[i] >= key`.
11+
/// This represents the first element greater than or equal to the search key.
12+
///
13+
/// * [`Bound::GreatestLower`] - Finds the largest index `i` such that `values[i] <= key`.
14+
/// This represents the last element less than or equal to the search key.
15+
#[allow(unused)]
16+
#[derive(Debug, Clone, Copy)]
17+
pub(crate) enum Bound {
18+
LeastUpper,
19+
GreatestLower,
20+
}
21+
22+
/// Represents the errors that can occur when performing binary search using
23+
/// [`binary_search_by_key_with_bounds`].
24+
#[allow(unused)]
25+
#[derive(Debug)]
26+
pub(crate) enum SearchError<T: Error> {
27+
/// Error that occurs when a search goes out of range. The meaning of "out of range" depends on
28+
/// the search [`Bound`]:
29+
/// - If the search bound is [`Bound::LeastUpper`], then no element was greater than or equal
30+
/// to the provided key.
31+
/// - If the search bound is [`Bound::GreatestLower`], then no element was less than or equal
32+
/// to the provided key.
33+
OutOfRange,
34+
/// Error that occurs when the `key_fn` fails when retrieving the key in
35+
/// [`binary_search_by_key_with_bounds`]. The error that occurred in the `key_fn` is returned in
36+
/// this error variant.
37+
KeyFunctionError(T),
38+
}
39+
40+
/// Performs a binary search on a slice to find the greatest lower or least upper bound.
41+
/// This function performs a binary search with the following properties:
42+
///
43+
/// 1. For [`Bound::LeastUpper`], find the smallest index `i` such that `values[i] >= key`
44+
/// 2. For [`Bound::GreatestLower`], find the largest index `i` such that `values[i] <= key`
45+
///
46+
/// # Arguments
47+
///
48+
/// * values - The slice to search in
49+
/// * key - The key value to search for
50+
/// * key_fn - A fallible function that extracts a comparable key from slice elements
51+
/// * bound - The type of bound to search for (least upper bound or greatest lower bound)
52+
///
53+
/// # Returns
54+
///
55+
/// * Ok(usize) - The index of the found element
56+
/// * [`SearchError::OutOfRange`] - If no suitable bound exists in the slice
57+
/// * [`SearchError::KeyFunctionError`] - If the key function fails. This contains the error
58+
/// emitted by the `key_fn`.
59+
///
60+
/// #Safety
61+
/// Assumes that the provided input is sorted by key using the `key_fn`. If the input is not
62+
/// sorted by key, then the output is unspecified and meaningless.
63+
///
64+
/// # Examples
65+
///
66+
/// ## Finding the Least Upper Bound
67+
/// ```rust,ignore
68+
/// # use delta_kernel::history_manager::search::*;
69+
/// # use delta_kernel::DeltaResult;
70+
/// let values = [10, 20, 30, 40, 50];
71+
/// // Simple key function that just returns the value
72+
/// let key_fn = |&val| -> DeltaResult<_> { Ok(val) };
73+
///
74+
/// // Find the least upper bound for 25 (element that is ≥ 25)
75+
/// let result = binary_search_by_key_with_bounds(
76+
/// &values,
77+
/// 25,
78+
/// key_fn,
79+
/// Bound::LeastUpper
80+
/// );
81+
/// assert_eq!(result.unwrap(), 2); // Index of 30
82+
/// ```
83+
///
84+
/// ## Finding the Greatest Lower Bound
85+
/// ```rust,ignore
86+
/// # use delta_kernel::history_manager::search::*;
87+
/// # use delta_kernel::DeltaResult;
88+
/// let values = [10, 20, 30, 40, 50];
89+
/// let key_fn = |&val| -> DeltaResult<_> { Ok(val) };
90+
///
91+
/// // Find the greatest lower bound for 25 (element that is ≤ 25)
92+
/// let result = binary_search_by_key_with_bounds(
93+
/// &values,
94+
/// 25,
95+
/// key_fn,
96+
/// Bound::GreatestLower
97+
/// );
98+
/// assert_eq!(result.unwrap(), 1); // Index of 20
99+
/// ```
100+
///
101+
/// ## Handling Out of Range Values
102+
/// ```rust,ignore
103+
/// # use delta_kernel::history_manager::search::*;
104+
/// # use delta_kernel::DeltaResult;
105+
/// let values = [10, 20, 30, 40, 50];
106+
/// let key_fn = |&val| -> DeltaResult<_> { Ok(val) };
107+
///
108+
/// // Finding a bound that's out of range
109+
/// let result = binary_search_by_key_with_bounds(
110+
/// &values,
111+
/// 5,
112+
/// key_fn,
113+
/// Bound::GreatestLower
114+
/// );
115+
/// assert!(matches!(result, Err(SearchError::OutOfRange)));
116+
/// ```
117+
///
118+
/// ## Using a Fallible Key Function
119+
/// ```rust,ignore
120+
/// # use delta_kernel::history_manager::search::*;
121+
/// # use delta_kernel::DeltaResult;
122+
/// // Using a fallible key function
123+
/// let values = ["10", "20", "thirty", "40"];
124+
/// let result = binary_search_by_key_with_bounds(
125+
/// &values,
126+
/// 25,
127+
/// |&s| s.parse::<i32>(),
128+
/// Bound::LeastUpper
129+
/// );
130+
/// assert!(matches!(result, Err(SearchError::KeyFunctionError(_))));
131+
/// ```
132+
#[allow(unused)]
133+
pub(crate) fn binary_search_by_key_with_bounds<'a, T, K: Ord + Debug, E: Error>(
134+
values: &'a [T],
135+
key: K,
136+
key_fn: impl Fn(&'a T) -> Result<K, E>,
137+
bound: Bound,
138+
) -> Result<usize, SearchError<E>> {
139+
let (mut lo, mut hi) = (0, values.len());
140+
while lo != hi {
141+
let mid = lo + (hi - lo) / 2;
142+
debug_assert!(lo <= mid && mid < hi);
143+
144+
// Use the key function to get a key. Return an error otherwise.
145+
let mid_key = key_fn(&values[mid]).map_err(SearchError::KeyFunctionError)?;
146+
match (key.cmp(&mid_key), bound) {
147+
(Ordering::Less, _) => hi = mid,
148+
(Ordering::Equal, Bound::LeastUpper) => hi = mid,
149+
(Ordering::Equal, Bound::GreatestLower) => lo = mid + 1,
150+
(Ordering::Greater, _) => lo = mid + 1,
151+
}
152+
}
153+
154+
// No exact match. We have a valid LUB (GLB) only if the upper (lower)
155+
// bound actually moved during the search.
156+
match bound {
157+
Bound::LeastUpper if hi < values.len() => Ok(hi),
158+
Bound::GreatestLower if lo > 0 => Ok(lo - 1),
159+
_ => Err(SearchError::OutOfRange),
160+
}
161+
}
162+
163+
#[cfg(test)]
164+
mod tests {
165+
use super::*;
166+
use crate::{DeltaResult, Error};
167+
168+
// Simple key extraction function
169+
fn get_val(x: &i32) -> DeltaResult<i32> {
170+
Ok(*x)
171+
}
172+
173+
#[test]
174+
fn test_exact_match() {
175+
let values = vec![1, 3, 5, 7, 9];
176+
177+
// LeastUpper bound with exact match
178+
let result =
179+
binary_search_by_key_with_bounds(&values, 5, get_val, Bound::LeastUpper).unwrap();
180+
assert_eq!(result, 2);
181+
182+
// GreatestLower bound with exact match
183+
let result =
184+
binary_search_by_key_with_bounds(&values, 5, get_val, Bound::GreatestLower).unwrap();
185+
assert_eq!(result, 2);
186+
}
187+
188+
#[test]
189+
fn test_no_exact_match() {
190+
let values = vec![1, 3, 5, 7, 9];
191+
192+
// LeastUpper bound (find element >= key)
193+
let result =
194+
binary_search_by_key_with_bounds(&values, 4, get_val, Bound::LeastUpper).unwrap();
195+
assert_eq!(result, 2); // Index of 5
196+
197+
// GreatestLower bound (find element <= key)
198+
let result =
199+
binary_search_by_key_with_bounds(&values, 6, get_val, Bound::GreatestLower).unwrap();
200+
assert_eq!(result, 2); // Index of 5
201+
}
202+
203+
#[test]
204+
fn test_duplicate_values() {
205+
let values = vec![1, 3, 5, 5, 5, 7, 9];
206+
207+
// LeastUpper should find first occurrence
208+
let result =
209+
binary_search_by_key_with_bounds(&values, 5, get_val, Bound::LeastUpper).unwrap();
210+
assert_eq!(result, 2); // First index of 5
211+
212+
// GreatestLower should find last occurrence
213+
let result =
214+
binary_search_by_key_with_bounds(&values, 5, get_val, Bound::GreatestLower).unwrap();
215+
assert_eq!(result, 4); // Last index of 5
216+
}
217+
218+
#[test]
219+
fn test_edge_cases() {
220+
// Empty array
221+
let empty: Vec<i32> = vec![];
222+
let result = binary_search_by_key_with_bounds(&empty, 5, get_val, Bound::LeastUpper);
223+
assert!(result.is_err());
224+
225+
// Value less than all elements (LeastUpper)
226+
let values = vec![5, 7, 9];
227+
let result =
228+
binary_search_by_key_with_bounds(&values, 3, get_val, Bound::LeastUpper).unwrap();
229+
assert_eq!(result, 0); // Should return index of first element
230+
231+
// Value less than all elements (GreatestLower)
232+
let result = binary_search_by_key_with_bounds(&values, 3, get_val, Bound::GreatestLower);
233+
assert!(matches!(result, Err(SearchError::OutOfRange))); // No lower bound exists
234+
235+
// Value greater than all elements (LeastUpper)
236+
let result = binary_search_by_key_with_bounds(&values, 10, get_val, Bound::LeastUpper);
237+
assert!(matches!(result, Err(SearchError::OutOfRange))); // No upper bound exists
238+
239+
// Value greater than all elements (GreatestLower)
240+
let result =
241+
binary_search_by_key_with_bounds(&values, 10, get_val, Bound::GreatestLower).unwrap();
242+
assert_eq!(result, 2); // Should return index of last element
243+
}
244+
#[test]
245+
fn test_error_propagation() {
246+
let values = vec![1, 3, 5, 7, 9];
247+
248+
let failing_key_fn = |x: &i32| -> DeltaResult<i32> {
249+
if *x == 5 {
250+
Err(Error::generic("Error extracting key"))
251+
} else {
252+
Ok(*x)
253+
}
254+
};
255+
256+
let result =
257+
binary_search_by_key_with_bounds(&values, 7, failing_key_fn, Bound::LeastUpper);
258+
assert!(matches!(
259+
result,
260+
Err(SearchError::KeyFunctionError(crate::Error::Generic(msg))) if msg.contains("Error extracting key")
261+
));
262+
}
263+
}

kernel/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ pub mod log_segment;
114114
#[cfg(not(feature = "internal-api"))]
115115
pub(crate) mod log_segment;
116116

117+
#[cfg(feature = "internal-api")]
118+
pub mod history_manager;
119+
#[cfg(not(feature = "internal-api"))]
120+
pub(crate) mod history_manager;
121+
117122
pub use delta_kernel_derive;
118123
pub use engine_data::{EngineData, RowVisitor};
119124
pub use error::{DeltaResult, Error};

0 commit comments

Comments
 (0)