Skip to main content

mito2/compaction/
run.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This file contains code to find sorted runs in a set if ranged items and
16//! along with the best way to merge these items to satisfy the desired run count.
17
18use std::cmp::Ordering;
19use std::collections::BinaryHeap;
20
21use bytes::{Buf, Bytes};
22use common_base::BitVec;
23use common_time::Timestamp;
24
25use crate::sst::file::FileHandle;
26use crate::sst::primary_key::PrimaryKeyRangeMapper;
27
28/// Trait for any items with specific range (both boundaries are inclusive).
29pub trait Ranged {
30    type BoundType: Ord + Copy;
31
32    /// Returns the inclusive range of item.
33    fn range(&self) -> (Self::BoundType, Self::BoundType);
34
35    fn overlap(&self, other: &Self) -> bool {
36        let (lhs_start, lhs_end) = self.range();
37        let (rhs_start, rhs_end) = other.range();
38
39        lhs_start.max(rhs_start) < lhs_end.min(rhs_end)
40    }
41
42    /// Like `overlap`, but treats touching boundaries as overlapping (inclusive).
43    /// Used by `find_overlapping_items` where shared boundaries count as overlap.
44    fn overlap_inclusive(&self, other: &Self) -> bool {
45        let (lhs_start, lhs_end) = self.range();
46        let (rhs_start, rhs_end) = other.range();
47
48        lhs_start.max(rhs_start) <= lhs_end.min(rhs_end)
49    }
50}
51
52pub(crate) fn primary_key_ranges_overlap(lhs: &(Bytes, Bytes), rhs: &(Bytes, Bytes)) -> bool {
53    lhs.0.chunk().max(rhs.0.chunk()) <= lhs.1.chunk().min(rhs.1.chunk())
54}
55
56pub(crate) fn merge_primary_key_ranges(
57    lhs: Option<(Bytes, Bytes)>,
58    rhs: Option<(Bytes, Bytes)>,
59) -> Option<(Bytes, Bytes)> {
60    match (lhs, rhs) {
61        (Some((lhs_min, lhs_max)), Some((rhs_min, rhs_max))) => {
62            Some((lhs_min.min(rhs_min), lhs_max.max(rhs_max)))
63        }
64        _ => None,
65    }
66}
67
68/// Uses the caller's inclusive overlap test after pruning disjoint time ranges.
69pub fn find_overlapping_items<T: Item + Clone>(
70    l: &mut SortedRun<T>,
71    r: &mut SortedRun<T>,
72    result: &mut Vec<T>,
73    overlaps: impl Fn(&T, &T) -> bool,
74) {
75    if l.items.is_empty() || r.items.is_empty() {
76        return;
77    }
78
79    result.clear();
80    result.reserve(l.items.len() + r.items.len());
81
82    // Sort both arrays by start boundary for more efficient overlap detection
83    if !l.sorted {
84        sort_ranged_items(&mut l.items);
85        l.sorted = true;
86    }
87    if !r.sorted {
88        sort_ranged_items(&mut r.items);
89        r.sorted = true;
90    }
91
92    let mut r_idx = 0;
93
94    let mut selected = BitVec::repeat(false, r.items().len() + l.items.len());
95
96    for (lhs_idx, lhs) in l.items.iter().enumerate() {
97        let (lhs_start, lhs_end) = lhs.range();
98
99        // Skip right elements that end before current left element starts
100        while r_idx < r.items.len() {
101            let (_, rhs_end) = r.items[r_idx].range();
102            if rhs_end < lhs_start {
103                r_idx += 1;
104            } else {
105                break;
106            }
107        }
108
109        // Check for overlaps with remaining right elements
110        let mut j = r_idx;
111        while j < r.items.len() {
112            let (rhs_start, _rhs_end) = r.items[j].range();
113
114            // If right element starts after left element ends, no more overlaps possible
115            if rhs_start > lhs_end {
116                break;
117            }
118
119            // We have an overlap (inclusive: touching boundaries count)
120            if overlaps(lhs, &r.items[j]) {
121                if !selected[lhs_idx] {
122                    result.push(lhs.clone());
123                    selected.set(lhs_idx, true);
124                }
125
126                let rhs_selected_idx = l.items.len() + j;
127                if !selected[rhs_selected_idx] {
128                    result.push(r.items[j].clone());
129                    selected.set(rhs_selected_idx, true);
130                }
131            }
132
133            j += 1;
134        }
135    }
136}
137
138// Sorts ranges by start asc and end desc.
139fn sort_ranged_items<T: Ranged>(values: &mut [T]) {
140    values.sort_unstable_by(|l, r| {
141        let (l_start, l_end) = l.range();
142        let (r_start, r_end) = r.range();
143        l_start.cmp(&r_start).then(r_end.cmp(&l_end))
144    });
145}
146
147/// Trait for items to merge.
148pub trait Item: Ranged + Clone {
149    /// Size is used to calculate the cost of merging items.
150    fn size(&self) -> usize;
151}
152
153// Physical handles only supply time ranges. PK-aware comparisons need a target schema.
154impl Ranged for FileHandle {
155    type BoundType = Timestamp;
156
157    fn range(&self) -> (Self::BoundType, Self::BoundType) {
158        self.time_range()
159    }
160}
161
162/// Tests exclusive time overlap and inclusive PK overlap in one pinned schema.
163pub(crate) fn files_overlap(
164    lhs: &FileHandle,
165    rhs: &FileHandle,
166    mapper: &PrimaryKeyRangeMapper,
167) -> bool {
168    lhs.overlap(rhs) && file_primary_keys_overlap(lhs, rhs, mapper)
169}
170
171/// Includes touching time and PK boundaries when checking file dependencies.
172pub(crate) fn files_overlap_inclusive(
173    lhs: &FileHandle,
174    rhs: &FileHandle,
175    mapper: &PrimaryKeyRangeMapper,
176) -> bool {
177    lhs.overlap_inclusive(rhs) && file_primary_keys_overlap(lhs, rhs, mapper)
178}
179
180fn file_primary_keys_overlap(
181    lhs: &FileHandle,
182    rhs: &FileHandle,
183    mapper: &PrimaryKeyRangeMapper,
184) -> bool {
185    match (lhs.primary_key_range(mapper), rhs.primary_key_range(mapper)) {
186        (Some(lhs), Some(rhs)) => primary_key_ranges_overlap(&lhs, &rhs),
187        _ => true,
188    }
189}
190
191impl Item for FileHandle {
192    fn size(&self) -> usize {
193        self.size() as usize
194    }
195}
196
197/// A set of files with non-overlapping time ranges.
198#[derive(Debug, Clone)]
199pub struct SortedRun<T: Item> {
200    /// Items to merge
201    items: Vec<T>,
202    /// The total size of all items.
203    size: usize,
204    /// The lower bound of all items.
205    start: Option<T::BoundType>,
206    /// The upper bound of all items.
207    end: Option<T::BoundType>,
208    /// Whether items are sorted.
209    sorted: bool,
210}
211
212impl<T: Item> From<Vec<T>> for SortedRun<T> {
213    fn from(items: Vec<T>) -> Self {
214        let mut r = Self {
215            items: Vec::with_capacity(items.len()),
216            size: 0,
217            start: None,
218            end: None,
219            sorted: false,
220        };
221        for item in items {
222            r.push_item(item);
223        }
224
225        r
226    }
227}
228
229impl<T> Default for SortedRun<T>
230where
231    T: Item,
232{
233    fn default() -> Self {
234        Self {
235            items: vec![],
236            size: 0,
237            start: None,
238            end: None,
239            sorted: false,
240        }
241    }
242}
243
244impl<T> SortedRun<T>
245where
246    T: Item,
247{
248    pub fn items(&self) -> &[T] {
249        &self.items
250    }
251
252    fn push_item(&mut self, t: T) {
253        let (file_start, file_end) = t.range();
254        self.size += t.size();
255        self.items.push(t);
256        self.start = Some(self.start.map_or(file_start, |v| v.min(file_start)));
257        self.end = Some(self.end.map_or(file_end, |v| v.max(file_end)));
258    }
259}
260
261/// Finds sorted runs using the caller's overlap test within each active time range.
262pub fn find_sorted_runs<T>(items: &mut [T], overlaps: impl Fn(&T, &T) -> bool) -> Vec<SortedRun<T>>
263where
264    T: Item,
265{
266    if items.is_empty() {
267        return vec![];
268    }
269    // sort files
270    sort_ranged_items(items);
271
272    let mut current_run = SortedRun::default();
273    let mut runs = vec![];
274    let mut active_run_item_indices = Vec::new();
275
276    let mut selection = BitVec::repeat(false, items.len());
277    while !selection.all() {
278        // until all items are assigned to some sorted run.
279        let mut last_pruned_start = None;
280        for (item, mut selected) in items.iter().zip(selection.iter_mut()) {
281            if *selected {
282                // item is already assigned.
283                continue;
284            }
285            if current_run.items.is_empty() {
286                // current run is empty, just add current_item
287                selected.set(true);
288                current_run.push_item(item.clone());
289                active_run_item_indices.push(current_run.items.len() - 1);
290            } else {
291                // the current item does not overlap with any item in current run,
292                // then it belongs to current run. Because now we introduced primary
293                // key range, we cannot simply use timestamps to check overlapping.
294                let (item_start, _) = item.range();
295                if last_pruned_start != Some(item_start) {
296                    active_run_item_indices.retain(|idx| {
297                        let (_, run_item_end) = current_run.items[*idx].range();
298                        run_item_end > item_start
299                    });
300                    last_pruned_start = Some(item_start);
301                }
302
303                let mut overlaps_any = false;
304                for idx in &active_run_item_indices {
305                    let run_item = &current_run.items[*idx];
306                    if overlaps(run_item, item) {
307                        overlaps_any = true;
308                        break;
309                    }
310                }
311                if !overlaps_any {
312                    // does not overlap, push to current run
313                    selected.set(true);
314                    let item_idx = current_run.items.len();
315                    current_run.push_item(item.clone());
316                    active_run_item_indices.push(item_idx);
317                }
318            }
319        }
320        // finished an iteration, we've found a new run.
321        runs.push(std::mem::take(&mut current_run));
322        active_run_item_indices.clear();
323    }
324    runs
325}
326
327#[cfg(any(test, feature = "test", feature = "testing"))]
328pub fn find_sorted_runs_original<T>(items: &mut [T]) -> Vec<SortedRun<T>>
329where
330    T: Item,
331{
332    if items.is_empty() {
333        return vec![];
334    }
335    // sort files
336    sort_ranged_items(items);
337
338    let mut current_run = SortedRun::default();
339    let mut runs = vec![];
340
341    let mut selection = BitVec::repeat(false, items.len());
342    while !selection.all() {
343        // until all items are assigned to some sorted run.
344        for (item, mut selected) in items.iter().zip(selection.iter_mut()) {
345            if *selected {
346                // item is already assigned.
347                continue;
348            }
349            if current_run.items.is_empty() {
350                // current run is empty, just add current_item
351                selected.set(true);
352                current_run.push_item(item.clone());
353            } else {
354                // the current item does not overlap with any item in current run,
355                // then it belongs to current run. Because now we introduced primary
356                // key range, we cannot simply use timestamps to check overlapping.
357                let overlaps_any = current_run.items.iter().any(|i| i.overlap(item));
358                if !overlaps_any {
359                    // does not overlap, push to current run
360                    selected.set(true);
361                    current_run.push_item(item.clone());
362                }
363            }
364        }
365        // finished an iteration, we've found a new run.
366        runs.push(std::mem::take(&mut current_run));
367    }
368    runs
369}
370
371pub(crate) fn find_sorted_runs_by_time_range<T>(items: &mut [T]) -> Vec<SortedRun<T>>
372where
373    T: Item,
374{
375    if items.is_empty() {
376        return vec![];
377    }
378    sort_ranged_items(items);
379
380    use derive_more::{Eq, PartialEq};
381
382    /// `SortedRun` with a creation sequence `i`.
383    #[derive(PartialEq, Eq)]
384    struct Run<T: Item> {
385        i: usize,
386        #[partial_eq(skip)]
387        run: SortedRun<T>,
388    }
389
390    impl<T: Item> Run<T> {
391        fn new(i: usize, item: &T) -> Run<T> {
392            let mut run = SortedRun::default();
393            run.push_item(item.clone());
394            Run { i, run }
395        }
396
397        fn push_item(&mut self, item: &T) {
398            self.run.push_item(item.clone());
399        }
400    }
401
402    impl<T: Item> PartialOrd for Run<T> {
403        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
404            Some(self.cmp(other))
405        }
406    }
407
408    /// Sort by run's `end` desc then `start` asc.
409    impl<T: Item> Ord for Run<T> {
410        fn cmp(&self, other: &Self) -> Ordering {
411            let l_run = &self.run;
412            let r_run = &other.run;
413
414            // Safety: `start` and `end` must both exist because it's guaranteed that whenever a
415            // `Run` is created, an item is pushed into it immediately (see its `new` method above).
416            // And there are no other ways to create a `Run` beyond its `new` method in this
417            // function's scope.
418            let l_end = l_run.end.unwrap();
419            let r_end = r_run.end.unwrap();
420            r_end
421                .cmp(&l_end)
422                .then_with(|| {
423                    let l_start = l_run.start.unwrap();
424                    let r_start = r_run.start.unwrap();
425                    l_start.cmp(&r_start)
426                })
427                .then_with(|| self.i.cmp(&other.i))
428        }
429    }
430
431    /// Wrapper around the `Run` above, to support sorting them by their creation sequence `i`.
432    #[derive(PartialEq, Eq)]
433    struct Wrapper<T: Item>(Run<T>);
434
435    impl<T: Item> PartialOrd for Wrapper<T> {
436        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
437            Some(self.cmp(other))
438        }
439    }
440
441    impl<T: Item> Ord for Wrapper<T> {
442        fn cmp(&self, other: &Self) -> Ordering {
443            other.0.i.cmp(&self.0.i)
444        }
445    }
446
447    // Two heaps for finding a run that is both:
448    // 1. not overlapping with item's range,
449    // 2. and is created earliest,
450    // when iterating the items.
451    //
452    // Heap 1 (`runs_sorted_by_end`) is for storing the runs of which top has the minimal "end"
453    // just about to overlap with the current selected item.
454    //
455    // Heap 2 (`runs_sort_by_index`) is for storing the runs that all have "end"s non-overlap with
456    // the current selected item, and of which top is the earliest created run.
457    //
458    // The finding of a suitable run basically works like this:
459    // 1. moves the runs in heap 1 to heap 2, until the top is overlapping with the current item;
460    // 2. now heap 2 has all the runs that can accept the current item, pop its top;
461    // 3. the top is the earliest created run, push the current item;
462    // 4. because the run has changed, push it back to heap 1;
463    // 5. check the next item. Important: we don't need to push the runs in heap 2 to 1, because
464    //    the items are sorted by "start". When checking the next item, heap 2's runs must all have
465    //    "end"s smaller than next item's "start".
466    //
467    // Actually the heap 2 is only for aligning with the runs selection outcomes in the original
468    // `find_sorted_runs` implementation. If we just need the invariant that each run has the
469    // non-overlapping items, we can get rid of heap 2 and make the codes simpler.
470
471    let mut runs_sort_by_end = BinaryHeap::<Run<T>>::new();
472    let mut runs_sort_by_index = BinaryHeap::<Wrapper<T>>::new();
473    let mut i = 0;
474
475    for item in items {
476        let (start, _) = item.range();
477
478        while runs_sort_by_end
479            .peek()
480            .is_some_and(|x| x.run.end.unwrap() <= start)
481        {
482            let run = runs_sort_by_end.pop().unwrap();
483            runs_sort_by_index.push(Wrapper(run));
484        }
485
486        let Some(mut run) = runs_sort_by_index.pop() else {
487            i += 1;
488            runs_sort_by_end.push(Run::new(i, item));
489            continue;
490        };
491
492        run.0.push_item(item);
493        runs_sort_by_end.push(run.0);
494    }
495
496    let mut runs = runs_sort_by_end.into_vec();
497    runs.extend(runs_sort_by_index.into_vec().into_iter().map(|x| x.0));
498    runs.sort_unstable_by_key(|run| run.i);
499    runs.into_iter().map(|x| x.run).collect()
500}
501
502#[cfg(test)]
503mod tests {
504    use store_api::storage::FileId;
505
506    use super::*;
507    use crate::compaction::test_util::{
508        new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
509        primary_key_mapper_for_test,
510    };
511
512    #[derive(Clone, Debug, PartialEq)]
513    struct MockFile {
514        start: i64,
515        end: i64,
516        size: usize,
517    }
518
519    impl Ranged for MockFile {
520        type BoundType = i64;
521
522        fn range(&self) -> (Self::BoundType, Self::BoundType) {
523            (self.start, self.end)
524        }
525    }
526
527    impl Item for MockFile {
528        fn size(&self) -> usize {
529            self.size
530        }
531    }
532
533    fn build_items(ranges: &[(i64, i64)]) -> Vec<MockFile> {
534        ranges
535            .iter()
536            .map(|(start, end)| MockFile {
537                start: *start,
538                end: *end,
539                size: (*end - *start) as usize,
540            })
541            .collect()
542    }
543
544    fn check_sorted_runs(
545        ranges: &[(i64, i64)],
546        expected_runs: &[Vec<(i64, i64)>],
547    ) -> Vec<SortedRun<MockFile>> {
548        let mut files = build_items(ranges);
549        let mut files_clone = files.clone();
550
551        let runs = find_sorted_runs(&mut files, Ranged::overlap);
552
553        let result_file_ranges: Vec<Vec<_>> = runs
554            .iter()
555            .map(|r| r.items.iter().map(|f| f.range()).collect())
556            .collect();
557        assert_eq!(&expected_runs, &result_file_ranges);
558
559        let runs_by_time_range = find_sorted_runs_by_time_range(&mut files_clone);
560        let results: Vec<Vec<_>> = runs_by_time_range
561            .iter()
562            .map(|r| r.items.iter().map(|f| f.range()).collect())
563            .collect();
564        assert_eq!(&expected_runs, &results);
565        runs
566    }
567
568    fn sorted_run_ranges<T: Item>(runs: &[SortedRun<T>]) -> Vec<Vec<T::BoundType>> {
569        runs.iter()
570            .map(|r| {
571                r.items
572                    .iter()
573                    .flat_map(|f| {
574                        let (start, end) = f.range();
575                        [start, end]
576                    })
577                    .collect()
578            })
579            .collect()
580    }
581
582    fn check_find_sorted_runs_consistency(ranges: &[(i64, i64)]) {
583        let mut files = build_items(ranges);
584        let mut files_for_original = files.clone();
585
586        let runs = find_sorted_runs(&mut files, Ranged::overlap);
587        let original_runs = find_sorted_runs_original(&mut files_for_original);
588
589        assert_eq!(sorted_run_ranges(&original_runs), sorted_run_ranges(&runs));
590    }
591
592    #[test]
593    fn test_find_sorted_runs() {
594        check_sorted_runs(&[], &[]);
595        check_sorted_runs(&[(1, 1), (2, 2)], &[vec![(1, 1), (2, 2)]]);
596        check_sorted_runs(&[(1, 2)], &[vec![(1, 2)]]);
597        check_sorted_runs(&[(1, 2), (2, 3)], &[vec![(1, 2), (2, 3)]]);
598        check_sorted_runs(&[(1, 2), (3, 4)], &[vec![(1, 2), (3, 4)]]);
599        check_sorted_runs(&[(2, 4), (1, 3)], &[vec![(1, 3)], vec![(2, 4)]]);
600        check_sorted_runs(
601            &[(1, 3), (2, 4), (4, 5)],
602            &[vec![(1, 3), (4, 5)], vec![(2, 4)]],
603        );
604
605        check_sorted_runs(
606            &[(1, 2), (3, 4), (3, 5)],
607            &[vec![(1, 2), (3, 5)], vec![(3, 4)]],
608        );
609
610        check_sorted_runs(
611            &[(1, 3), (2, 4), (5, 6)],
612            &[vec![(1, 3), (5, 6)], vec![(2, 4)]],
613        );
614
615        check_sorted_runs(
616            &[(1, 2), (3, 5), (4, 6)],
617            &[vec![(1, 2), (3, 5)], vec![(4, 6)]],
618        );
619
620        check_sorted_runs(
621            &[(1, 2), (3, 4), (4, 6), (7, 8)],
622            &[vec![(1, 2), (3, 4), (4, 6), (7, 8)]],
623        );
624        check_sorted_runs(
625            &[(1, 2), (3, 4), (5, 6), (3, 6), (7, 8), (8, 9)],
626            &[vec![(1, 2), (3, 6), (7, 8), (8, 9)], vec![(3, 4), (5, 6)]],
627        );
628
629        check_sorted_runs(
630            &[(10, 19), (20, 21), (20, 29), (30, 39)],
631            &[vec![(10, 19), (20, 29), (30, 39)], vec![(20, 21)]],
632        );
633
634        check_sorted_runs(
635            &[(10, 19), (20, 29), (21, 22), (30, 39), (31, 32), (32, 42)],
636            &[
637                vec![(10, 19), (20, 29), (30, 39)],
638                vec![(21, 22), (31, 32), (32, 42)],
639            ],
640        );
641    }
642
643    #[test]
644    fn test_find_sorted_runs_matches_original_impl() {
645        for ranges in [
646            &[][..],
647            &[(1, 1), (2, 2)],
648            &[(1, 2), (2, 3)],
649            &[(2, 4), (1, 3)],
650            &[(1, 3), (2, 4), (4, 5)],
651            &[(1, 2), (3, 4), (3, 5)],
652            &[(1, 3), (2, 4), (5, 6)],
653            &[(1, 2), (3, 5), (4, 6)],
654            &[(1, 2), (3, 4), (4, 6), (7, 8)],
655            &[(1, 2), (3, 4), (5, 6), (3, 6), (7, 8), (8, 9)],
656            &[(10, 19), (20, 21), (20, 29), (30, 39)],
657            &[(10, 19), (20, 29), (21, 22), (30, 39), (31, 32), (32, 42)],
658            &[(32, 42), (10, 19), (31, 32), (20, 29), (21, 22), (30, 39)],
659        ] {
660            check_find_sorted_runs_consistency(ranges);
661        }
662    }
663
664    #[test]
665    fn test_find_overlapping_items() {
666        let mut result = Vec::new();
667
668        // Test empty inputs
669        find_overlapping_items(
670            &mut SortedRun::from(Vec::<MockFile>::new()),
671            &mut SortedRun::from(Vec::<MockFile>::new()),
672            &mut result,
673            Ranged::overlap_inclusive,
674        );
675        assert_eq!(result, Vec::<MockFile>::new());
676
677        let files1 = build_items(&[(1, 3)]);
678        find_overlapping_items(
679            &mut SortedRun::from(files1.clone()),
680            &mut SortedRun::from(Vec::<MockFile>::new()),
681            &mut result,
682            Ranged::overlap_inclusive,
683        );
684        assert_eq!(result, Vec::<MockFile>::new());
685
686        find_overlapping_items(
687            &mut SortedRun::from(Vec::<MockFile>::new()),
688            &mut SortedRun::from(files1.clone()),
689            &mut result,
690            Ranged::overlap_inclusive,
691        );
692        assert_eq!(result, Vec::<MockFile>::new());
693
694        // Test non-overlapping ranges
695        let files1 = build_items(&[(1, 3), (5, 7)]);
696        let files2 = build_items(&[(10, 12), (15, 20)]);
697        find_overlapping_items(
698            &mut SortedRun::from(files1),
699            &mut SortedRun::from(files2),
700            &mut result,
701            Ranged::overlap_inclusive,
702        );
703        assert_eq!(result, Vec::<MockFile>::new());
704
705        // Test simple overlap
706        let files1 = build_items(&[(1, 5)]);
707        let files2 = build_items(&[(3, 7)]);
708        find_overlapping_items(
709            &mut SortedRun::from(files1),
710            &mut SortedRun::from(files2),
711            &mut result,
712            Ranged::overlap_inclusive,
713        );
714        assert_eq!(result.len(), 2);
715        assert_eq!(result[0].range(), (1, 5));
716        assert_eq!(result[1].range(), (3, 7));
717
718        // Test multiple overlaps
719        let files1 = build_items(&[(1, 5), (8, 12), (15, 20)]);
720        let files2 = build_items(&[(3, 6), (7, 10), (18, 25)]);
721        find_overlapping_items(
722            &mut SortedRun::from(files1),
723            &mut SortedRun::from(files2),
724            &mut result,
725            Ranged::overlap_inclusive,
726        );
727        assert_eq!(result.len(), 6);
728
729        // Test boundary cases (touching but not overlapping)
730        let files1 = build_items(&[(1, 5)]);
731        let files2 = build_items(&[(5, 10)]); // Touching at 5
732        find_overlapping_items(
733            &mut SortedRun::from(files1),
734            &mut SortedRun::from(files2),
735            &mut result,
736            Ranged::overlap_inclusive,
737        );
738        assert_eq!(result.len(), 2); // Should overlap since ranges are inclusive
739
740        // Test completely contained ranges
741        let files1 = build_items(&[(1, 10)]);
742        let files2 = build_items(&[(3, 7)]);
743        find_overlapping_items(
744            &mut SortedRun::from(files1),
745            &mut SortedRun::from(files2),
746            &mut result,
747            Ranged::overlap_inclusive,
748        );
749        assert_eq!(result.len(), 2);
750
751        // Test identical ranges
752        let files1 = build_items(&[(1, 5)]);
753        let files2 = build_items(&[(1, 5)]);
754        find_overlapping_items(
755            &mut SortedRun::from(files1),
756            &mut SortedRun::from(files2),
757            &mut result,
758            Ranged::overlap_inclusive,
759        );
760        assert_eq!(result.len(), 2);
761
762        // Test unsorted input handling
763        let files1 = build_items(&[(5, 10), (1, 3)]); // Unsorted
764        let files2 = build_items(&[(2, 7), (8, 12)]); // Unsorted
765        find_overlapping_items(
766            &mut SortedRun::from(files1),
767            &mut SortedRun::from(files2),
768            &mut result,
769            Ranged::overlap_inclusive,
770        );
771        assert_eq!(result.len(), 4); // Should find both overlaps
772    }
773
774    #[test]
775    fn test_file_overlap_time_overlap_pk_disjoint() {
776        let lhs = new_file_handle_with_size_sequence_and_primary_key_range(
777            FileId::random(),
778            0,
779            100,
780            0,
781            1,
782            10,
783            pk_range(b"a", b"f"),
784        );
785        let rhs = new_file_handle_with_size_sequence_and_primary_key_range(
786            FileId::random(),
787            50,
788            150,
789            0,
790            2,
791            10,
792            pk_range(b"x", b"z"),
793        );
794
795        assert!(!files_overlap(&lhs, &rhs, &primary_key_mapper_for_test()));
796    }
797
798    #[test]
799    fn test_find_sorted_runs_collapses_pk_disjoint_files_into_one_run() {
800        let mut files = vec![
801            new_file_handle_with_size_sequence_and_primary_key_range(
802                FileId::random(),
803                0,
804                100,
805                0,
806                1,
807                10,
808                pk_range(b"a", b"f"),
809            ),
810            new_file_handle_with_size_sequence_and_primary_key_range(
811                FileId::random(),
812                50,
813                150,
814                0,
815                2,
816                10,
817                pk_range(b"x", b"z"),
818            ),
819        ];
820
821        let ranges = primary_key_mapper_for_test();
822        let runs = find_sorted_runs(&mut files, |lhs, rhs| files_overlap(lhs, rhs, &ranges));
823
824        assert_eq!(1, runs.len());
825        assert_eq!(2, runs[0].items().len());
826    }
827
828    #[test]
829    fn test_find_sorted_runs_handles_2d_transitivity_break() {
830        let mut files = vec![
831            new_file_handle_with_size_sequence_and_primary_key_range(
832                FileId::random(),
833                0,
834                100,
835                0,
836                1,
837                10,
838                pk_range(b"a", b"f"),
839            ),
840            new_file_handle_with_size_sequence_and_primary_key_range(
841                FileId::random(),
842                50,
843                150,
844                0,
845                2,
846                10,
847                pk_range(b"x", b"z"),
848            ),
849            new_file_handle_with_size_sequence_and_primary_key_range(
850                FileId::random(),
851                50,
852                150,
853                0,
854                3,
855                10,
856                pk_range(b"a", b"f"),
857            ),
858        ];
859
860        let ranges = primary_key_mapper_for_test();
861        let runs = find_sorted_runs(&mut files, |lhs, rhs| files_overlap(lhs, rhs, &ranges));
862
863        assert_eq!(2, runs.len());
864        assert_eq!(2, runs[0].items().len());
865        assert_eq!(1, runs[1].items().len());
866    }
867
868    #[test]
869    fn test_find_overlapping_items_skips_pk_disjoint_pairs() {
870        let mut left = SortedRun::from(vec![
871            new_file_handle_with_size_sequence_and_primary_key_range(
872                FileId::random(),
873                0,
874                100,
875                0,
876                1,
877                10,
878                pk_range(b"a", b"f"),
879            ),
880        ]);
881        let mut right = SortedRun::from(vec![
882            new_file_handle_with_size_sequence_and_primary_key_range(
883                FileId::random(),
884                50,
885                150,
886                0,
887                2,
888                10,
889                pk_range(b"x", b"z"),
890            ),
891        ]);
892        let mut result = Vec::new();
893
894        let ranges = primary_key_mapper_for_test();
895        find_overlapping_items(&mut left, &mut right, &mut result, |lhs, rhs| {
896            files_overlap_inclusive(lhs, rhs, &ranges)
897        });
898
899        assert!(result.is_empty());
900    }
901
902    #[test]
903    fn test_file_touching_time_boundary_with_same_pk_is_not_overlap() {
904        let lhs = new_file_handle_with_size_sequence_and_primary_key_range(
905            FileId::random(),
906            0,
907            100,
908            0,
909            1,
910            10,
911            pk_range(b"a", b"f"),
912        );
913        let rhs = new_file_handle_with_size_sequence_and_primary_key_range(
914            FileId::random(),
915            100,
916            150,
917            0,
918            2,
919            10,
920            pk_range(b"a", b"f"),
921        );
922
923        assert!(!files_overlap(&lhs, &rhs, &primary_key_mapper_for_test()));
924    }
925}