Skip to main content

lance_core/utils/
row_addr_remap.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Compact row-address remapping for compaction.
5//!
6//! Compaction rewrites rows into new fragments, so indices that store physical
7//! row addresses need an old-address to new-address mapping without building an
8//! O(total rows) `HashMap<u64, Option<u64>>`.
9//!
10//! Layout:
11//!
12//! * Old rows: `old_fragment_id -> (old_offsets, old_rows_before)`
13//!     * `old_offsets`: rewritten old row offsets in this old fragment.
14//!     * `old_rows_before`: rewritten row count before this old fragment.
15//! * New rows: ordered new-fragment ranges
16//!   `(fragment_id, new_rows_before, physical_rows)`
17//!     * `new_rows_before`: rewritten row count before this new fragment.
18//!
19//! Lookup:
20//!
21//! * An address whose fragment was not rewritten returns `None`.
22//! * For an address whose fragment was rewritten:
23//!     * Read `(old_offsets, old_rows_before)` from the old-row layout.
24//!     * If `offset` is not in `old_offsets`, return `Some(None)` because the
25//!       row was deleted.
26//!     * Otherwise, `old_offsets.rank(offset) - 1` is this row's 0-based
27//!       position among rewritten old rows in this old fragment. Add
28//!       `old_rows_before` to get `k`, the row's 0-based position among all
29//!       rewritten old rows.
30//!     * In the new-row layout, find the range
31//!       `(fragment_id, new_rows_before, physical_rows)` where
32//!       `new_rows_before <= k < new_rows_before + physical_rows`.
33//!     * The new address is `(fragment_id, k - new_rows_before)`.
34//!
35//! Ordering:
36//!
37//! Compact remap does not store each old-to-new row mapping. It computes `k`
38//! from the old-row layout, then maps it to the k-th row written to the new
39//! fragments. This requires the reader-to-writer pipeline to preserve row order.
40//!
41//! * `old_frag_ids` must match the order old fragments are read. Within each
42//!   old fragment, rewritten rows are interpreted by ascending old row offset.
43//! * `new_frags` must match the order new rows are written.
44//! * Current compaction satisfies this because it scans selected fragments in
45//!   order and writes the resulting stream without reordering rows.
46
47use crate::utils::address::RowAddress;
48use crate::{Error, Result};
49use roaring::{RoaringBitmap, RoaringTreemap};
50use std::collections::HashMap;
51
52/// A queryable row-address remapping with the exact semantics of
53/// `HashMap<u64, Option<u64>>::get(&addr).copied()`:
54///
55/// * `None` — the address is not affected by this remap (keep it unchanged)
56/// * `Some(None)` — the row was deleted
57/// * `Some(Some(addr))` — the row moved to `addr`
58#[derive(Clone)]
59pub enum RowAddrRemap {
60    /// Compact, `O(#fragments)` remap built from per-group rewritten-row
61    /// bitmaps and new-fragment layouts.
62    Compact(CompactRowAddrRemap),
63    /// Full materialized old-to-new address map. Uses `O(#rows)` memory.
64    Direct(HashMap<u64, Option<u64>>),
65}
66
67impl RowAddrRemap {
68    pub fn compact(groups: impl IntoIterator<Item = GroupInput>) -> Result<Self> {
69        Ok(Self::Compact(CompactRowAddrRemap::new(groups)?))
70    }
71
72    /// Build a remap from a fully materialized old-to-new address map.
73    pub fn direct(map: HashMap<u64, Option<u64>>) -> Self {
74        Self::Direct(map)
75    }
76
77    /// An empty remap that leaves every address unchanged.
78    pub fn empty() -> Self {
79        Self::Direct(HashMap::new())
80    }
81
82    /// Look up `addr`. See [`RowAddrRemap`] for the tri-state return semantics.
83    #[inline]
84    pub fn get(&self, addr: u64) -> Option<Option<u64>> {
85        match self {
86            Self::Compact(c) => c.get(addr),
87            Self::Direct(m) => m.get(&addr).copied(),
88        }
89    }
90
91    pub fn is_empty(&self) -> bool {
92        match self {
93            Self::Compact(c) => c.is_empty(),
94            Self::Direct(m) => m.is_empty(),
95        }
96    }
97
98    pub fn affected_fragments(&self) -> RoaringBitmap {
99        match self {
100            Self::Compact(c) => RoaringBitmap::from_iter(c.frag_to_group.keys().copied()),
101            Self::Direct(m) => RoaringBitmap::from_iter(m.keys().map(|addr| (addr >> 32) as u32)),
102        }
103    }
104
105    pub fn fully_deleted_fragments(&self) -> Option<RoaringBitmap> {
106        match self {
107            Self::Compact(c) => c.fully_deleted_fragments(),
108            Self::Direct(m) => {
109                if m.values().all(|v| v.is_none()) {
110                    Some(RoaringBitmap::from_iter(
111                        m.keys().map(|addr| (addr >> 32) as u32),
112                    ))
113                } else {
114                    None
115                }
116            }
117        }
118    }
119}
120
121/// Input describing one rewrite group: the old row addresses that were
122/// rewritten plus the fragment layout before/after the rewrite.
123pub struct GroupInput {
124    /// Old row addresses that were read and re-written into the new fragments.
125    pub rewritten_old_row_addrs: RoaringTreemap,
126    /// Old fragment ids covered by this group.
127    pub old_frag_ids: Vec<u32>,
128    /// New fragments produced by this group, as `(fragment_id, physical_rows)`,
129    pub new_frags: Vec<(u32, u32)>,
130}
131
132#[derive(Clone)]
133struct GroupRemap {
134    /// Old fragment id -> (rewritten old row offsets in that fragment,
135    /// rewritten row count before this fragment in the group).
136    frags: HashMap<u32, (RoaringBitmap, u64)>,
137    /// New fragment ranges as `(fragment_id, rewritten_rows_before, physical_rows)`,
138    /// used to map a rewritten row's group-local index to its new address via binary search.
139    new_frag_row_ranges: Vec<(u32, u64, u32)>,
140}
141
142impl GroupRemap {
143    fn new(input: GroupInput) -> Result<Self> {
144        // `compute_new_addr` maps a rewritten row's group-local index to a new
145        // address by accumulating `physical_rows` in `new_frags` order, so that
146        // order must be the order rows were written. New fragment ids are
147        // reserved monotonically in write order (see `reserve_fragment_ids` in
148        // compaction), so ascending id is a proxy for write order; reject any
149        // input that violates it before it can silently misplace addresses.
150        let mut new_frag_row_ranges = Vec::with_capacity(input.new_frags.len());
151        let mut rewritten_rows_before = 0u64;
152        let mut prev_frag_id: Option<u32> = None;
153        for (frag_id, physical_rows) in input.new_frags {
154            if physical_rows == 0 {
155                continue;
156            }
157            if let Some(prev) = prev_frag_id
158                && frag_id <= prev
159            {
160                return Err(Error::invalid_input(format!(
161                    "compaction new fragments must be in ascending id (write) order, but fragment {frag_id} follows {prev}",
162                )));
163            }
164            prev_frag_id = Some(frag_id);
165            new_frag_row_ranges.push((frag_id, rewritten_rows_before, physical_rows));
166            rewritten_rows_before += physical_rows as u64;
167        }
168        let total_new_rows = rewritten_rows_before;
169
170        let mut per_frag: HashMap<u32, RoaringBitmap> = input
171            .rewritten_old_row_addrs
172            .bitmaps()
173            .map(|(frag_id, bitmap)| (frag_id, bitmap.clone()))
174            .collect();
175        let mut frags = HashMap::new();
176        let mut rewritten_rows_before = 0u64;
177        for &frag_id in &input.old_frag_ids {
178            // A fragment with no rewritten rows (fully deleted) contributes
179            // nothing to the rewritten row sequence.
180            if let Some(bitmap) = per_frag.remove(&frag_id) {
181                let num_rewritten_rows = bitmap.len();
182                frags.insert(frag_id, (bitmap, rewritten_rows_before));
183                rewritten_rows_before += num_rewritten_rows;
184            }
185        }
186        // Rewritten old row addresses must reference only fragments listed in `old_frag_ids`.
187        if !per_frag.is_empty() {
188            return Err(Error::invalid_input(format!(
189                "compaction rewritten old row addresses reference fragments {:?} not in the rewrite group's old fragments {:?}",
190                per_frag.keys().collect::<Vec<_>>(),
191                input.old_frag_ids,
192            )));
193        }
194
195        // Rewritten old rows are mapped positionally onto the new rows, so the
196        // two counts must match exactly
197        let total_rewritten_old_rows = input.rewritten_old_row_addrs.len();
198        if total_new_rows != total_rewritten_old_rows {
199            return Err(Error::invalid_input(format!(
200                "compaction rewrote {total_rewritten_old_rows} old rows from fragments {:?} but the new fragments hold {total_new_rows} rows",
201                input.old_frag_ids,
202            )));
203        }
204
205        Ok(Self {
206            frags,
207            new_frag_row_ranges,
208        })
209    }
210
211    fn compute_new_addr(&self, rewritten_row_index: u64) -> u64 {
212        let idx =
213            match self
214                .new_frag_row_ranges
215                .binary_search_by(|(_, rewritten_rows_before, _)| {
216                    rewritten_rows_before.cmp(&rewritten_row_index)
217                }) {
218                Ok(i) => i,
219                Err(i) => i - 1,
220            };
221        let (frag_id, rewritten_rows_before, _rows) = self.new_frag_row_ranges[idx];
222        let offset = (rewritten_row_index - rewritten_rows_before) as u32;
223        u64::from(RowAddress::new_from_parts(frag_id, offset))
224    }
225
226    /// Compute the new address for an old row in this group.
227    /// Returns `None` if the old row was not rewritten.
228    #[inline]
229    fn get(&self, frag: u32, offset: u32) -> Option<u64> {
230        match self.frags.get(&frag) {
231            Some((bitmap, rewritten_rows_before)) if bitmap.contains(offset) => {
232                let rewritten_row_index = rewritten_rows_before + bitmap.rank(offset) - 1;
233                Some(self.compute_new_addr(rewritten_row_index))
234            }
235            _ => None,
236        }
237    }
238}
239
240/// Compact remap backed by per-group rewritten row bitmaps + new-fragment layouts.
241#[derive(Clone)]
242pub struct CompactRowAddrRemap {
243    groups: Vec<GroupRemap>,
244    /// Old fragment id -> index into `groups`. Size is O(#fragments), not rows.
245    frag_to_group: HashMap<u32, usize>,
246}
247
248impl CompactRowAddrRemap {
249    fn new(groups: impl IntoIterator<Item = GroupInput>) -> Result<Self> {
250        let mut frag_to_group = HashMap::new();
251        let mut group_remaps = Vec::new();
252        for input in groups {
253            let gi = group_remaps.len();
254            for &frag_id in &input.old_frag_ids {
255                frag_to_group.insert(frag_id, gi);
256            }
257            group_remaps.push(GroupRemap::new(input)?);
258        }
259        Ok(Self {
260            groups: group_remaps,
261            frag_to_group,
262        })
263    }
264
265    #[inline]
266    pub fn get(&self, addr: u64) -> Option<Option<u64>> {
267        let frag = (addr >> 32) as u32;
268        // Not in any rewrite group -> unaffected by this remap.
269        let gi = *self.frag_to_group.get(&frag)?;
270        Some(self.groups[gi].get(frag, addr as u32))
271    }
272
273    pub fn is_empty(&self) -> bool {
274        self.groups.is_empty()
275    }
276
277    fn fully_deleted_fragments(&self) -> Option<RoaringBitmap> {
278        // A group with any rewritten row moved at least one row.
279        if self.groups.iter().any(|g| !g.frags.is_empty()) {
280            return None;
281        }
282        Some(RoaringBitmap::from_iter(self.frag_to_group.keys().copied()))
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    fn addr(frag: u32, offset: u32) -> u64 {
291        u64::from(RowAddress::new_from_parts(frag, offset))
292    }
293
294    #[test]
295    fn test_compact_lookup() {
296        // Group A: out-of-order old frags [4, 3], split new frags (11 empty),
297        // some deletions. frag 4 (5 rows) keeps 0,2,4; frag 3 keeps 0,1, so the
298        // rewritten rows (4,0)(4,2)(4,4)(3,0)(3,1) go to new frags 10(2), 12(3).
299        // Group B is a fully-deleted fragment.
300        let group_a = GroupInput {
301            rewritten_old_row_addrs: RoaringTreemap::from_iter([
302                addr(4, 0),
303                addr(4, 2),
304                addr(4, 4),
305                addr(3, 0),
306                addr(3, 1),
307            ]),
308            old_frag_ids: vec![4, 3],
309            new_frags: vec![(10, 2), (11, 0), (12, 3)],
310        };
311        let group_b = GroupInput {
312            rewritten_old_row_addrs: RoaringTreemap::new(),
313            old_frag_ids: vec![7],
314            new_frags: vec![],
315        };
316        let remap = RowAddrRemap::compact([group_a, group_b]).unwrap();
317
318        // Moves, in rewrite order; frag 4 comes first despite the larger id.
319        assert_eq!(remap.get(addr(4, 0)), Some(Some(addr(10, 0))));
320        assert_eq!(remap.get(addr(4, 2)), Some(Some(addr(10, 1))));
321        // Rank 2 skips the zero-row new fragment 11 and lands in fragment 12.
322        assert_eq!(remap.get(addr(4, 4)), Some(Some(addr(12, 0))));
323        assert_eq!(remap.get(addr(3, 0)), Some(Some(addr(12, 1))));
324        assert_eq!(remap.get(addr(3, 1)), Some(Some(addr(12, 2))));
325        // Deleted offsets inside a rewritten fragment.
326        assert_eq!(remap.get(addr(4, 1)), Some(None));
327        assert_eq!(remap.get(addr(4, 3)), Some(None));
328        // Covered but fully-deleted fragment -> Some(None), not None.
329        assert_eq!(remap.get(addr(7, 0)), Some(None));
330        // Fragment in no group -> unaffected.
331        assert_eq!(remap.get(addr(9, 0)), None);
332        assert!(!remap.is_empty());
333    }
334
335    #[test]
336    fn test_fragment_sets() {
337        // No rewritten rows at all: every covered fragment is fully deleted.
338        let dead = RowAddrRemap::compact([GroupInput {
339            rewritten_old_row_addrs: RoaringTreemap::new(),
340            old_frag_ids: vec![3, 7],
341            new_frags: vec![],
342        }])
343        .unwrap();
344        assert_eq!(
345            dead.fully_deleted_fragments(),
346            Some(RoaringBitmap::from_iter([3u32, 7u32]))
347        );
348        assert_eq!(
349            dead.affected_fragments(),
350            RoaringBitmap::from_iter([3u32, 7u32])
351        );
352
353        // At least one rewritten row -> not fully deleted, but both covered
354        // fragments (including the fully-deleted frag 1) are still affected.
355        let alive = RowAddrRemap::compact([GroupInput {
356            rewritten_old_row_addrs: RoaringTreemap::from_iter([addr(0, 0)]),
357            old_frag_ids: vec![0, 1],
358            new_frags: vec![(10, 1)],
359        }])
360        .unwrap();
361        assert!(alive.fully_deleted_fragments().is_none());
362        assert_eq!(
363            alive.affected_fragments(),
364            RoaringBitmap::from_iter([0u32, 1u32])
365        );
366    }
367
368    #[test]
369    fn test_compact_rejects_rewritten_addrs_outside_old_frags() {
370        // Rewritten addresses reference frag 5, not in old_frag_ids. The count
371        // still matches (2 == 2), so only the per-fragment split catches it.
372        let input = GroupInput {
373            rewritten_old_row_addrs: RoaringTreemap::from_iter([addr(0, 0), addr(5, 0)]),
374            old_frag_ids: vec![0],
375            new_frags: vec![(10, 2)],
376        };
377        assert!(RowAddrRemap::compact([input]).is_err());
378    }
379
380    #[test]
381    fn test_compact_rejects_new_frags_out_of_write_order() {
382        // New fragments out of ascending id (write) order would make
383        // `compute_new_addr` accumulate rows in the wrong order, silently
384        // misplacing addresses. A zero-row fragment between them is ignored.
385        let input = GroupInput {
386            rewritten_old_row_addrs: RoaringTreemap::from_iter([addr(0, 0), addr(0, 1)]),
387            old_frag_ids: vec![0],
388            new_frags: vec![(12, 1), (11, 1)],
389        };
390        assert!(RowAddrRemap::compact([input]).is_err());
391    }
392
393    #[test]
394    fn test_direct_and_empty() {
395        // Direct covers arbitrary maps the compact form can't express.
396        let mut map = HashMap::new();
397        map.insert(addr(2, 0), Some(addr(9, 9)));
398        map.insert(addr(5, 1), None);
399        let remap = RowAddrRemap::direct(map);
400        assert_eq!(remap.get(addr(2, 0)), Some(Some(addr(9, 9))));
401        assert_eq!(remap.get(addr(5, 1)), Some(None));
402        assert_eq!(remap.get(addr(2, 1)), None);
403        // affected_fragments over an explicit map: the fragment of every key.
404        assert_eq!(
405            remap.affected_fragments(),
406            RoaringBitmap::from_iter([2u32, 5u32])
407        );
408
409        let empty = RowAddrRemap::empty();
410        assert!(empty.is_empty());
411        assert_eq!(empty.get(addr(0, 0)), None);
412    }
413}