1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-present, fjall-rs
// Copyright (c) 2026-present, Dmitry Prudnikov
//! Custom merge heap backed by a sorted vector of slot numbers.
//!
//! Supports both min and max extraction (for forward and reverse
//! iteration) on the same data structure, unlike two separate heaps.
//!
//! The key optimisation is `replace_min` / `replace_max`: replacing the
//! extremum in its slot and moving its number to the sorted position. In
//! the common case — a sequential scan where the same source keeps
//! winning — the replacement is still the extremum and the operation
//! completes in **one comparison** (O(1)), which a tournament tree cannot
//! match. Inputs that take turns cost O(log n) comparisons: the position
//! is found by binary search, and only the slot numbers move, never the
//! entries.
use crate::InternalValue;
use crate::comparator::SharedComparator;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::cmp::Ordering;
/// A single entry in the merge heap.
///
/// Comparator is stored once in the heap, not per entry — eliminating
/// the per-item `Arc` clone that the old `HeapItem` required.
pub(crate) struct HeapEntry {
index: usize,
value: InternalValue,
}
impl HeapEntry {
pub(crate) fn new(index: usize, value: InternalValue) -> Self {
Self { index, value }
}
#[inline]
pub(crate) fn index(&self) -> usize {
self.index
}
pub(crate) fn into_value(self) -> InternalValue {
self.value
}
}
// ---------------------------------------------------------------------------
// MergeHeap
// ---------------------------------------------------------------------------
/// Merge heap backed by a sorted vector of slot numbers.
///
/// Entries stay in `slots`; `order` lists the occupied slots in ascending
/// entry order, so `order[0]` is the minimum and `order[last]` the maximum.
/// This makes both `pop_min` / `pop_max` and `replace_min` / `replace_max`
/// straightforward, and an entry never moves while it is in the heap.
pub(crate) struct MergeHeap {
slots: Vec<Option<HeapEntry>>,
/// Slots a pop emptied, for the next push.
free: Vec<u32>,
order: Vec<u32>,
comparator: SharedComparator,
/// The comparator orders keys as their bytes do, so keys are compared
/// directly rather than through it. See
/// [`crate::comparator::UserComparator::is_lexicographic`].
lexicographic: bool,
/// Each slot's [`key_word`], so most comparisons stop at two integers
/// beside `order` and never reach the entries, let alone their keys.
words: Vec<u64>,
/// Bytes every key of this merge starts with, which no comparison needs
/// to look at.
common_prefix: usize,
}
/// The eight bytes of `key` past `offset`, as a big-endian integer padded with
/// zeros, so two words that differ order their keys as their bytes do: a word
/// only ties, and a tie falls back to the keys. Zero unless keys are ordered by
/// their bytes, which ties every comparison.
#[inline]
fn key_word(key: &[u8], offset: usize, lexicographic: bool) -> u64 {
if !lexicographic {
return 0;
}
let tail = key.get(offset..).unwrap_or_default();
if let Some(head) = tail.first_chunk::<8>() {
return u64::from_be_bytes(*head);
}
let mut padded = [0; 8];
if let Some(head) = padded.get_mut(..tail.len()) {
head.copy_from_slice(tail);
}
u64::from_be_bytes(padded)
}
/// The order of two entries.
///
/// Ties (same user key + same seqno) are broken by source index,
/// with lower indices sorting first. This ensures deterministic
/// merge order; callers that need "newer wins" semantics must pass
/// sources in newest-first precedence order.
#[inline]
fn compare(
a: &HeapEntry,
b: &HeapEntry,
lexicographic: bool,
comparator: &SharedComparator,
) -> Ordering {
let keys = if lexicographic {
a.value.key.cmp(&b.value.key)
} else {
a.value.key.compare_with(&b.value.key, comparator.as_ref())
};
keys.then_with(|| a.index.cmp(&b.index))
}
/// The entry in `slot`, which `order` lists as occupied.
#[inline]
#[expect(
clippy::indexing_slicing,
reason = "`order` only holds numbers of slots"
)]
#[expect(clippy::expect_used, reason = "`order` only holds occupied slots")]
fn occupied(slots: &[Option<HeapEntry>], slot: u32) -> &HeapEntry {
slots[slot as usize]
.as_ref()
.expect("an ordered slot holds an entry")
}
impl MergeHeap {
/// Creates an empty heap pre-allocated for `cap` entries.
pub(crate) fn with_capacity(cap: usize, comparator: SharedComparator) -> Self {
let lexicographic = comparator.is_lexicographic();
Self {
slots: Vec::with_capacity(cap),
free: Vec::with_capacity(cap),
order: Vec::with_capacity(cap),
comparator,
lexicographic,
words: Vec::with_capacity(cap),
common_prefix: 0,
}
}
/// Declares that every key this merge will hold starts with the same
/// `len` bytes, which the comparisons then skip.
///
/// Must be set before the first push.
pub(crate) fn use_common_prefix(&mut self, len: usize) {
debug_assert!(self.order.is_empty(), "set before the first push");
self.common_prefix = len;
}
fn word_of(&self, entry: &HeapEntry) -> u64 {
debug_assert!(
!self.lexicographic || entry.value.key.user_key.len() >= self.common_prefix,
"a key shorter than the prefix every key is said to start with",
);
key_word(
&entry.value.key.user_key,
self.common_prefix,
self.lexicographic,
)
}
#[inline]
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.order.is_empty()
}
/// Returns a reference to the minimum (first) entry.
#[inline]
pub(crate) fn peek_min(&self) -> Option<&HeapEntry> {
let slot = *self.order.first()?;
Some(occupied(&self.slots, slot))
}
/// Returns a reference to the maximum (last) entry.
#[inline]
pub(crate) fn peek_max(&self) -> Option<&HeapEntry> {
let slot = *self.order.last()?;
Some(occupied(&self.slots, slot))
}
/// Inserts a new entry, maintaining sorted order.
#[expect(
clippy::indexing_slicing,
reason = "a free slot is a slot; the new one is pushed just before"
)]
#[expect(
clippy::cast_possible_truncation,
reason = "the heap holds at most two entries per source"
)]
pub(crate) fn push(&mut self, entry: HeapEntry) {
// A lone entry's word is not kept up: nothing compares it.
if let [lone] = *self.order.as_slice() {
self.words[lone as usize] = self.word_of(occupied(&self.slots, lone));
}
let word = self.word_of(&entry);
let pos = self.order.partition_point(|&slot| {
let held = occupied(&self.slots, slot);
compare(held, &entry, self.lexicographic, &self.comparator) != Ordering::Greater
});
let slot = if let Some(slot) = self.free.pop() {
self.slots[slot as usize] = Some(entry);
self.words[slot as usize] = word;
slot
} else {
self.slots.push(Some(entry));
self.words.push(word);
(self.slots.len() - 1) as u32
};
self.order.insert(pos, slot);
}
/// The order of the entry in `slot`, whose word is `word`, against the one
/// in `held`: by their words, and by the entries when those tie.
#[inline]
#[expect(
clippy::indexing_slicing,
reason = "`order` only holds numbers of slots"
)]
fn order_against(&self, slot: u32, word: u64, held: u32) -> Ordering {
match word.cmp(&self.words[held as usize]) {
Ordering::Equal => compare(
occupied(&self.slots, slot),
occupied(&self.slots, held),
self.lexicographic,
&self.comparator,
),
order => order,
}
}
/// Empties `slot` for a later push.
#[expect(
clippy::indexing_slicing,
reason = "`order` only holds numbers of slots"
)]
fn release(&mut self, slot: u32) -> Option<HeapEntry> {
self.free.push(slot);
self.slots[slot as usize].take()
}
/// Removes and returns the minimum entry.
pub(crate) fn pop_min(&mut self) -> Option<HeapEntry> {
if self.order.is_empty() {
return None;
}
let slot = self.order.remove(0);
self.release(slot)
}
/// Removes and returns the maximum entry.
pub(crate) fn pop_max(&mut self) -> Option<HeapEntry> {
let slot = self.order.pop()?;
self.release(slot)
}
/// Replaces the minimum entry and moves the replacement to its
/// sorted position.
///
/// Returns the old minimum. In the common case (replacement is
/// still the minimum), this completes in **one comparison**.
///
/// # Panics
///
/// Panics if the heap is empty.
#[expect(
clippy::indexing_slicing,
reason = "an empty heap panics by contract; the rest is behind the length guard"
)]
#[expect(clippy::expect_used, reason = "`order` only holds occupied slots")]
pub(crate) fn replace_min(&mut self, entry: HeapEntry) -> HeapEntry {
let slot = self.order[0];
let held = self.slots[slot as usize]
.as_mut()
.expect("an ordered slot holds an entry");
let old = core::mem::replace(held, entry);
if self.order.len() == 1 {
return old;
}
let word = self.word_of(occupied(&self.slots, slot));
self.words[slot as usize] = word;
if self.order_against(slot, word, self.order[1]) != Ordering::Greater {
return old;
}
// Past `order[1]`: the number moves to behind every smaller entry.
let rest = self.order[2..]
.partition_point(|&held| self.order_against(slot, word, held) == Ordering::Greater);
let end = 2 + rest;
self.order.copy_within(1..end, 0);
self.order[end - 1] = slot;
old
}
/// Replaces the maximum entry and moves the replacement to its
/// sorted position.
///
/// Returns the old maximum. In the common case (replacement is
/// still the maximum), this completes in **one comparison**.
///
/// # Panics
///
/// Panics if the heap is empty.
#[expect(
clippy::indexing_slicing,
reason = "an empty heap panics by contract; the rest is behind the length guard"
)]
#[expect(clippy::expect_used, reason = "`order` only holds occupied slots")]
pub(crate) fn replace_max(&mut self, entry: HeapEntry) -> HeapEntry {
let last = self.order.len() - 1;
let slot = self.order[last];
let held = self.slots[slot as usize]
.as_mut()
.expect("an ordered slot holds an entry");
let old = core::mem::replace(held, entry);
if last == 0 {
return old;
}
let word = self.word_of(occupied(&self.slots, slot));
self.words[slot as usize] = word;
if self.order_against(slot, word, self.order[last - 1]) != Ordering::Less {
return old;
}
// Before `order[last - 1]`: the number moves to ahead of every greater
// entry.
let pos = self.order[..last - 1]
.partition_point(|&held| self.order_against(slot, word, held) != Ordering::Less);
self.order.copy_within(pos..last, pos + 1);
self.order[pos] = slot;
old
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "test assertions use unwrap for brevity")]
mod tests;