Skip to content

Commit eb60a11

Browse files
committed
Fix regression in prefix iterator
# Description The issue was that in the case that prefix search terminated in an inner node due, it was confusing the cases of (1) running out of key bytes or (2) an actual mismatch between bytes of the inner node prefix and the key bytes. In case (1) we know that all the nodes of the subtree should be returned by the iterator, since the key is a prefix of all those leaves. In case (2), no nodes of the subtree should be returned, since there was a mismatch all nodes will not be prefixed by the key. The fix was to make the `InnerNodeSearchResultReason` have another variant which reports when the search ran out of bytes. # Testing Regression tests were added in a previous commit and labeled `should_panic`, this commit just removed that label and all tests still passed.
1 parent 0a90700 commit eb60a11

3 files changed

Lines changed: 85 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2020
- `TreeMap::{get_prefix, get_prefix_key_value, get_prefix_mut, force_insert}` were renamed to `TreeMap::{prefix_get, prefix_get_key_value, prefix_get_mut, prefix_insert}`. This makes it clear that they're all centered around the theme of manipulating or querying key prefixes.
2121
- Update MSRV to 1.85
2222

23+
### Fixed
24+
25+
- `TreeMap::prefix` iterator was broken (see [issue 66]) because a refactor stopped distinguishing between different cases of inner nodes matching or not matching the prefix.
26+
27+
[issue 66]: https://github.com/declanvk/blart/issues/66
28+
2329
## [0.4.0] - 2025-07-10
2430

2531
### Added

src/collections/map/iterators/prefix.rs

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::{
66
};
77
use crate::{
88
allocator::{Allocator, Global},
9-
map::DEFAULT_PREFIX_LEN,
9+
map::{NonEmptyTree, DEFAULT_PREFIX_LEN},
1010
raw::{maximum_unchecked, minimum_unchecked, RawIterator},
1111
AsBytes, TreeMap,
1212
};
@@ -40,48 +40,11 @@ macro_rules! implement_prefix_iter {
4040
};
4141
};
4242

43-
// SAFETY: Since we have a (shared or mutable) reference to the original
44-
// TreeMap, we know there will be no concurrent mutation
45-
let search_result = unsafe { find_terminating_node(tree_state.root, prefix) };
46-
let (start, end) = match search_result {
47-
TerminatingNodeSearchResult::Leaf { leaf_ptr, .. } => {
48-
// SAFETY: Its safe to create a shared reference to the leaf since we hold
49-
// either a shared or mutable reference to the owning TreeMap, which prevents
50-
// other concurrent mutable references.
51-
let leaf = unsafe { leaf_ptr.as_ref() };
52-
53-
// Only include the item in the iterator if the prefix actually matches
54-
if leaf.key_ref().as_bytes().starts_with(prefix) {
55-
(leaf_ptr, leaf_ptr)
56-
} else {
57-
return Self {
58-
_tree: tree,
59-
inner: RawIterator::empty(),
60-
};
61-
}
62-
},
63-
TerminatingNodeSearchResult::InnerNode(InnerNodeSearchResult { node_ptr, reason, .. }) => {
64-
if matches!(reason, InnerNodeSearchResultReason::MissingChild) {
65-
// if the child is missing, then there is nothing to be the prefix of
66-
return Self {
67-
_tree: tree,
68-
inner: RawIterator::empty(),
69-
};
70-
}
71-
72-
// SAFETY: Its safe to create a shared reference to the leaf since we hold
73-
// either a shared or mutable reference to the owning TreeMap, which prevents
74-
// other concurrent mutable references.
75-
unsafe { (minimum_unchecked(node_ptr), maximum_unchecked(node_ptr)) }
76-
},
77-
};
43+
let inner = prefix_iter_constructor(tree_state, prefix);
7844

