Skip to main content

lance_table/rowids/
index.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::ops::RangeInclusive;
5use std::sync::Arc;
6
7use super::{RowIdSequence, U64Segment};
8use lance_core::deepsize::DeepSizeOf;
9use lance_core::utils::address::RowAddress;
10use lance_core::utils::deletion::DeletionVector;
11use lance_core::{Error, Result};
12use rangemap::RangeInclusiveMap;
13
14/// An index of row ids
15///
16/// This index is used to map row ids to their corresponding addresses. These
17/// addresses correspond to physical positions in the dataset. See [RowAddress].
18///
19/// This structure only contains rows that physically exist. However, it may
20/// map to addresses that have been tombstoned. A separate tombstone index is
21/// used to track tombstoned rows.
22// (Implementation)
23// Disjoint ranges of row ids are stored as the keys of the map. The values are
24// a pair of segments. The first segment is the row ids, and the second segment
25// is the addresses.
26#[derive(Debug)]
27pub struct RowIdIndex(RangeInclusiveMap<u64, (U64Segment, U64Segment)>);
28
29pub struct FragmentRowIdIndex {
30    pub fragment_id: u32,
31    pub row_id_sequence: Arc<RowIdSequence>,
32    pub deletion_vector: Arc<DeletionVector>,
33}
34
35impl RowIdIndex {
36    /// Create a new index from a list of fragment ids and their corresponding row id sequences.
37    pub fn new(fragment_indices: &[FragmentRowIdIndex]) -> Result<Self> {
38        let chunks = fragment_indices
39            .iter()
40            .flat_map(decompose_sequence)
41            .collect::<Vec<_>>();
42
43        let mut final_chunks = Vec::new();
44        for processed_chunk in prep_index_chunks(chunks) {
45            match processed_chunk {
46                RawIndexChunk::NonOverlapping(chunk) => {
47                    final_chunks.push(chunk);
48                }
49                RawIndexChunk::Overlapping(_range, overlapping_chunks) => {
50                    // Intersecting row-id ranges don't imply intersecting id sets;
51                    // sparse ids and deletion holes leave the union short of the span.
52                    // The real invariant (no id in two fragments) is checked in the merge.
53                    let merged_chunk = merge_overlapping_chunks(overlapping_chunks)?;
54                    final_chunks.push(merged_chunk);
55                }
56            }
57        }
58
59        Ok(Self(RangeInclusiveMap::from_iter(final_chunks)))
60    }
61
62    /// Get the address for a given row id.
63    ///
64    /// Will return None if the row id does not exist in the index.
65    pub fn get(&self, row_id: u64) -> Option<RowAddress> {
66        let (row_id_segment, address_segment) = self.0.get(&row_id)?;
67        let pos = row_id_segment.position(row_id)?;
68        let address = address_segment.get(pos)?;
69        Some(RowAddress::from(address))
70    }
71
72    /// Get addresses for many row ids in one pass over the index.
73    ///
74    /// Returns one entry per input id, in input order (`None` for missing).
75    /// Sorts a working copy of the input internally so the chunk iterator
76    /// is advanced at most once per chunk, amortizing the per-id tree walk
77    /// from O(N ยท log F) to O(F + N).
78    pub fn get_many(&self, row_ids: &[u64]) -> Vec<Option<RowAddress>> {
79        let n = row_ids.len();
80        let mut out = vec![None; n];
81        if n == 0 {
82            return out;
83        }
84
85        let mut sorted: Vec<(u64, usize)> = row_ids.iter().copied().zip(0..n).collect();
86        sorted.sort_unstable_by_key(|&(id, _)| id);
87
88        let mut chunks = self.0.iter().peekable();
89        for (id, orig_idx) in sorted {
90            // Advance past chunks that end before this id.
91            while let Some((range, _)) = chunks.peek() {
92                if *range.end() < id {
93                    chunks.next();
94                } else {
95                    break;
96                }
97            }
98            let Some((range, (row_id_seg, addr_seg))) = chunks.peek() else {
99                break;
100            };
101            if id < *range.start() {
102                continue; // falls in a gap between chunks
103            }
104            if let Some(pos) = row_id_seg.position(id)
105                && let Some(addr) = addr_seg.get(pos)
106            {
107                out[orig_idx] = Some(RowAddress::from(addr));
108            }
109        }
110        out
111    }
112}
113
114impl DeepSizeOf for RowIdIndex {
115    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
116        self.0
117            .iter()
118            .map(|(_, (row_id_segment, address_segment))| {
119                (2 * std::mem::size_of::<u64>())
120                    + std::mem::size_of::<(U64Segment, U64Segment)>()
121                    + row_id_segment.deep_size_of_children(context)
122                    + address_segment.deep_size_of_children(context)
123            })
124            .sum()
125    }
126}
127
128fn decompose_sequence(
129    frag_index: &FragmentRowIdIndex,
130) -> Vec<(RangeInclusive<u64>, (U64Segment, U64Segment))> {
131    let mut start_address: u64 = RowAddress::first_row(frag_index.fragment_id).into();
132    let mut current_offset = 0u32;
133    let no_deletions = frag_index.deletion_vector.is_empty();
134
135    frag_index
136        .row_id_sequence
137        .0
138        .iter()
139        .filter_map(|segment| {
140            let segment_len = segment.len();
141
142            let result = if no_deletions {
143                decompose_segment_no_deletions(segment, start_address)
144            } else {
145                decompose_segment_with_deletions(
146                    segment,
147                    start_address,
148                    current_offset,
149                    &frag_index.deletion_vector,
150                )
151            };
152
153            current_offset += segment_len as u32;
154            start_address += segment_len as u64;
155
156            result
157        })
158        .collect()
159}
160
161/// Build an IndexChunk from a list of (row_id, address) pairs.
162fn build_chunk_from_pairs(pairs: Vec<(u64, u64)>) -> Option<IndexChunk> {
163    if pairs.is_empty() {
164        return None;
165    }
166    let (row_ids, addresses): (Vec<u64>, Vec<u64>) = pairs.into_iter().unzip();
167    let row_id_segment = U64Segment::from_iter(row_ids);
168    let address_segment = U64Segment::from_iter(addresses);
169    let coverage = row_id_segment.range()?;
170    Some((coverage, (row_id_segment, address_segment)))
171}
172
173/// Fast path: no deletions. O(1) for Range segments.
174fn decompose_segment_no_deletions(segment: &U64Segment, start_address: u64) -> Option<IndexChunk> {
175    match segment {
176        U64Segment::Range(range) if !range.is_empty() => {
177            let len = range.end - range.start;
178            let row_id_segment = U64Segment::Range(range.clone());
179            let address_segment = U64Segment::Range(start_address..start_address + len);
180            let coverage = range.start..=range.end - 1;
181            Some((coverage, (row_id_segment, address_segment)))
182        }
183        _ if segment.is_empty() => None,
184        _ => {
185            // Non-Range segments: must iterate to build address mapping.
186            let pairs: Vec<(u64, u64)> = segment
187                .iter()
188                .enumerate()
189                .map(|(i, row_id)| (row_id, start_address + i as u64))
190                .collect();
191            build_chunk_from_pairs(pairs)
192        }
193    }
194}
195
196/// Slow path: has deletions, must check each row.
197fn decompose_segment_with_deletions(
198    segment: &U64Segment,
199    start_address: u64,
200    current_offset: u32,
201    deletion_vector: &DeletionVector,
202) -> Option<IndexChunk> {
203    let pairs: Vec<(u64, u64)> = segment
204        .iter()
205        .enumerate()
206        .filter_map(|(i, row_id)| {
207            let row_offset = current_offset + i as u32;
208            if !deletion_vector.contains(row_offset) {
209                Some((row_id, start_address + i as u64))
210            } else {
211                None
212            }
213        })
214        .collect();
215    build_chunk_from_pairs(pairs)
216}
217
218type IndexChunk = (RangeInclusive<u64>, (U64Segment, U64Segment));
219
220#[derive(Debug)]
221enum RawIndexChunk {
222    NonOverlapping(IndexChunk),
223    Overlapping(RangeInclusive<u64>, Vec<IndexChunk>),
224}
225
226impl RawIndexChunk {
227    fn range_end(&self) -> u64 {
228        match self {
229            Self::NonOverlapping((range, _)) => *range.end(),
230            Self::Overlapping(range, _) => *range.end(),
231        }
232    }
233}
234
235/// Given a vector of index chunks, sort them and return an iterator of index chunks.
236///
237/// The iterator will yield chunks that are non-overlapping or a set of chunks
238/// that are overlapping.
239fn prep_index_chunks(mut chunks: Vec<IndexChunk>) -> impl Iterator<Item = RawIndexChunk> {
240    chunks.sort_by_key(|(range, _)| u64::MAX - *range.start());
241
242    let mut output = Vec::new();
243
244    // Start assuming non-overlapping in first chunk.
245    if let Some(first_chunk) = chunks.pop() {
246        output.push(RawIndexChunk::NonOverlapping(first_chunk));
247    } else {
248        // Early return for empty.
249        return output.into_iter();
250    }
251
252    let mut current_range = 0..=0;
253    let mut current_overlap = Vec::new();
254    while let Some(chunk) = chunks.pop() {
255        debug_assert_eq!(
256            current_overlap
257                .iter()
258                .map(|(range, _): &IndexChunk| *range.start())
259                .min()
260                .unwrap_or_default(),
261            *current_range.start(),
262        );
263        debug_assert_eq!(
264            current_overlap
265                .iter()
266                .map(|(range, _): &IndexChunk| *range.end())
267                .max()
268                .unwrap_or_default(),
269            *current_range.end(),
270        );
271
272        if current_overlap.is_empty() {
273            // We haven't found overlap yet.
274            let last_chunk_end = output.last().unwrap().range_end();
275            if *chunk.0.start() <= last_chunk_end {
276                // We have found overlap.
277                match output.pop().unwrap() {
278                    RawIndexChunk::NonOverlapping(chunk) => {
279                        current_overlap.push(chunk);
280                    }
281                    _ => unreachable!(),
282                }
283                current_overlap.push(chunk);
284
285                let range_start = *current_overlap.first().unwrap().0.start();
286                let range_end = *current_overlap
287                    .last()
288                    .unwrap()
289                    .0
290                    .end()
291                    .max(current_overlap.first().unwrap().0.end());
292                current_range = range_start..=range_end;
293            } else {
294                // We are still in non-overlapping space.
295                output.push(RawIndexChunk::NonOverlapping(chunk));
296            }
297        } else {
298            // We are making an overlap chunk
299            if chunk.0.start() <= current_range.end() {
300                // We are still in overlap.
301                let range_end = *chunk.0.end().max(current_range.end());
302                current_range = *current_range.start()..=range_end;
303
304                current_overlap.push(chunk);
305            } else {
306                // We have exited overlap.
307                output.push(RawIndexChunk::Overlapping(
308                    std::mem::replace(&mut current_range, 0..=0),
309                    std::mem::take(&mut current_overlap),
310                ));
311                output.push(RawIndexChunk::NonOverlapping(chunk));
312            }
313        }
314    }
315    debug_assert_eq!(
316        current_overlap
317            .iter()
318            .map(|(range, _): &IndexChunk| *range.start())
319            .min()
320            .unwrap_or_default(),
321        *current_range.start(),
322    );
323    debug_assert_eq!(
324        current_overlap
325            .iter()
326            .map(|(range, _): &IndexChunk| *range.end())
327            .max()
328            .unwrap_or_default(),
329        *current_range.end(),
330    );
331
332    if !current_overlap.is_empty() {
333        output.push(RawIndexChunk::Overlapping(
334            current_range.clone(),
335            current_overlap,
336        ));
337    }
338
339    output.into_iter()
340}
341
342fn merge_overlapping_chunks(overlapping_chunks: Vec<IndexChunk>) -> Result<IndexChunk> {
343    let total_capacity = overlapping_chunks
344        .iter()
345        .map(|(_, (row_ids, _))| row_ids.len())
346        .sum();
347    let mut values = Vec::with_capacity(total_capacity);
348    for (_, (row_ids, row_addrs)) in overlapping_chunks.iter() {
349        values.extend(row_ids.iter().zip(row_addrs.iter()));
350    }
351    values.sort_by_key(|(row_id, _)| *row_id);
352    // A duplicate row id here means two fragments claim the same live id: a
353    // corrupt index, not a resolvable sparse-coverage case.
354    if let Some(w) = values.windows(2).find(|w| w[0].0 == w[1].0) {
355        return Err(Error::internal(format!(
356            "row id index corrupt: stable row id {} is live in multiple fragments",
357            w[0].0
358        )));
359    }
360    let row_id_segment = U64Segment::from_iter(values.iter().map(|(row_id, _)| *row_id));
361    let address_segment = U64Segment::from_iter(values.iter().map(|(_, row_addr)| *row_addr));
362
363    let range = row_id_segment.range().unwrap();
364
365    Ok((range, (row_id_segment, address_segment)))
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use proptest::{prelude::Strategy, prop_assert_eq};
372
373    #[test]
374    fn test_new_index() {
375        let fragment_indices = vec![
376            FragmentRowIdIndex {
377                fragment_id: 10,
378                row_id_sequence: Arc::new(RowIdSequence(vec![
379                    U64Segment::Range(0..10),
380                    U64Segment::RangeWithHoles {
381                        range: 10..17,
382                        holes: vec![12, 15].into(),
383                    },
384                    U64Segment::SortedArray(vec![20, 25, 30].into()),
385                ])),
386                deletion_vector: Arc::new(DeletionVector::default()),
387            },
388            FragmentRowIdIndex {
389                fragment_id: 20,
390                row_id_sequence: Arc::new(RowIdSequence(vec![
391                    U64Segment::RangeWithBitmap {
392                        range: 17..20,
393                        bitmap: [true, false, true].as_slice().into(),
394                    },
395                    U64Segment::Array(vec![40, 50, 60].into()),
396                ])),
397                deletion_vector: Arc::new(DeletionVector::default()),
398            },
399        ];
400
401        let index = RowIdIndex::new(&fragment_indices).unwrap();
402
403        // Check various queries.
404        assert_eq!(index.get(0), Some(RowAddress::new_from_parts(10, 0)));
405        assert_eq!(index.get(15), None);
406        assert_eq!(index.get(16), Some(RowAddress::new_from_parts(10, 14)));
407        assert_eq!(index.get(17), Some(RowAddress::new_from_parts(20, 0)));
408        assert_eq!(index.get(25), Some(RowAddress::new_from_parts(10, 16)));
409        assert_eq!(index.get(40), Some(RowAddress::new_from_parts(20, 2)));
410        assert_eq!(index.get(60), Some(RowAddress::new_from_parts(20, 4)));
411        assert_eq!(index.get(61), None);
412    }
413
414    #[test]
415    fn test_new_index_overlap() {
416        let fragment_indices = vec![
417            FragmentRowIdIndex {
418                fragment_id: 23,
419                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::SortedArray(
420                    vec![3, 6, 9].into(),
421                )])),
422                deletion_vector: Arc::new(DeletionVector::default()),
423            },
424            FragmentRowIdIndex {
425                fragment_id: 42,
426                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::SortedArray(
427                    vec![2, 5, 8].into(),
428                )])),
429                deletion_vector: Arc::new(DeletionVector::default()),
430            },
431            FragmentRowIdIndex {
432                fragment_id: 10,
433                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::SortedArray(
434                    vec![1, 4, 7].into(),
435                )])),
436                deletion_vector: Arc::new(DeletionVector::default()),
437            },
438        ];
439
440        let index = RowIdIndex::new(&fragment_indices).unwrap();
441
442        // Check various queries.
443        assert_eq!(index.get(1), Some(RowAddress::new_from_parts(10, 0)));
444        assert_eq!(index.get(2), Some(RowAddress::new_from_parts(42, 0)));
445        assert_eq!(index.get(3), Some(RowAddress::new_from_parts(23, 0)));
446        assert_eq!(index.get(4), Some(RowAddress::new_from_parts(10, 1)));
447        assert_eq!(index.get(5), Some(RowAddress::new_from_parts(42, 1)));
448        assert_eq!(index.get(6), Some(RowAddress::new_from_parts(23, 1)));
449        assert_eq!(index.get(7), Some(RowAddress::new_from_parts(10, 2)));
450        assert_eq!(index.get(8), Some(RowAddress::new_from_parts(42, 2)));
451        assert_eq!(index.get(9), Some(RowAddress::new_from_parts(23, 2)));
452    }
453
454    #[test]
455    fn test_new_index_unsorted_row_ids() {
456        // Test case with unsorted row ids within fragments
457        let fragment_indices = vec![
458            FragmentRowIdIndex {
459                fragment_id: 10,
460                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Array(
461                    vec![9, 3, 6].into(), // Unsorted array
462                )])),
463                deletion_vector: Arc::new(DeletionVector::default()),
464            },
465            FragmentRowIdIndex {
466                fragment_id: 20,
467                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Array(
468                    vec![8, 2, 5].into(), // Unsorted array
469                )])),
470                deletion_vector: Arc::new(DeletionVector::default()),
471            },
472            FragmentRowIdIndex {
473                fragment_id: 30,
474                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Array(
475                    vec![7, 1, 4].into(), // Unsorted array
476                )])),
477                deletion_vector: Arc::new(DeletionVector::default()),
478            },
479        ];
480
481        let index = RowIdIndex::new(&fragment_indices).unwrap();
482
483        // Check that all row ids can be found regardless of their order in the segments
484        assert_eq!(index.get(1), Some(RowAddress::new_from_parts(30, 1)));
485        assert_eq!(index.get(2), Some(RowAddress::new_from_parts(20, 1)));
486        assert_eq!(index.get(3), Some(RowAddress::new_from_parts(10, 1)));
487        assert_eq!(index.get(4), Some(RowAddress::new_from_parts(30, 2)));
488        assert_eq!(index.get(5), Some(RowAddress::new_from_parts(20, 2)));
489        assert_eq!(index.get(6), Some(RowAddress::new_from_parts(10, 2)));
490        assert_eq!(index.get(7), Some(RowAddress::new_from_parts(30, 0)));
491        assert_eq!(index.get(8), Some(RowAddress::new_from_parts(20, 0)));
492        assert_eq!(index.get(9), Some(RowAddress::new_from_parts(10, 0)));
493
494        // Check that non-existent row ids return None
495        assert_eq!(index.get(0), None);
496        assert_eq!(index.get(10), None);
497    }
498
499    #[test]
500    fn test_new_index_partial_overlap() {
501        let fragment_indices = vec![
502            FragmentRowIdIndex {
503                fragment_id: 0,
504                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::RangeWithHoles {
505                    range: 0..100,
506                    holes: vec![50].into(),
507                }])),
508                deletion_vector: Arc::new(DeletionVector::default()),
509            },
510            FragmentRowIdIndex {
511                fragment_id: 1,
512                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(50..51)])),
513                deletion_vector: Arc::new(DeletionVector::default()),
514            },
515        ];
516
517        let index = RowIdIndex::new(&fragment_indices).unwrap();
518
519        // Check various queries.
520        assert_eq!(index.get(0), Some(RowAddress::new_from_parts(0, 0)));
521        assert_eq!(index.get(49), Some(RowAddress::new_from_parts(0, 49)));
522        assert_eq!(index.get(50), Some(RowAddress::new_from_parts(1, 0)));
523        assert_eq!(index.get(51), Some(RowAddress::new_from_parts(0, 50)));
524        assert_eq!(index.get(99), Some(RowAddress::new_from_parts(0, 98)));
525    }
526
527    #[test]
528    fn test_overlapping_chunks_sparse_with_deletions() {
529        // Interleaved (overlapping) id ranges plus a deletion that leaves a hole,
530        // so the union doesn't tile the span. Every live id must still resolve.
531        let fragment_indices = vec![
532            FragmentRowIdIndex {
533                fragment_id: 10,
534                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::SortedArray(
535                    vec![1, 3, 5, 7, 9].into(),
536                )])),
537                deletion_vector: Arc::new(DeletionVector::default()),
538            },
539            FragmentRowIdIndex {
540                fragment_id: 20,
541                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::SortedArray(
542                    vec![0, 2, 4, 6, 8].into(),
543                )])),
544                // Delete offset 2 (id 4) -> a hole in the span.
545                deletion_vector: Arc::new(DeletionVector::from_iter(vec![2])),
546            },
547        ];
548
549        let index = RowIdIndex::new(&fragment_indices).unwrap();
550
551        assert_eq!(index.get(0), Some(RowAddress::new_from_parts(20, 0)));
552        assert_eq!(index.get(1), Some(RowAddress::new_from_parts(10, 0)));
553        assert_eq!(index.get(2), Some(RowAddress::new_from_parts(20, 1)));
554        assert_eq!(index.get(3), Some(RowAddress::new_from_parts(10, 1)));
555        assert_eq!(index.get(4), None);
556        // Surviving ids keep their original offsets (the hole is not compacted).
557        assert_eq!(index.get(6), Some(RowAddress::new_from_parts(20, 3)));
558        assert_eq!(index.get(8), Some(RowAddress::new_from_parts(20, 4)));
559        assert_eq!(index.get(9), Some(RowAddress::new_from_parts(10, 4)));
560    }
561
562    #[test]
563    fn test_index_with_deletion_vector() {
564        let deletion_vector = DeletionVector::from_iter(vec![2, 3]);
565
566        let fragment_indices = vec![FragmentRowIdIndex {
567            fragment_id: 10,
568            row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(0..6)])),
569            deletion_vector: Arc::new(deletion_vector),
570        }];
571
572        let index = RowIdIndex::new(&fragment_indices).unwrap();
573
574        assert_eq!(index.get(0), Some(RowAddress::new_from_parts(10, 0)));
575        assert_eq!(index.get(1), Some(RowAddress::new_from_parts(10, 1)));
576        assert_eq!(index.get(4), Some(RowAddress::new_from_parts(10, 4)));
577        assert_eq!(index.get(5), Some(RowAddress::new_from_parts(10, 5)));
578
579        assert_eq!(index.get(2), None);
580        assert_eq!(index.get(3), None);
581    }
582
583    #[test]
584    fn test_empty_fragment_sequences() {
585        let fragment_indices = vec![
586            FragmentRowIdIndex {
587                fragment_id: 10,
588                row_id_sequence: Arc::new(RowIdSequence(vec![])),
589                deletion_vector: Arc::new(DeletionVector::default()),
590            },
591            FragmentRowIdIndex {
592                fragment_id: 20,
593                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(5..8)])),
594                deletion_vector: Arc::new(DeletionVector::default()),
595            },
596        ];
597
598        let index = RowIdIndex::new(&fragment_indices).unwrap();
599
600        assert_eq!(index.get(5), Some(RowAddress::new_from_parts(20, 0)));
601        assert_eq!(index.get(7), Some(RowAddress::new_from_parts(20, 2)));
602        assert_eq!(index.get(4), None);
603    }
604
605    #[test]
606    fn test_completely_empty_index() {
607        let fragment_indices = vec![];
608        let index = RowIdIndex::new(&fragment_indices).unwrap();
609
610        assert_eq!(index.get(0), None);
611        assert_eq!(index.get(100), None);
612    }
613
614    #[test]
615    fn test_non_overlapping_ranges() {
616        let fragment_indices = vec![
617            FragmentRowIdIndex {
618                fragment_id: 10,
619                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(0..5)])),
620                deletion_vector: Arc::new(DeletionVector::default()),
621            },
622            FragmentRowIdIndex {
623                fragment_id: 20,
624                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(5..10)])),
625                deletion_vector: Arc::new(DeletionVector::default()),
626            },
627            FragmentRowIdIndex {
628                fragment_id: 30,
629                row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(10..15)])),
630                deletion_vector: Arc::new(DeletionVector::default()),
631            },
632        ];
633
634        let index = RowIdIndex::new(&fragment_indices).unwrap();
635
636        assert_eq!(index.get(0), Some(RowAddress::new_from_parts(10, 0)));
637        assert_eq!(index.get(4), Some(RowAddress::new_from_parts(10, 4)));
638        assert_eq!(index.get(5), Some(RowAddress::new_from_parts(20, 0)));
639        assert_eq!(index.get(9), Some(RowAddress::new_from_parts(20, 4)));
640        assert_eq!(index.get(10), Some(RowAddress::new_from_parts(30, 0)));
641        assert_eq!(index.get(14), Some(RowAddress::new_from_parts(30, 4)));
642    }
643
644    fn arbitrary_row_ids(
645        num_fragments_range: std::ops::Range<usize>,
646        frag_size_range: std::ops::Range<usize>,
647    ) -> impl Strategy<Value = Vec<(u32, Arc<RowIdSequence>)>> {
648        let fragment_sizes = proptest::collection::vec(frag_size_range, num_fragments_range);
649        fragment_sizes.prop_flat_map(|fragment_sizes| {
650            let num_rows = fragment_sizes.iter().sum::<usize>() as u64;
651            let row_ids = 0..num_rows;
652            let row_ids = row_ids.collect::<Vec<_>>();
653            let row_ids_shuffled = proptest::strategy::Just(row_ids).prop_shuffle();
654            row_ids_shuffled.prop_map(move |row_ids| {
655                let mut sequences = Vec::with_capacity(fragment_sizes.len());
656                let mut i = 0;
657                for size in &fragment_sizes {
658                    let end = i + size;
659                    let sequence =
660                        RowIdSequence(vec![U64Segment::from_slice(row_ids[i..end].into())]);
661                    sequences.push((i as u32, Arc::new(sequence)));
662                    i = end;
663                }
664                sequences
665            })
666        })
667    }
668
669    #[test]
670    fn test_large_range_segments_no_deletions() {
671        // Simulates a real-world scenario: many fragments with large Range segments
672        // and no deletions. Before optimization, this would iterate over all rows
673        // (O(total_rows)). After optimization, it's O(num_fragments).
674        let rows_per_fragment = 250_000u64;
675        let num_fragments = 100u32;
676        let mut offset = 0u64;
677
678        let fragment_indices: Vec<FragmentRowIdIndex> = (0..num_fragments)
679            .map(|frag_id| {
680                let start = offset;
681                offset += rows_per_fragment;
682                FragmentRowIdIndex {
683                    fragment_id: frag_id,
684                    row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(
685                        start..start + rows_per_fragment,
686                    )])),
687                    deletion_vector: Arc::new(DeletionVector::default()),
688                }
689            })
690            .collect();
691
692        let start = std::time::Instant::now();
693        let index = RowIdIndex::new(&fragment_indices).unwrap();
694        let elapsed = start.elapsed();
695
696        // Verify correctness at boundaries
697        assert_eq!(index.get(0), Some(RowAddress::new_from_parts(0, 0)));
698        assert_eq!(
699            index.get(rows_per_fragment - 1),
700            Some(RowAddress::new_from_parts(0, rows_per_fragment as u32 - 1))
701        );
702        assert_eq!(
703            index.get(rows_per_fragment),
704            Some(RowAddress::new_from_parts(1, 0))
705        );
706        let last_row = num_fragments as u64 * rows_per_fragment - 1;
707        assert_eq!(
708            index.get(last_row),
709            Some(RowAddress::new_from_parts(
710                num_fragments - 1,
711                rows_per_fragment as u32 - 1
712            ))
713        );
714        assert_eq!(index.get(last_row + 1), None);
715
716        // With the optimization, building an index for 25M rows across 100 fragments
717        // should complete in well under 1 second (typically < 1ms).
718        assert!(
719            elapsed.as_secs() < 1,
720            "Index build took {:?} for {} fragments x {} rows = {} total rows. \
721             This suggests the O(rows) -> O(fragments) optimization is not working.",
722            elapsed,
723            num_fragments,
724            rows_per_fragment,
725            num_fragments as u64 * rows_per_fragment,
726        );
727    }
728
729    #[test]
730    fn test_large_range_segments_with_deletions() {
731        let rows_per_fragment = 1_000u64;
732        let num_fragments = 10u32;
733        let mut offset = 0u64;
734
735        let fragment_indices: Vec<FragmentRowIdIndex> = (0..num_fragments)
736            .map(|frag_id| {
737                let start = offset;
738                offset += rows_per_fragment;
739
740                // Delete every 3rd row (offsets 0, 3, 6, ...) within each fragment.
741                let mut deleted = roaring::RoaringBitmap::new();
742                for i in (0..rows_per_fragment as u32).step_by(3) {
743                    deleted.insert(i);
744                }
745
746                FragmentRowIdIndex {
747                    fragment_id: frag_id,
748                    row_id_sequence: Arc::new(RowIdSequence(vec![U64Segment::Range(
749                        start..start + rows_per_fragment,
750                    )])),
751                    deletion_vector: Arc::new(DeletionVector::Bitmap(deleted)),
752                }
753            })
754            .collect();
755
756        let index = RowIdIndex::new(&fragment_indices).unwrap();
757
758        // Deleted rows (offset 0, 3, 6, ...) should not be found.
759        // Row ID 0 has offset 0 in fragment 0 -> deleted.
760        assert_eq!(index.get(0), None);
761        // Row ID 3 has offset 3 in fragment 0 -> deleted.
762        assert_eq!(index.get(3), None);
763
764        // Non-deleted rows should resolve correctly.
765        // Row ID 1 has offset 1 in fragment 0 -> address (frag=0, row=1).
766        assert_eq!(index.get(1), Some(RowAddress::new_from_parts(0, 1)));
767        // Row ID 2 has offset 2 in fragment 0 -> address (frag=0, row=2).
768        assert_eq!(index.get(2), Some(RowAddress::new_from_parts(0, 2)));
769        // Row ID 4 has offset 4 in fragment 0 -> address (frag=0, row=4).
770        assert_eq!(index.get(4), Some(RowAddress::new_from_parts(0, 4)));
771
772        // Check second fragment: row IDs start at 1000.
773        // Row ID 1000 has offset 0 in fragment 1 -> deleted.
774        assert_eq!(index.get(rows_per_fragment), None);
775        // Row ID 1001 has offset 1 in fragment 1 -> address (frag=1, row=1).
776        assert_eq!(
777            index.get(rows_per_fragment + 1),
778            Some(RowAddress::new_from_parts(1, 1))
779        );
780
781        // Last fragment, last non-deleted row.
782        // Row ID 9999 has offset 999 in fragment 9 -> 999 % 3 == 0 -> deleted.
783        let last_row = num_fragments as u64 * rows_per_fragment - 1;
784        assert_eq!(index.get(last_row), None);
785        // Row ID 9998 has offset 998 -> 998 % 3 == 2 -> not deleted.
786        assert_eq!(
787            index.get(last_row - 1),
788            Some(RowAddress::new_from_parts(num_fragments - 1, 998))
789        );
790
791        // Out of range.
792        assert_eq!(index.get(last_row + 1), None);
793    }
794
795    proptest::proptest! {
796        #[test]
797        fn test_new_index_robustness(row_ids in arbitrary_row_ids(0..5, 0..32)) {
798            let fragment_indices: Vec<FragmentRowIdIndex> = row_ids
799                .iter()
800                .map(|(frag_id, sequence)| FragmentRowIdIndex {
801                    fragment_id: *frag_id,
802                    row_id_sequence: sequence.clone(),
803                    deletion_vector: Arc::new(DeletionVector::default()),
804                })
805                .collect();
806
807            let index = RowIdIndex::new(&fragment_indices).unwrap();
808            for (frag_id, sequence) in row_ids.iter() {
809                for (local_offset, row_id) in sequence.iter().enumerate() {
810                    prop_assert_eq!(
811                        index.get(row_id),
812                        Some(RowAddress::new_from_parts(*frag_id, local_offset as u32)),
813                        "Row id {} in sequence {:?} not found in index {:?}",
814                        row_id,
815                        sequence,
816                        index
817                    );
818                }
819            }
820        }
821    }
822}