lance_table/format/overlay.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Data overlay files.
5//!
6//! An overlay file supplies new values for a subset of `(physical offset, field)`
7//! cells within a fragment, without rewriting the fragment's base data files. See
8//! the Data Overlay Files specification for the full rules; the invariants this
9//! module relies on are:
10//!
11//! - **Physical-offset coverage.** Coverage bitmaps index *physical* row offsets
12//! (positions in the base data files, counting deleted rows), so they are stable
13//! across deletions, like deletion vectors.
14//! - **Rank-based values.** The overlay's `data_file` stores one value column per
15//! field, with no row-offset key column. Within a value column, a covered
16//! offset's value sits at its **rank** — the 0-based count of set bits below it
17//! in that field's coverage bitmap.
18//! - **Dense vs. sparse coverage.** A dense overlay shares one bitmap across every
19//! field ([`OverlayCoverage::Shared`]); a sparse overlay carries one bitmap per
20//! field ([`OverlayCoverage::PerField`]).
21//! - **Parse once.** Bitmaps are parsed from their 32-bit Roaring encoding a single
22//! time when the fragment loads and held behind an `Arc`, so cloning a fragment
23//! is cheap.
24//! - **Newest-last ordering.** A fragment's overlays are stored newest-last and
25//! stable-sorted by `committed_version` on load (see [`sort_overlays_newest_last`]),
26//! with list position breaking ties for equal versions. When two overlays cover
27//! the same `(offset, field)`, the higher `committed_version` wins.
28//! - **Field tombstones.** When new base values are written for a field (a
29//! DataReplacement, or an in-place column rewrite), any overlay value for that
30//! field is stale and must stop shadowing the fresh base. The field is marked
31//! obsolete in the overlay's `data_file.fields` with [`TOMBSTONE_FIELD_ID`]
32//! (the same sentinel used for obsolete base columns) rather than physically
33//! removed, so the overlay's other fields — and its coverage positions — stay
34//! intact (see [`tombstone_overlay_fields`]).
35
36use std::sync::Arc;
37
38use lance_core::Error;
39use lance_core::deepsize::DeepSizeOf;
40use lance_core::error::Result;
41use roaring::RoaringBitmap;
42use serde::{Deserialize, Serialize};
43
44use object_store::path::Path;
45
46use super::DataFile;
47use crate::format::pb;
48
49/// Field-id sentinel marking a tombstoned (obsolete) field within an overlay's
50/// `data_file.fields`. Matches the tombstone convention for obsolete columns in
51/// base data files; a tombstoned field's values are ignored on read.
52pub const TOMBSTONE_FIELD_ID: i32 = -2;
53
54/// Which `(physical offset, field)` cells a [`DataOverlayFile`] provides values
55/// for.
56///
57/// Bitmaps are parsed from their 32-bit Roaring encoding once when the fragment
58/// is loaded and held behind an `Arc` so cloning a fragment is cheap; use
59/// [`DataOverlayFile::coverage_for_field`] to obtain the one that applies to a
60/// given field.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(into = "OverlayCoverageBytes", try_from = "OverlayCoverageBytes")]
63pub enum OverlayCoverage {
64 /// A single bitmap that applies to every field in the overlay's
65 /// `data_file.fields` (a dense / rectangular overlay): every covered offset
66 /// has a value for every field.
67 Shared(Arc<RoaringBitmap>),
68 /// One bitmap per field, in the same order as the overlay's
69 /// `data_file.fields` (a sparse overlay): different fields may cover
70 /// different offset sets.
71 PerField(Vec<Arc<RoaringBitmap>>),
72}
73
74/// Serialized form of [`OverlayCoverage`] — each bitmap as its 32-bit Roaring
75/// byte encoding. The in-memory form parses these once at load.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77enum OverlayCoverageBytes {
78 Shared(Vec<u8>),
79 PerField(Vec<Vec<u8>>),
80}
81
82// The bytes come from a persisted overlay (the protobuf manifest or a
83// serialized fragment), so a decode failure is on-disk corruption, not caller
84// input. `path` locates the overlay's data file when known (empty on the serde
85// path, which deserializes coverage in isolation).
86fn deserialize_roaring(bytes: &[u8], path: &Path) -> Result<RoaringBitmap> {
87 RoaringBitmap::deserialize_from(bytes).map_err(|e| {
88 Error::corrupt_file(
89 path.clone(),
90 format!("failed to deserialize overlay coverage bitmap: {e}"),
91 )
92 })
93}
94
95fn serialize_roaring(bitmap: &RoaringBitmap) -> Vec<u8> {
96 let mut bytes = Vec::with_capacity(bitmap.serialized_size());
97 // Writing to a Vec is infallible.
98 bitmap.serialize_into(&mut bytes).unwrap();
99 bytes
100}
101
102impl From<OverlayCoverage> for OverlayCoverageBytes {
103 fn from(coverage: OverlayCoverage) -> Self {
104 match coverage {
105 OverlayCoverage::Shared(bitmap) => Self::Shared(serialize_roaring(&bitmap)),
106 OverlayCoverage::PerField(bitmaps) => {
107 Self::PerField(bitmaps.iter().map(|b| serialize_roaring(b)).collect())
108 }
109 }
110 }
111}
112
113impl TryFrom<OverlayCoverageBytes> for OverlayCoverage {
114 type Error = Error;
115
116 fn try_from(bytes: OverlayCoverageBytes) -> Result<Self> {
117 // Serde deserializes the coverage in isolation, so the owning data
118 // file's path is not available here.
119 let path = Path::default();
120 Ok(match bytes {
121 OverlayCoverageBytes::Shared(b) => {
122 Self::Shared(Arc::new(deserialize_roaring(&b, &path)?))
123 }
124 OverlayCoverageBytes::PerField(bs) => Self::PerField(
125 bs.iter()
126 .map(|b| deserialize_roaring(b, &path).map(Arc::new))
127 .collect::<Result<_>>()?,
128 ),
129 })
130 }
131}
132
133impl DeepSizeOf for OverlayCoverage {
134 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
135 // The same `Arc<RoaringBitmap>` is shared across every clone of a
136 // fragment, so mark each Arc's pointer and count its heap only the first
137 // time it is seen — otherwise walking many fragments double-counts the
138 // shared bitmaps. RoaringBitmap does not expose its allocation size; its
139 // serialized size is a cheap, close proxy for the heap it holds.
140 let bitmap_heap = |bitmap: &Arc<RoaringBitmap>,
141 context: &mut lance_core::deepsize::Context| {
142 if context.mark_seen(Arc::as_ptr(bitmap) as usize) {
143 std::mem::size_of::<RoaringBitmap>() + bitmap.serialized_size()
144 } else {
145 0
146 }
147 };
148 match self {
149 Self::Shared(bitmap) => bitmap_heap(bitmap, context),
150 Self::PerField(bitmaps) => {
151 bitmaps.capacity() * std::mem::size_of::<Arc<RoaringBitmap>>()
152 + bitmaps
153 .iter()
154 .map(|b| bitmap_heap(b, context))
155 .sum::<usize>()
156 }
157 }
158 }
159}
160
161impl OverlayCoverage {
162 /// Build a dense coverage from a single bitmap shared across every field.
163 pub fn dense(bitmap: RoaringBitmap) -> Self {
164 Self::Shared(Arc::new(bitmap))
165 }
166
167 /// Build a sparse coverage from one bitmap per field.
168 pub fn sparse(bitmaps: Vec<RoaringBitmap>) -> Self {
169 Self::PerField(bitmaps.into_iter().map(Arc::new).collect())
170 }
171}
172
173/// An overlay file supplies new values for a subset of `(physical offset, field)`
174/// cells within a fragment, without rewriting the fragment's base data files. See
175/// the [module documentation](self) for the coverage, rank, and versioning rules.
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
177pub struct DataOverlayFile {
178 /// The data file storing the overlay's new cell values.
179 pub data_file: DataFile,
180 /// Which cells this overlay provides values for.
181 pub coverage: OverlayCoverage,
182 /// The dataset version at which this overlay became effective (the version of
183 /// the commit that introduced it, stamped at commit time and re-stamped on
184 /// retry). Higher wins when two overlays cover the same `(offset, field)`.
185 pub committed_version: u64,
186}
187
188impl DataOverlayFile {
189 /// The parsed coverage bitmap that applies to the field stored at
190 /// `field_pos` within `data_file.fields`.
191 ///
192 /// For a dense overlay the same shared bitmap is returned for every field;
193 /// for a sparse overlay the per-field bitmap at `field_pos` is returned. The
194 /// bitmap is already parsed, so this is a cheap `Arc` clone.
195 pub fn coverage_for_field(&self, field_pos: usize) -> Result<Arc<RoaringBitmap>> {
196 match &self.coverage {
197 OverlayCoverage::Shared(bitmap) => Ok(bitmap.clone()),
198 OverlayCoverage::PerField(bitmaps) => {
199 bitmaps.get(field_pos).cloned().ok_or_else(|| {
200 Error::invalid_input(format!(
201 "overlay per-field coverage has {} bitmaps but field position {} was requested",
202 bitmaps.len(),
203 field_pos
204 ))
205 })
206 }
207 }
208 }
209}
210
211/// Stable-sort a fragment's overlays newest-last by `committed_version`. The
212/// stable sort preserves list position as the tiebreak for equal versions, so
213/// resolution can rely on the ordering without re-checking. See the [module
214/// documentation](self) for the ordering invariant.
215pub fn sort_overlays_newest_last(overlays: &mut [DataOverlayFile]) {
216 overlays.sort_by_key(|overlay| overlay.committed_version);
217}
218
219/// Verify a fragment's overlays are stored newest-last (non-decreasing
220/// `committed_version`), the ordering invariant readers rely on for
221/// resolution. Returns an error identifying the first out-of-order pair.
222///
223/// [`sort_overlays_newest_last`] normalizes on load; this is the write-side
224/// guard that rejects any commit path that assembled overlays out of order. See
225/// the [module documentation](self) for the ordering invariant.
226pub fn verify_overlays_newest_last(overlays: &[DataOverlayFile]) -> Result<()> {
227 for pair in overlays.windows(2) {
228 if pair[0].committed_version > pair[1].committed_version {
229 return Err(Error::invalid_input(format!(
230 "overlay files must be stored newest-last, but committed_version {} precedes {}",
231 pair[0].committed_version, pair[1].committed_version
232 )));
233 }
234 }
235 Ok(())
236}
237
238/// Tombstone `fields` across a fragment's `overlays`, dropping any overlay left
239/// with no live fields.
240///
241/// Called when new base values are written for those fields (a DataReplacement,
242/// or an in-place column rewrite): the stale overlay values must stop shadowing
243/// the fresh base. Each matching field id is replaced with [`TOMBSTONE_FIELD_ID`]
244/// in place, preserving the overlay's remaining fields and its coverage positions
245/// (a per-field coverage bitmap stays aligned with `data_file.fields`). An overlay
246/// whose fields are now all tombstoned is removed entirely. See the [module
247/// documentation](self) for the tombstone invariant.
248pub fn tombstone_overlay_fields(overlays: &mut Vec<DataOverlayFile>, fields: &[u32]) {
249 for overlay in overlays.iter_mut() {
250 let tombstoned: Vec<i32> = overlay
251 .data_file
252 .fields
253 .iter()
254 .map(|&field| {
255 if field >= 0 && fields.contains(&(field as u32)) {
256 TOMBSTONE_FIELD_ID
257 } else {
258 field
259 }
260 })
261 .collect();
262 overlay.data_file.fields = tombstoned.into();
263 }
264 overlays.retain(|overlay| {
265 overlay
266 .data_file
267 .fields
268 .iter()
269 .any(|&field| field != TOMBSTONE_FIELD_ID)
270 });
271}
272
273impl From<&DataOverlayFile> for pb::DataOverlayFile {
274 fn from(overlay: &DataOverlayFile) -> Self {
275 let coverage = match &overlay.coverage {
276 OverlayCoverage::Shared(bitmap) => {
277 pb::data_overlay_file::Coverage::SharedOffsetBitmap(serialize_roaring(bitmap))
278 }
279 OverlayCoverage::PerField(bitmaps) => {
280 pb::data_overlay_file::Coverage::FieldCoverage(pb::FieldCoverage {
281 offset_bitmaps: bitmaps.iter().map(|b| serialize_roaring(b)).collect(),
282 })
283 }
284 };
285 Self {
286 data_file: Some(pb::DataFile::from(&overlay.data_file)),
287 coverage: Some(coverage),
288 committed_version: overlay.committed_version,
289 }
290 }
291}
292
293impl TryFrom<pb::DataOverlayFile> for DataOverlayFile {
294 type Error = Error;
295
296 fn try_from(proto: pb::DataOverlayFile) -> Result<Self> {
297 let data_file = proto
298 .data_file
299 .ok_or_else(|| Error::invalid_input("DataOverlayFile is missing its data_file"))?;
300 let path = Path::from(data_file.path.as_str());
301 let coverage = match proto.coverage {
302 Some(pb::data_overlay_file::Coverage::SharedOffsetBitmap(bytes)) => {
303 OverlayCoverage::Shared(Arc::new(deserialize_roaring(&bytes, &path)?))
304 }
305 Some(pb::data_overlay_file::Coverage::FieldCoverage(fc)) => OverlayCoverage::PerField(
306 fc.offset_bitmaps
307 .iter()
308 .map(|b| deserialize_roaring(b, &path).map(Arc::new))
309 .collect::<Result<_>>()?,
310 ),
311 None => {
312 return Err(Error::invalid_input(
313 "DataOverlayFile is missing its coverage",
314 ));
315 }
316 };
317 Ok(Self {
318 data_file: DataFile::try_from(data_file)?,
319 coverage,
320 committed_version: proto.committed_version,
321 })
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_data_overlay_missing_fields_error() {
331 // A DataOverlayFile proto missing its coverage or data_file is rejected.
332 let no_coverage = pb::DataOverlayFile {
333 data_file: Some(pb::DataFile::from(&DataFile::new_legacy_from_fields(
334 "overlay.lance",
335 vec![3],
336 None,
337 ))),
338 coverage: None,
339 committed_version: 1,
340 };
341 let err = DataOverlayFile::try_from(no_coverage).unwrap_err();
342 assert!(err.to_string().contains("missing its coverage"), "{err}");
343
344 let no_data_file = pb::DataOverlayFile {
345 data_file: None,
346 coverage: Some(pb::data_overlay_file::Coverage::SharedOffsetBitmap(
347 serialize_roaring(&RoaringBitmap::from_iter([0u32])),
348 )),
349 committed_version: 1,
350 };
351 let err = DataOverlayFile::try_from(no_data_file).unwrap_err();
352 assert!(err.to_string().contains("missing its data_file"), "{err}");
353 }
354
355 #[test]
356 fn test_overlay_coverage_serde_json_roundtrip() {
357 // The custom serde impl round-trips through JSON for dense/sparse,
358 // including empty bitmaps and a zero-bitmap sparse coverage.
359 for coverage in [
360 OverlayCoverage::dense(RoaringBitmap::from_iter([1u32, 5, 100])),
361 OverlayCoverage::dense(RoaringBitmap::new()),
362 OverlayCoverage::sparse(vec![
363 RoaringBitmap::from_iter([2u32, 3]),
364 RoaringBitmap::new(),
365 ]),
366 OverlayCoverage::sparse(vec![]),
367 ] {
368 let json = serde_json::to_string(&coverage).unwrap();
369 let back: OverlayCoverage = serde_json::from_str(&json).unwrap();
370 assert_eq!(back, coverage);
371 }
372 }
373
374 #[test]
375 fn test_tombstone_overlay_fields() {
376 // An overlay covering fields [3, 5]: replacing field 5 tombstones just
377 // field 5's slot and keeps field 3. An overlay covering only field 5 is
378 // dropped entirely. An overlay touching no replaced field is untouched.
379 let mut overlays = vec![
380 DataOverlayFile {
381 data_file: DataFile::new_legacy_from_fields("a.lance", vec![3, 5], None),
382 coverage: OverlayCoverage::sparse(vec![
383 RoaringBitmap::from_iter([0u32]),
384 RoaringBitmap::from_iter([1u32]),
385 ]),
386 committed_version: 1,
387 },
388 DataOverlayFile {
389 data_file: DataFile::new_legacy_from_fields("b.lance", vec![5], None),
390 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
391 committed_version: 1,
392 },
393 DataOverlayFile {
394 data_file: DataFile::new_legacy_from_fields("c.lance", vec![7], None),
395 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
396 committed_version: 1,
397 },
398 ];
399
400 tombstone_overlay_fields(&mut overlays, &[5]);
401
402 // The single-field overlay on field 5 is gone; the others remain.
403 assert_eq!(overlays.len(), 2);
404 // Field 3 preserved, field 5 tombstoned in place (coverage stays aligned).
405 assert_eq!(
406 overlays[0].data_file.fields.as_ref(),
407 &[3, TOMBSTONE_FIELD_ID]
408 );
409 // The untouched overlay keeps its field.
410 assert_eq!(overlays[1].data_file.fields.as_ref(), &[7]);
411 }
412
413 #[test]
414 fn test_verify_overlays_newest_last() {
415 let mk = |version: u64| DataOverlayFile {
416 data_file: DataFile::new_legacy_from_fields("o.lance", vec![3], None),
417 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
418 committed_version: version,
419 };
420 // Non-decreasing (including equal versions) is accepted.
421 assert!(verify_overlays_newest_last(&[]).is_ok());
422 assert!(verify_overlays_newest_last(&[mk(1), mk(2), mk(2), mk(5)]).is_ok());
423 // A newer version before an older one is rejected.
424 let err = verify_overlays_newest_last(&[mk(2), mk(1)]).unwrap_err();
425 assert!(err.to_string().contains("newest-last"), "{err}");
426 }
427
428 #[test]
429 fn test_coverage_for_field_out_of_bounds() {
430 let overlay = DataOverlayFile {
431 data_file: DataFile::new_legacy_from_fields("o.lance", vec![2, 4], None),
432 coverage: OverlayCoverage::sparse(vec![
433 RoaringBitmap::from_iter([1u32]),
434 RoaringBitmap::from_iter([2u32]),
435 ]),
436 committed_version: 1,
437 };
438 assert!(overlay.coverage_for_field(0).is_ok());
439 assert!(overlay.coverage_for_field(1).is_ok());
440 let err = overlay.coverage_for_field(5).unwrap_err();
441 assert!(err.to_string().contains("field position"), "{err}");
442 }
443}