7945
Self {
8046
_tree: tree,
81-
// SAFETY: `start` is guaranteed to be less than or equal to `end` in the iteration
82-
// order because of the check we do on the bytes of the resolved leaf pointers, just
83-
// above this line
84-
inner: unsafe { RawIterator::new(start, end) },
47+
inner,
8548
}
8649
}
8750
}
@@ -120,6 +83,53 @@ macro_rules! implement_prefix_iter {
12083
};
12184
}
12285

86+
fn prefix_iter_constructor<K: AsBytes, V, const PREFIX_LEN: usize>(
87+
tree_state: &NonEmptyTree<K, V, PREFIX_LEN>,
88+
prefix: &[u8],
89+
) -> RawIterator<K, V, PREFIX_LEN> {
90+
// SAFETY: Since we have a (shared or mutable) reference to the original
91+
// TreeMap, we know there will be no concurrent mutation
92+
let search_result = unsafe { find_terminating_node(tree_state.root, prefix) };
93+
let (start, end) = match search_result {
94+
TerminatingNodeSearchResult::Leaf { leaf_ptr, .. } => {
95+
// SAFETY: Its safe to create a shared reference to the leaf since we hold
96+
// either a shared or mutable reference to the owning TreeMap, which prevents
97+
// other concurrent mutable references.
98+
let leaf = unsafe { leaf_ptr.as_ref() };
99+
100+
// Only include the item in the iterator if the prefix actually matches
101+
if leaf.key_ref().as_bytes().starts_with(prefix) {
102+
(leaf_ptr, leaf_ptr)
103+
} else {
104+
return RawIterator::empty();
105+
}
106+
},
107+
TerminatingNodeSearchResult::InnerNode(InnerNodeSearchResult {
108+
node_ptr, reason, ..
109+
}) => {
110+
match reason {
111+
InnerNodeSearchResultReason::PrefixMismatch
112+
| InnerNodeSearchResultReason::MissingChild => {
113+
// if the child is missing OR there is a prefix mismatch, then there is nothing
114+
// to be the prefix of
115+
return RawIterator::empty();
116+
},
117+
InnerNodeSearchResultReason::InsufficientBytes => {
118+
// SAFETY: Its safe to create a shared reference to the leaf since we hold
119+
// either a shared or mutable reference to the owning TreeMap, which prevents
120+
// other concurrent mutable references.
121+
unsafe { (minimum_unchecked(node_ptr), maximum_unchecked(node_ptr)) }
122+
},
123+
}
124+
},
125+
};
126+
127+
// SAFETY: `start` is guaranteed to be less than or equal to `end` in the
128+
// iteration order because of the check we do on the bytes of the resolved
129+
// leaf pointers, just above this line
130+
unsafe { RawIterator::new(start, end) }
131+
}
132+
123133
implement_prefix_iter!(
124134
/// An iterator over a range of entries that all have the same key prefix in a [`TreeMap`].
125135
///
@@ -438,8 +448,7 @@ mod tests {
438448
/// the prefix. In blart 0.3.0 and 0.4.0 it incorrectly returns both
439449
/// keys.
440450
#[test]
441-
#[should_panic(expected = "`left == right` failed")]
442-
fn test_prefix_iterator_no_false_matches() {
451+
fn prefix_iterator_no_false_matches() {
443452
// Construct a map with the following keys
444453
// [0, 0]
445454
// [0, 1]

src/collections/map/iterators/range.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
raw::{
1515
match_concrete_node_ptr, maximum_unchecked, minimum_unchecked,
1616
AttemptOptimisticPrefixMatch, ConcreteNodePtr, InnerNode, LeafNode, NodePtr, OpaqueNodePtr,
17-
PrefixMatchBehavior, RawIterator,
17+
PessimisticMismatch, PrefixMatchBehavior, RawIterator,
1818
},
1919
AsBytes, TreeMap,
2020
};
@@ -55,9 +55,11 @@ impl<K, V, const PREFIX_LEN: usize> fmt::Debug for InnerNodeSearchResult<K, V, P
5555
#[derive(Debug)]
5656
pub(crate) enum InnerNodeSearchResultReason {
5757
/// This variant means the search terminated in a mismatched prefix of an
58-
/// inner node OR the prefix matched, but there were insufficient key bytes
59-
/// to lookup a child of the inner node.
60-
PrefixMismatchOrInsufficientBytes,
58+
/// inner node.
59+
PrefixMismatch,
60+
/// This variant means the search terminated because the key was too short,
61+
/// not because of a mismatch with prefix or child key byte.
62+
InsufficientBytes,
6163
/// This variant means the search terminated in an inner node, when there
6264
/// was no corresponding child for a key byte.
6365
MissingChild,
@@ -136,17 +138,34 @@ pub(crate) unsafe fn find_terminating_node<K: AsBytes, V, const PREFIX_LEN: usiz
136138
let match_prefix =
137139
prefix_match_behavior.match_prefix(inner_node, &key_bytes[*current_depth..]);
138140
match match_prefix {
139-
Err(_) => {
141+
Err(PessimisticMismatch { matched_bytes, .. }) => {
140142
let (full_prefix, _) = inner_node.read_full_prefix(*current_depth);
141143
let upper_bound = (*current_depth + full_prefix.len()).min(key_bytes.len());
142144
let key_segment = &key_bytes[(*current_depth)..upper_bound];
143145

144146
let node_prefix_comparison_to_search_key_segment = full_prefix.cmp(key_segment);
145147

148+
debug_assert_ne!(
149+
node_prefix_comparison_to_search_key_segment,
150+
Ordering::Equal,
151+
"if there was a mismatch, the prefix must not be equal"
152+
);
153+
154+
// We report a prefix mismatch if the prefix is longer that the remaining
155+
// portion of the key bytes. However, in the context of the
156+
// `InnerNodeSearchResultReason` running out of bytes is really a different
157+
// thing than a mismatch. We should only report mismatch as the reason if there
158+
// was an actual byte difference.
159+
let reason = if matched_bytes == key_bytes[*current_depth..].len() {
160+
InnerNodeSearchResultReason::InsufficientBytes
161+
} else {
162+
InnerNodeSearchResultReason::PrefixMismatch
163+
};
164+
146165
ControlFlow::Break(InnerNodeSearchResult {
147166
node_ptr: inner_ptr.to_opaque(),
148167
current_depth: *current_depth,
149-
reason: InnerNodeSearchResultReason::PrefixMismatchOrInsufficientBytes,
168+
reason,
150169
node_prefix_comparison_to_search_key_segment,
151170
})
152171
},
@@ -163,7 +182,7 @@ pub(crate) unsafe fn find_terminating_node<K: AsBytes, V, const PREFIX_LEN: usiz
163182
return ControlFlow::Break(InnerNodeSearchResult {
164183
node_ptr: inner_ptr.to_opaque(),
165184
current_depth: *current_depth,
166-
reason: InnerNodeSearchResultReason::PrefixMismatchOrInsufficientBytes,
185+
reason: InnerNodeSearchResultReason::InsufficientBytes,
167186
node_prefix_comparison_to_search_key_segment: Ordering::Equal,
168187
});
169188
};
@@ -308,7 +327,8 @@ pub(crate) unsafe fn find_leaf_pointer_for_bound<K: AsBytes, V, const PREFIX_LEN
308327
reason,
309328
node_prefix_comparison_to_search_key_segment,
310329
}) => match reason {
311-
InnerNodeSearchResultReason::PrefixMismatchOrInsufficientBytes => {
330+
InnerNodeSearchResultReason::PrefixMismatch
331+
| InnerNodeSearchResultReason::InsufficientBytes => {
312332
match (is_start, node_prefix_comparison_to_search_key_segment) {
313333
(true, Ordering::Equal | Ordering::Greater) => unsafe {
314334
// SAFETY: Covered by function safety doc

0 commit comments

Comments
 (0)