mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-07-12 03:12:11 +02:00
iss-merge-rowid-overlap-corrupts-filtered-reads / lance#7444: an
update-style merge_insert over a merge-written fragment legally reuses the
updated rows' stable row ids (row-id-lineage spec: updates preserve
_rowid) while the superseded fragment keeps its full sequence plus a
deletion vector. A later delete leaves the overlapping id range sparsely
tiled, and lance-table 7.0.0's RowIdIndex::new asserted dense tiling —
failing every filtered read that builds the id→address map ("Wrong range"
debug assert; "all columns in a record batch must have the same length"
or a silently-wrong batch in release).
The upstream fix (lance#7480, merged 2026-07-01) landed hours AFTER
v8.0.0 was cut, so no release ≤ 8.0.0 carries it. Consume it now as a
vendored pin: vendor/lance-table is the pristine published 7.0.0 source
plus ONLY the #7480 rowids/index.rs hunk (drop the false tiling assert;
hard-error on the true invariant — one live id claimed by two fragments)
and upstream's regression unit test, wired via [patch.crates-io]. The fix
is read-side only, so already-written graphs become readable as-is — no
data repair.
Removal condition (see vendor/lance-table/README.omnigraph.md): drop the
vendor dir + patch entry at the first Lance bump whose lance-table ships
lance#7480 (9.0.0, or a backported 8.0.1). The surface guard
filtered_scan_tolerates_merge_update_row_id_overlap keeps that honest in
both directions.
Turns the previous commit's red tests green. Full workspace gate passes
(cargo test --workspace --locked --no-fail-fast, 68 suites).
1141 lines
43 KiB
Rust
1141 lines
43 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
|
|
|
use std::ops::{Range, RangeInclusive};
|
|
|
|
use super::{bitmap::Bitmap, encoded_array::EncodedU64Array};
|
|
use deepsize::DeepSizeOf;
|
|
|
|
/// Convert an estimated serialized byte cost from `u128` to `usize`, saturating
|
|
/// at [`usize::MAX`] when the value does not fit (infeasible encodings).
|
|
#[inline]
|
|
fn u128_byte_cost_to_usize(v: u128) -> usize {
|
|
usize::try_from(v).unwrap_or(usize::MAX)
|
|
}
|
|
|
|
/// Different ways to represent a sequence of distinct u64s.
|
|
///
|
|
/// This is designed to be especially efficient for sequences that are sorted,
|
|
/// but not meaningfully larger than a `Vec<u64>` in the worst case.
|
|
///
|
|
/// The representation is chosen based on the properties of the sequence:
|
|
///
|
|
/// Sorted?───►Yes ───►Contiguous?─► Yes─► Range
|
|
/// │ ▼
|
|
/// │ No
|
|
/// │ ▼
|
|
/// │ Dense?─────► Yes─► RangeWithBitmap/RangeWithHoles
|
|
/// │ ▼
|
|
/// │ No─────────────► SortedArray
|
|
/// ▼
|
|
/// No──────────────────────────────► Array
|
|
///
|
|
/// "Dense" is decided based on the estimated byte size of the representation.
|
|
///
|
|
/// Size of RangeWithBitMap for N values:
|
|
/// 8 bytes + 8 bytes + ceil((max - min) / 8) bytes
|
|
/// Size of SortedArray for N values (assuming u16 packed):
|
|
/// 8 bytes + 8 bytes + 8 bytes + 2 bytes * N
|
|
///
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub enum U64Segment {
|
|
/// A contiguous sorted range of row ids.
|
|
///
|
|
/// Total size: 16 bytes
|
|
Range(Range<u64>),
|
|
/// A sorted range of row ids, that is mostly contiguous.
|
|
///
|
|
/// Total size: 24 bytes + n_holes * 4 bytes
|
|
/// Use when: 32 * n_holes < max - min
|
|
RangeWithHoles {
|
|
range: Range<u64>,
|
|
/// Bitmap of offsets from the start of the range that are holes.
|
|
/// This is sorted, so binary search can be used. It's typically
|
|
/// relatively small.
|
|
holes: EncodedU64Array,
|
|
},
|
|
/// A sorted range of row ids, that is mostly contiguous.
|
|
///
|
|
/// Bitmap is 1 when the value is present, 0 when it's missing.
|
|
///
|
|
/// Total size: 24 bytes + ceil((max - min) / 8) bytes
|
|
/// Use when: max - min > 16 * len
|
|
RangeWithBitmap { range: Range<u64>, bitmap: Bitmap },
|
|
/// A sorted array of row ids, that is sparse.
|
|
///
|
|
/// Total size: 24 bytes + 2 * n_values bytes
|
|
SortedArray(EncodedU64Array),
|
|
/// An array of row ids, that is not sorted.
|
|
Array(EncodedU64Array),
|
|
}
|
|
|
|
impl DeepSizeOf for U64Segment {
|
|
fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
|
|
match self {
|
|
Self::Range(_) => 0,
|
|
Self::RangeWithHoles { holes, .. } => holes.deep_size_of_children(context),
|
|
Self::RangeWithBitmap { bitmap, .. } => bitmap.deep_size_of_children(context),
|
|
Self::SortedArray(array) => array.deep_size_of_children(context),
|
|
Self::Array(array) => array.deep_size_of_children(context),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Statistics about a segment of u64s.
|
|
#[derive(Debug)]
|
|
struct SegmentStats {
|
|
/// Min value in the segment.
|
|
min: u64,
|
|
/// Max value in the segment
|
|
max: u64,
|
|
/// Total number of values in the segment
|
|
count: u64,
|
|
/// Whether the segment is sorted
|
|
sorted: bool,
|
|
}
|
|
|
|
impl SegmentStats {
|
|
/// Number of missing values ("holes") in the range `[min, max]`.
|
|
///
|
|
/// Returns `u128` because the total slot count `max - min + 1` can be up
|
|
/// to `2^64` (when `min = 0, max = u64::MAX`), which exceeds `u64::MAX`.
|
|
fn n_holes(&self) -> u128 {
|
|
debug_assert!(self.sorted);
|
|
if self.count == 0 {
|
|
0
|
|
} else {
|
|
let total_slots = self.max as u128 - self.min as u128 + 1;
|
|
total_slots - self.count as u128
|
|
}
|
|
}
|
|
}
|
|
|
|
impl U64Segment {
|
|
/// Return the values that are missing from the slice.
|
|
fn holes_in_slice<'a>(
|
|
range: RangeInclusive<u64>,
|
|
existing: impl IntoIterator<Item = u64> + 'a,
|
|
) -> impl Iterator<Item = u64> + 'a {
|
|
let mut existing = existing.into_iter().peekable();
|
|
range.filter(move |val| {
|
|
if let Some(&existing_val) = existing.peek()
|
|
&& existing_val == *val
|
|
{
|
|
existing.next();
|
|
return false;
|
|
}
|
|
true
|
|
})
|
|
}
|
|
|
|
fn compute_stats(values: impl IntoIterator<Item = u64>) -> SegmentStats {
|
|
let mut sorted = true;
|
|
let mut min = u64::MAX;
|
|
let mut max = 0;
|
|
let mut count = 0;
|
|
|
|
for val in values {
|
|
count += 1;
|
|
if val < min {
|
|
min = val;
|
|
}
|
|
if val > max {
|
|
max = val;
|
|
}
|
|
if sorted && count > 1 && val < max {
|
|
sorted = false;
|
|
}
|
|
}
|
|
|
|
if count == 0 {
|
|
min = 0;
|
|
max = 0;
|
|
}
|
|
|
|
SegmentStats {
|
|
min,
|
|
max,
|
|
count,
|
|
sorted,
|
|
}
|
|
}
|
|
|
|
/// Estimate the serialized byte size of each sorted encoding variant.
|
|
///
|
|
/// All arithmetic is performed in `u128` to avoid overflow when the range
|
|
/// span `max - min + 1` approaches or exceeds `2^64`. Infeasible sizes
|
|
/// saturate to `usize::MAX` so they always lose the `min()` comparison.
|
|
fn sorted_sequence_sizes(stats: &SegmentStats) -> [usize; 3] {
|
|
let n_holes = stats.n_holes();
|
|
let total_slots = stats.max as u128 - stats.min as u128 + 1;
|
|
|
|
let range_with_holes = 24u128.saturating_add(4u128.saturating_mul(n_holes));
|
|
let range_with_bitmap = 24u128.saturating_add(total_slots.div_ceil(8));
|
|
let sorted_array = 24u128.saturating_add(2u128.saturating_mul(stats.count as u128));
|
|
|
|
[
|
|
u128_byte_cost_to_usize(range_with_holes),
|
|
u128_byte_cost_to_usize(range_with_bitmap),
|
|
u128_byte_cost_to_usize(sorted_array),
|
|
]
|
|
}
|
|
|
|
fn from_stats_and_sequence(
|
|
stats: SegmentStats,
|
|
sequence: impl IntoIterator<Item = u64>,
|
|
) -> Self {
|
|
if stats.sorted {
|
|
let n_holes = stats.n_holes();
|
|
// Range-backed encodings store an exclusive end as `Range<u64>`,
|
|
// which cannot represent `u64::MAX + 1`. Compute the end once and
|
|
// gate all range-backed branches on its representability.
|
|
let exclusive_end = stats.max.checked_add(1);
|
|
if stats.count == 0 {
|
|
Self::Range(0..0)
|
|
} else if n_holes == 0 && exclusive_end.is_some() {
|
|
Self::Range(stats.min..exclusive_end.unwrap())
|
|
} else if let Some(end) = exclusive_end {
|
|
let sizes = Self::sorted_sequence_sizes(&stats);
|
|
let min_size = sizes.iter().min().unwrap();
|
|
if min_size == &sizes[0] {
|
|
let range = stats.min..end;
|
|
let mut holes =
|
|
Self::holes_in_slice(stats.min..=stats.max, sequence).collect::<Vec<_>>();
|
|
holes.sort_unstable();
|
|
let holes = EncodedU64Array::from(holes);
|
|
Self::RangeWithHoles { range, holes }
|
|
} else if min_size == &sizes[1] {
|
|
let range = stats.min..end;
|
|
let mut bitmap = Bitmap::new_full((stats.max - stats.min) as usize + 1);
|
|
for hole in Self::holes_in_slice(stats.min..=stats.max, sequence) {
|
|
let offset = (hole - stats.min) as usize;
|
|
bitmap.clear(offset);
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap }
|
|
} else {
|
|
Self::SortedArray(EncodedU64Array::from_iter(sequence))
|
|
}
|
|
} else {
|
|
// max == u64::MAX: exclusive end is unrepresentable in Range<u64>,
|
|
// so no range-backed encoding can be used.
|
|
Self::SortedArray(EncodedU64Array::from_iter(sequence))
|
|
}
|
|
} else {
|
|
Self::Array(EncodedU64Array::from_iter(sequence))
|
|
}
|
|
}
|
|
|
|
pub fn from_slice(slice: &[u64]) -> Self {
|
|
Self::from_iter(slice.iter().copied())
|
|
}
|
|
}
|
|
|
|
impl FromIterator<u64> for U64Segment {
|
|
fn from_iter<T: IntoIterator<Item = u64>>(iter: T) -> Self {
|
|
let values: Vec<u64> = iter.into_iter().collect();
|
|
let stats = Self::compute_stats(values.iter().copied());
|
|
Self::from_stats_and_sequence(stats, values)
|
|
}
|
|
}
|
|
|
|
impl U64Segment {
|
|
pub fn iter(&self) -> Box<dyn DoubleEndedIterator<Item = u64> + '_> {
|
|
match self {
|
|
Self::Range(range) => Box::new(range.clone()),
|
|
Self::RangeWithHoles { range, holes } => {
|
|
Box::new((range.start..range.end).filter(move |&val| {
|
|
// TODO: we could write a more optimal version of this
|
|
// iterator, but would need special handling to make it
|
|
// double ended.
|
|
holes.binary_search(val).is_err()
|
|
}))
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap } => {
|
|
Box::new((range.start..range.end).filter(|val| {
|
|
let offset = (val - range.start) as usize;
|
|
bitmap.get(offset)
|
|
}))
|
|
}
|
|
Self::SortedArray(array) => Box::new(array.iter()),
|
|
Self::Array(array) => Box::new(array.iter()),
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
match self {
|
|
Self::Range(range) => (range.end - range.start) as usize,
|
|
Self::RangeWithHoles { range, holes } => {
|
|
let holes = holes.iter().count();
|
|
(range.end - range.start) as usize - holes
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap } => {
|
|
let holes = bitmap.count_zeros();
|
|
(range.end - range.start) as usize - holes
|
|
}
|
|
Self::SortedArray(array) => array.len(),
|
|
Self::Array(array) => array.len(),
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
|
|
/// Get the min and max value of the segment, excluding tombstones.
|
|
pub fn range(&self) -> Option<RangeInclusive<u64>> {
|
|
match self {
|
|
Self::Range(range) if range.is_empty() => None,
|
|
Self::Range(range)
|
|
| Self::RangeWithBitmap { range, .. }
|
|
| Self::RangeWithHoles { range, .. } => Some(range.start..=(range.end - 1)),
|
|
Self::SortedArray(array) => {
|
|
// We can assume that the array is sorted.
|
|
let min_value = array.first().unwrap();
|
|
let max_value = array.last().unwrap();
|
|
Some(min_value..=max_value)
|
|
}
|
|
Self::Array(array) => {
|
|
let min_value = array.min().unwrap();
|
|
let max_value = array.max().unwrap();
|
|
Some(min_value..=max_value)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn slice(&self, offset: usize, len: usize) -> Self {
|
|
if len == 0 {
|
|
return Self::Range(0..0);
|
|
}
|
|
|
|
let values: Vec<u64> = self.iter().skip(offset).take(len).collect();
|
|
|
|
// `from_slice` will compute stats and select the best representation.
|
|
Self::from_slice(&values)
|
|
}
|
|
|
|
pub fn position(&self, val: u64) -> Option<usize> {
|
|
match self {
|
|
Self::Range(range) => {
|
|
if range.contains(&val) {
|
|
Some((val - range.start) as usize)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Self::RangeWithHoles { range, holes } => {
|
|
if !range.contains(&val) {
|
|
return None;
|
|
}
|
|
// binary_search returns Err(idx) where idx is the count of holes
|
|
// strictly less than val (holes are unique and sorted).
|
|
match holes.binary_search(val) {
|
|
Ok(_) => None,
|
|
Err(num_holes_before) => {
|
|
let offset = (val - range.start) as usize;
|
|
Some(offset - num_holes_before)
|
|
}
|
|
}
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap } => {
|
|
if range.contains(&val) && bitmap.get((val - range.start) as usize) {
|
|
let offset = (val - range.start) as usize;
|
|
let num_zeros = bitmap.slice(0, offset).count_zeros();
|
|
Some(offset - num_zeros)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Self::SortedArray(array) => array.binary_search(val).ok(),
|
|
Self::Array(array) => array.iter().position(|v| v == val),
|
|
}
|
|
}
|
|
|
|
pub fn get(&self, i: usize) -> Option<u64> {
|
|
match self {
|
|
Self::Range(range) => match range.start.checked_add(i as u64) {
|
|
Some(val) if val < range.end => Some(val),
|
|
_ => None,
|
|
},
|
|
Self::RangeWithHoles { range, holes } => {
|
|
let len = (range.end - range.start) as usize - holes.len();
|
|
if i >= len {
|
|
return None;
|
|
}
|
|
// The i-th surviving value v satisfies v = range.start + i + k,
|
|
// where k = |{h ∈ holes : h < v}|. holes[k] - k is monotone
|
|
// non-decreasing in k (holes are sorted and unique), so binary
|
|
// search for the smallest k such that holes[k] - k > range.start + i.
|
|
let target = range.start + i as u64;
|
|
let mut lo = 0usize;
|
|
let mut hi = holes.len();
|
|
while lo < hi {
|
|
let mid = (lo + hi) / 2;
|
|
let h = holes.get(mid).unwrap();
|
|
if h.saturating_sub(mid as u64) > target {
|
|
hi = mid;
|
|
} else {
|
|
lo = mid + 1;
|
|
}
|
|
}
|
|
Some(range.start + i as u64 + lo as u64)
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap } => {
|
|
// Find the i-th set bit (a "select1") via byte-wise popcount.
|
|
// Bytes past `bitmap.len()` are zero-padded by construction
|
|
// (Bitmap::new_full), so popcount counts only valid positions.
|
|
let mut remaining = i;
|
|
for (byte_idx, &byte) in bitmap.data.iter().enumerate() {
|
|
let ones = byte.count_ones() as usize;
|
|
if remaining < ones {
|
|
let mut b = byte;
|
|
for _ in 0..remaining {
|
|
b &= b - 1; // clear lowest set bit
|
|
}
|
|
let bit = b.trailing_zeros() as usize;
|
|
return Some(range.start + (byte_idx * 8 + bit) as u64);
|
|
}
|
|
remaining -= ones;
|
|
}
|
|
None
|
|
}
|
|
Self::SortedArray(array) => array.get(i),
|
|
Self::Array(array) => array.get(i),
|
|
}
|
|
}
|
|
|
|
/// Check if a value is contained in the segment
|
|
pub fn contains(&self, val: u64) -> bool {
|
|
match self {
|
|
Self::Range(range) => range.contains(&val),
|
|
Self::RangeWithHoles { range, holes } => {
|
|
if !range.contains(&val) {
|
|
return false;
|
|
}
|
|
// Check if the value is not in the holes
|
|
!holes.iter().any(|hole| hole == val)
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap } => {
|
|
if !range.contains(&val) {
|
|
return false;
|
|
}
|
|
// Check if the bitmap has the value set (not cleared)
|
|
let idx = (val - range.start) as usize;
|
|
bitmap.get(idx)
|
|
}
|
|
Self::SortedArray(array) => array.binary_search(val).is_ok(),
|
|
Self::Array(array) => array.iter().any(|v| v == val),
|
|
}
|
|
}
|
|
|
|
/// Produce a new segment that has `val` as the new highest value in the segment
|
|
pub fn with_new_high(self, val: u64) -> lance_core::Result<Self> {
|
|
// Check that the new value is higher than the current maximum
|
|
if let Some(range) = self.range()
|
|
&& val <= *range.end()
|
|
{
|
|
return Err(lance_core::Error::invalid_input(format!(
|
|
"New value {} must be higher than current maximum {}",
|
|
val,
|
|
range.end()
|
|
)));
|
|
}
|
|
|
|
Ok(match self {
|
|
Self::Range(range) => {
|
|
// Special case for empty range: create a range containing only the new value
|
|
if range.start == range.end {
|
|
Self::Range(Range {
|
|
start: val,
|
|
end: val + 1,
|
|
})
|
|
} else if val == range.end {
|
|
Self::Range(Range {
|
|
start: range.start,
|
|
end: val + 1,
|
|
})
|
|
} else {
|
|
Self::RangeWithHoles {
|
|
range: Range {
|
|
start: range.start,
|
|
end: val + 1,
|
|
},
|
|
holes: EncodedU64Array::U64((range.end..val).collect()),
|
|
}
|
|
}
|
|
}
|
|
Self::RangeWithHoles { range, holes } => {
|
|
if val == range.end {
|
|
Self::RangeWithHoles {
|
|
range: Range {
|
|
start: range.start,
|
|
end: val + 1,
|
|
},
|
|
holes,
|
|
}
|
|
} else {
|
|
let mut new_holes: Vec<u64> = holes.iter().collect();
|
|
new_holes.extend(range.end..val);
|
|
Self::RangeWithHoles {
|
|
range: Range {
|
|
start: range.start,
|
|
end: val + 1,
|
|
},
|
|
holes: EncodedU64Array::U64(new_holes),
|
|
}
|
|
}
|
|
}
|
|
Self::RangeWithBitmap { range, bitmap } => {
|
|
let new_range = Range {
|
|
start: range.start,
|
|
end: val + 1,
|
|
};
|
|
let gap_size = (val - range.end) as usize;
|
|
let new_bitmap = bitmap
|
|
.iter()
|
|
.chain(std::iter::repeat_n(false, gap_size))
|
|
.chain(std::iter::once(true))
|
|
.collect::<Vec<bool>>();
|
|
|
|
Self::RangeWithBitmap {
|
|
range: new_range,
|
|
bitmap: Bitmap::from(new_bitmap.as_slice()),
|
|
}
|
|
}
|
|
Self::SortedArray(array) => match array {
|
|
EncodedU64Array::U64(mut vec) => {
|
|
vec.push(val);
|
|
Self::SortedArray(EncodedU64Array::U64(vec))
|
|
}
|
|
EncodedU64Array::U16 { base, offsets } => {
|
|
if let Some(offset) = val.checked_sub(base) {
|
|
if offset <= u16::MAX as u64 {
|
|
let mut offsets = offsets;
|
|
offsets.push(offset as u16);
|
|
return Ok(Self::SortedArray(EncodedU64Array::U16 { base, offsets }));
|
|
} else if offset <= u32::MAX as u64 {
|
|
let mut u32_offsets: Vec<u32> =
|
|
offsets.into_iter().map(|o| o as u32).collect();
|
|
u32_offsets.push(offset as u32);
|
|
return Ok(Self::SortedArray(EncodedU64Array::U32 {
|
|
base,
|
|
offsets: u32_offsets,
|
|
}));
|
|
}
|
|
}
|
|
let mut new_array: Vec<u64> =
|
|
offsets.into_iter().map(|o| base + o as u64).collect();
|
|
new_array.push(val);
|
|
Self::SortedArray(EncodedU64Array::from(new_array))
|
|
}
|
|
EncodedU64Array::U32 { base, mut offsets } => {
|
|
if let Some(offset) = val.checked_sub(base)
|
|
&& offset <= u32::MAX as u64
|
|
{
|
|
offsets.push(offset as u32);
|
|
return Ok(Self::SortedArray(EncodedU64Array::U32 { base, offsets }));
|
|
}
|
|
let mut new_array: Vec<u64> =
|
|
offsets.into_iter().map(|o| base + o as u64).collect();
|
|
new_array.push(val);
|
|
Self::SortedArray(EncodedU64Array::from(new_array))
|
|
}
|
|
},
|
|
Self::Array(array) => match array {
|
|
EncodedU64Array::U64(mut vec) => {
|
|
vec.push(val);
|
|
Self::Array(EncodedU64Array::U64(vec))
|
|
}
|
|
EncodedU64Array::U16 { base, offsets } => {
|
|
if let Some(offset) = val.checked_sub(base) {
|
|
if offset <= u16::MAX as u64 {
|
|
let mut offsets = offsets;
|
|
offsets.push(offset as u16);
|
|
return Ok(Self::Array(EncodedU64Array::U16 { base, offsets }));
|
|
} else if offset <= u32::MAX as u64 {
|
|
let mut u32_offsets: Vec<u32> =
|
|
offsets.into_iter().map(|o| o as u32).collect();
|
|
u32_offsets.push(offset as u32);
|
|
return Ok(Self::Array(EncodedU64Array::U32 {
|
|
base,
|
|
offsets: u32_offsets,
|
|
}));
|
|
}
|
|
}
|
|
let mut new_array: Vec<u64> =
|
|
offsets.into_iter().map(|o| base + o as u64).collect();
|
|
new_array.push(val);
|
|
Self::Array(EncodedU64Array::from(new_array))
|
|
}
|
|
EncodedU64Array::U32 { base, mut offsets } => {
|
|
if let Some(offset) = val.checked_sub(base)
|
|
&& offset <= u32::MAX as u64
|
|
{
|
|
offsets.push(offset as u32);
|
|
return Ok(Self::Array(EncodedU64Array::U32 { base, offsets }));
|
|
}
|
|
let mut new_array: Vec<u64> =
|
|
offsets.into_iter().map(|o| base + o as u64).collect();
|
|
new_array.push(val);
|
|
Self::Array(EncodedU64Array::from(new_array))
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Delete a set of row ids from the segment.
|
|
/// The row ids are assumed to be in the segment. (within the range, not
|
|
/// already deleted.)
|
|
/// They are also assumed to be ordered by appearance in the segment.
|
|
pub fn delete(&self, vals: &[u64]) -> Self {
|
|
// TODO: can we enforce these assumptions? or make them safer?
|
|
debug_assert!(vals.iter().all(|&val| self.range().unwrap().contains(&val)));
|
|
|
|
let make_new_iter = || {
|
|
let mut vals_iter = vals.iter().copied().peekable();
|
|
self.iter().filter(move |val| {
|
|
if let Some(&next_val) = vals_iter.peek()
|
|
&& next_val == *val
|
|
{
|
|
vals_iter.next();
|
|
return false;
|
|
}
|
|
true
|
|
})
|
|
};
|
|
let stats = Self::compute_stats(make_new_iter());
|
|
Self::from_stats_and_sequence(stats, make_new_iter())
|
|
}
|
|
|
|
pub fn mask(&mut self, positions: &[u32]) {
|
|
if positions.is_empty() {
|
|
return;
|
|
}
|
|
if positions.len() == self.len() {
|
|
*self = Self::Range(0..0);
|
|
return;
|
|
}
|
|
let count = (self.len() - positions.len()) as u64;
|
|
let sorted = match self {
|
|
Self::Range(_) => true,
|
|
Self::RangeWithHoles { .. } => true,
|
|
Self::RangeWithBitmap { .. } => true,
|
|
Self::SortedArray(_) => true,
|
|
Self::Array(_) => false,
|
|
};
|
|
// To get minimum, need to find the first value that is not masked.
|
|
let first_unmasked = (0..self.len())
|
|
.zip(positions.iter().cycle())
|
|
.find(|(sequential_i, i)| **i != *sequential_i as u32)
|
|
.map(|(sequential_i, _)| sequential_i)
|
|
.unwrap();
|
|
let min = self.get(first_unmasked).unwrap();
|
|
|
|
let last_unmasked = (0..self.len())
|
|
.rev()
|
|
.zip(positions.iter().rev().cycle())
|
|
.filter(|(sequential_i, i)| **i != *sequential_i as u32)
|
|
.map(|(sequential_i, _)| sequential_i)
|
|
.next()
|
|
.unwrap();
|
|
let max = self.get(last_unmasked).unwrap();
|
|
|
|
let stats = SegmentStats {
|
|
min,
|
|
max,
|
|
count,
|
|
sorted,
|
|
};
|
|
|
|
let mut positions = positions.iter().copied().peekable();
|
|
let sequence = self.iter().enumerate().filter_map(move |(i, val)| {
|
|
if let Some(next_pos) = positions.peek()
|
|
&& *next_pos == i as u32
|
|
{
|
|
positions.next();
|
|
return None;
|
|
}
|
|
Some(val)
|
|
});
|
|
*self = Self::from_stats_and_sequence(stats, sequence)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_segments() {
|
|
fn check_segment(values: &[u64], expected: &U64Segment) {
|
|
let segment = U64Segment::from_slice(values);
|
|
assert_eq!(segment, *expected);
|
|
assert_eq!(values.len(), segment.len());
|
|
|
|
let roundtripped = segment.iter().collect::<Vec<_>>();
|
|
assert_eq!(roundtripped, values);
|
|
|
|
let expected_min = values.iter().copied().min();
|
|
let expected_max = values.iter().copied().max();
|
|
match segment.range() {
|
|
Some(range) => {
|
|
assert_eq!(range.start(), &expected_min.unwrap());
|
|
assert_eq!(range.end(), &expected_max.unwrap());
|
|
}
|
|
None => {
|
|
assert_eq!(expected_min, None);
|
|
assert_eq!(expected_max, None);
|
|
}
|
|
}
|
|
|
|
for (i, value) in values.iter().enumerate() {
|
|
assert_eq!(segment.get(i), Some(*value), "i = {}", i);
|
|
assert_eq!(segment.position(*value), Some(i), "i = {}", i);
|
|
}
|
|
|
|
check_segment_iter(&segment);
|
|
}
|
|
|
|
fn check_segment_iter(segment: &U64Segment) {
|
|
// Should be able to iterate forwards and backwards, and get the same thing.
|
|
let forwards = segment.iter().collect::<Vec<_>>();
|
|
let mut backwards = segment.iter().rev().collect::<Vec<_>>();
|
|
backwards.reverse();
|
|
assert_eq!(forwards, backwards);
|
|
|
|
// Should be able to pull from both sides in lockstep.
|
|
let mut expected = Vec::with_capacity(segment.len());
|
|
let mut actual = Vec::with_capacity(segment.len());
|
|
let mut iter = segment.iter();
|
|
// Alternating forwards and backwards
|
|
for i in 0..segment.len() {
|
|
if i % 2 == 0 {
|
|
actual.push(iter.next().unwrap());
|
|
expected.push(segment.get(i / 2).unwrap());
|
|
} else {
|
|
let i = segment.len() - 1 - i / 2;
|
|
actual.push(iter.next_back().unwrap());
|
|
expected.push(segment.get(i).unwrap());
|
|
};
|
|
}
|
|
assert_eq!(expected, actual);
|
|
}
|
|
|
|
// Empty
|
|
check_segment(&[], &U64Segment::Range(0..0));
|
|
|
|
// Single value
|
|
check_segment(&[42], &U64Segment::Range(42..43));
|
|
|
|
// Contiguous range
|
|
check_segment(
|
|
&(100..200).collect::<Vec<_>>(),
|
|
&U64Segment::Range(100..200),
|
|
);
|
|
|
|
// Range with a hole
|
|
let values = (0..1000).filter(|&x| x != 100).collect::<Vec<_>>();
|
|
check_segment(
|
|
&values,
|
|
&U64Segment::RangeWithHoles {
|
|
range: 0..1000,
|
|
holes: vec![100].into(),
|
|
},
|
|
);
|
|
|
|
// Range with every other value missing
|
|
let values = (0..1000).filter(|&x| x % 2 == 0).collect::<Vec<_>>();
|
|
check_segment(
|
|
&values,
|
|
&U64Segment::RangeWithBitmap {
|
|
range: 0..999,
|
|
bitmap: Bitmap::from((0..999).map(|x| x % 2 == 0).collect::<Vec<_>>().as_slice()),
|
|
},
|
|
);
|
|
|
|
// Sparse but sorted sequence
|
|
check_segment(
|
|
&[1, 7000, 24000],
|
|
&U64Segment::SortedArray(vec![1, 7000, 24000].into()),
|
|
);
|
|
|
|
// Sparse unsorted sequence
|
|
check_segment(
|
|
&[7000, 1, 24000],
|
|
&U64Segment::Array(vec![7000, 1, 24000].into()),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_segment_overflow_boundary() {
|
|
// Sparse range spanning i64::MAX — the original overflow reproducer.
|
|
// n_holes ≈ 2^63, which overflows `4 * n_holes as usize` without u128 arithmetic.
|
|
let values: Vec<u64> = vec![0, 1, 2, 100, i64::MAX as u64];
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert!(
|
|
matches!(segment, U64Segment::SortedArray(_)),
|
|
"sparse range spanning i64::MAX should be SortedArray, got {:?}",
|
|
std::mem::discriminant(&segment)
|
|
);
|
|
assert_eq!(segment.len(), 5);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
|
|
// Two values at u64 extremes — triggers n_holes() total_slots overflow
|
|
// (u64::MAX - 0 + 1 wraps to 0 without u128).
|
|
let values: Vec<u64> = vec![0, u64::MAX];
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert!(
|
|
matches!(segment, U64Segment::SortedArray(_)),
|
|
"full u64 span should be SortedArray, got {:?}",
|
|
std::mem::discriminant(&segment)
|
|
);
|
|
assert_eq!(segment.len(), 2);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
|
|
// Small dense set near u64::MAX — cost estimation correctly prefers a
|
|
// range-backed encoding, but Range<u64> cannot represent u64::MAX + 1
|
|
// as the exclusive end. Must fall back to SortedArray.
|
|
let values: Vec<u64> = vec![u64::MAX - 3, u64::MAX - 1, u64::MAX];
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert!(
|
|
matches!(segment, U64Segment::SortedArray(_)),
|
|
"dense set near u64::MAX should be SortedArray (exclusive end unrepresentable), got {:?}",
|
|
std::mem::discriminant(&segment)
|
|
);
|
|
assert_eq!(segment.len(), 3);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
|
|
// Single value at u64::MAX — contiguous range with n_holes == 0, but
|
|
// exclusive end u64::MAX + 1 overflows.
|
|
let values: Vec<u64> = vec![u64::MAX];
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert!(
|
|
matches!(segment, U64Segment::SortedArray(_)),
|
|
"single u64::MAX should be SortedArray, got {:?}",
|
|
std::mem::discriminant(&segment)
|
|
);
|
|
assert_eq!(segment.len(), 1);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
|
|
// Contiguous range ending just below u64::MAX — exclusive end is
|
|
// representable, so Range encoding should still be used.
|
|
let values: Vec<u64> = vec![u64::MAX - 3, u64::MAX - 2, u64::MAX - 1];
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert_eq!(segment, U64Segment::Range((u64::MAX - 3)..u64::MAX));
|
|
assert_eq!(segment.len(), 3);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
|
|
// Regression: normal dense range with few holes still picks RangeWithHoles.
|
|
// Needs total_slots > 32 * n_holes for RangeWithHoles to beat RangeWithBitmap.
|
|
let values: Vec<u64> = (100..1100).filter(|&x| x != 500).collect();
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert_eq!(
|
|
segment,
|
|
U64Segment::RangeWithHoles {
|
|
range: 100..1100,
|
|
holes: vec![500].into(),
|
|
}
|
|
);
|
|
assert_eq!(segment.len(), 999);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
|
|
// Regression: small dense range with hole picks RangeWithBitmap.
|
|
let values: Vec<u64> = vec![100, 101, 102, 103, 105];
|
|
let segment = U64Segment::from_slice(&values);
|
|
assert!(
|
|
matches!(segment, U64Segment::RangeWithBitmap { .. }),
|
|
"small dense range with hole should be RangeWithBitmap, got {:?}",
|
|
std::mem::discriminant(&segment)
|
|
);
|
|
assert_eq!(segment.len(), 5);
|
|
assert_eq!(segment.iter().collect::<Vec<_>>(), values);
|
|
}
|
|
|
|
#[test]
|
|
fn test_u128_byte_cost_to_usize() {
|
|
assert_eq!(super::u128_byte_cost_to_usize(0), 0);
|
|
assert_eq!(super::u128_byte_cost_to_usize(42), 42);
|
|
assert_eq!(
|
|
super::u128_byte_cost_to_usize(usize::MAX as u128),
|
|
usize::MAX
|
|
);
|
|
assert_eq!(super::u128_byte_cost_to_usize(u128::MAX), usize::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sorted_sequence_sizes_sparse_span_saturates_range_with_holes_cost() {
|
|
let stats = super::SegmentStats {
|
|
min: 0,
|
|
max: i64::MAX as u64,
|
|
count: 5,
|
|
sorted: true,
|
|
};
|
|
let sizes = U64Segment::sorted_sequence_sizes(&stats);
|
|
assert_eq!(sizes[0], usize::MAX);
|
|
assert!(sizes[2] < sizes[0]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sorted_sequence_sizes_sorted_array_cost_saturates() {
|
|
// Nearly full [0, u64::MAX] with one hole: count = u64::MAX, n_holes = 1.
|
|
// SortedArray cost 24 + 2 * u64::MAX does not fit in usize on 64-bit.
|
|
let stats = super::SegmentStats {
|
|
min: 0,
|
|
max: u64::MAX,
|
|
count: u64::MAX,
|
|
sorted: true,
|
|
};
|
|
let sizes = U64Segment::sorted_sequence_sizes(&stats);
|
|
assert_eq!(sizes[2], usize::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sorted_sequence_sizes_full_span_bitmap_cost() {
|
|
// Synthetic stats: full [0, u64::MAX] slot space; exercises `range_with_bitmap`
|
|
// cost path (always fits in `usize` on 64-bit targets).
|
|
let stats = super::SegmentStats {
|
|
min: 0,
|
|
max: u64::MAX,
|
|
count: 1,
|
|
sorted: true,
|
|
};
|
|
let sizes = U64Segment::sorted_sequence_sizes(&stats);
|
|
assert!(sizes[1] < sizes[0]);
|
|
assert!(sizes[1] < usize::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_new_high() {
|
|
// Test Range: contiguous sequence
|
|
let segment = U64Segment::Range(10..20);
|
|
|
|
// Test adding value that extends the range
|
|
let result = segment.clone().with_new_high(20).unwrap();
|
|
assert_eq!(result, U64Segment::Range(10..21));
|
|
|
|
// Test adding value that creates holes
|
|
let result = segment.with_new_high(25).unwrap();
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::RangeWithHoles {
|
|
range: 10..26,
|
|
holes: EncodedU64Array::U64(vec![20, 21, 22, 23, 24]),
|
|
}
|
|
);
|
|
|
|
// Test RangeWithHoles: sequence with existing holes
|
|
let segment = U64Segment::RangeWithHoles {
|
|
range: 10..20,
|
|
holes: EncodedU64Array::U64(vec![15, 17]),
|
|
};
|
|
|
|
// Test adding value that extends the range without new holes
|
|
let result = segment.clone().with_new_high(20).unwrap();
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::RangeWithHoles {
|
|
range: 10..21,
|
|
holes: EncodedU64Array::U64(vec![15, 17]),
|
|
}
|
|
);
|
|
|
|
// Test adding value that creates additional holes
|
|
let result = segment.with_new_high(25).unwrap();
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::RangeWithHoles {
|
|
range: 10..26,
|
|
holes: EncodedU64Array::U64(vec![15, 17, 20, 21, 22, 23, 24]),
|
|
}
|
|
);
|
|
|
|
// Test RangeWithBitmap: sequence with bitmap representation
|
|
let mut bitmap = Bitmap::new_full(10);
|
|
bitmap.clear(3); // Clear position 3 (value 13)
|
|
bitmap.clear(7); // Clear position 7 (value 17)
|
|
let segment = U64Segment::RangeWithBitmap {
|
|
range: 10..20,
|
|
bitmap,
|
|
};
|
|
|
|
// Test adding value that extends the range without new holes
|
|
let result = segment.clone().with_new_high(20).unwrap();
|
|
let expected_bitmap = {
|
|
let mut b = Bitmap::new_full(11);
|
|
b.clear(3); // Clear position 3 (value 13)
|
|
b.clear(7); // Clear position 7 (value 17)
|
|
b
|
|
};
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::RangeWithBitmap {
|
|
range: 10..21,
|
|
bitmap: expected_bitmap,
|
|
}
|
|
);
|
|
|
|
// Test adding value that creates additional holes
|
|
let result = segment.with_new_high(25).unwrap();
|
|
let expected_bitmap = {
|
|
let mut b = Bitmap::new_full(16);
|
|
b.clear(3); // Clear position 3 (value 13)
|
|
b.clear(7); // Clear position 7 (value 17)
|
|
// Clear positions 10-14 (values 20-24)
|
|
for i in 10..15 {
|
|
b.clear(i);
|
|
}
|
|
b
|
|
};
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::RangeWithBitmap {
|
|
range: 10..26,
|
|
bitmap: expected_bitmap,
|
|
}
|
|
);
|
|
|
|
// Test SortedArray: sparse sorted sequence
|
|
let segment = U64Segment::SortedArray(EncodedU64Array::U64(vec![1, 5, 10]));
|
|
|
|
let result = segment.with_new_high(15).unwrap();
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::SortedArray(EncodedU64Array::U64(vec![1, 5, 10, 15]))
|
|
);
|
|
|
|
// Test Array: unsorted sequence
|
|
let segment = U64Segment::Array(EncodedU64Array::U64(vec![10, 5, 1]));
|
|
|
|
let result = segment.with_new_high(15).unwrap();
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::Array(EncodedU64Array::U64(vec![10, 5, 1, 15]))
|
|
);
|
|
|
|
// Test edge cases
|
|
// Empty segment
|
|
let segment = U64Segment::Range(0..0);
|
|
let result = segment.with_new_high(5).unwrap();
|
|
assert_eq!(result, U64Segment::Range(5..6));
|
|
|
|
// Single value segment
|
|
let segment = U64Segment::Range(42..43);
|
|
let result = segment.with_new_high(50).unwrap();
|
|
assert_eq!(
|
|
result,
|
|
U64Segment::RangeWithHoles {
|
|
range: 42..51,
|
|
holes: EncodedU64Array::U64(vec![43, 44, 45, 46, 47, 48, 49]),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_new_high_assertion() {
|
|
let segment = U64Segment::Range(10..20);
|
|
// This should return an error because 15 is not higher than the current maximum 19
|
|
let result = segment.with_new_high(15);
|
|
assert!(result.is_err());
|
|
let error = result.unwrap_err();
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("New value 15 must be higher than current maximum 19")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_new_high_assertion_equal() {
|
|
let segment = U64Segment::Range(1..6);
|
|
// This should return an error because 5 is not higher than the current maximum 5
|
|
let result = segment.with_new_high(5);
|
|
assert!(result.is_err());
|
|
let error = result.unwrap_err();
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("New value 5 must be higher than current maximum 5")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_contains() {
|
|
// Test Range: contiguous sequence
|
|
let segment = U64Segment::Range(10..20);
|
|
assert!(segment.contains(10), "Should contain 10");
|
|
assert!(segment.contains(15), "Should contain 15");
|
|
assert!(segment.contains(19), "Should contain 19");
|
|
assert!(!segment.contains(9), "Should not contain 9");
|
|
assert!(!segment.contains(20), "Should not contain 20");
|
|
assert!(!segment.contains(25), "Should not contain 25");
|
|
|
|
// Test RangeWithHoles: sequence with holes
|
|
let segment = U64Segment::RangeWithHoles {
|
|
range: 10..20,
|
|
holes: EncodedU64Array::U64(vec![15, 17]),
|
|
};
|
|
assert!(segment.contains(10), "Should contain 10");
|
|
assert!(segment.contains(14), "Should contain 14");
|
|
assert!(!segment.contains(15), "Should not contain 15 (hole)");
|
|
assert!(segment.contains(16), "Should contain 16");
|
|
assert!(!segment.contains(17), "Should not contain 17 (hole)");
|
|
assert!(segment.contains(18), "Should contain 18");
|
|
assert!(
|
|
!segment.contains(20),
|
|
"Should not contain 20 (out of range)"
|
|
);
|
|
|
|
// Test RangeWithBitmap: sequence with bitmap
|
|
let mut bitmap = Bitmap::new_full(10);
|
|
bitmap.clear(3); // Clear position 3 (value 13)
|
|
bitmap.clear(7); // Clear position 7 (value 17)
|
|
let segment = U64Segment::RangeWithBitmap {
|
|
range: 10..20,
|
|
bitmap,
|
|
};
|
|
assert!(segment.contains(10), "Should contain 10");
|
|
assert!(segment.contains(12), "Should contain 12");
|
|
assert!(
|
|
!segment.contains(13),
|
|
"Should not contain 13 (cleared in bitmap)"
|
|
);
|
|
assert!(segment.contains(16), "Should contain 16");
|
|
assert!(
|
|
!segment.contains(17),
|
|
"Should not contain 17 (cleared in bitmap)"
|
|
);
|
|
assert!(segment.contains(19), "Should contain 19");
|
|
assert!(
|
|
!segment.contains(20),
|
|
"Should not contain 20 (out of range)"
|
|
);
|
|
|
|
// Test SortedArray: sparse sorted sequence
|
|
let segment = U64Segment::SortedArray(EncodedU64Array::U64(vec![1, 5, 10]));
|
|
assert!(segment.contains(1), "Should contain 1");
|
|
assert!(segment.contains(5), "Should contain 5");
|
|
assert!(segment.contains(10), "Should contain 10");
|
|
assert!(!segment.contains(0), "Should not contain 0");
|
|
assert!(!segment.contains(3), "Should not contain 3");
|
|
assert!(!segment.contains(15), "Should not contain 15");
|
|
|
|
// Test Array: unsorted sequence
|
|
let segment = U64Segment::Array(EncodedU64Array::U64(vec![10, 5, 1]));
|
|
assert!(segment.contains(1), "Should contain 1");
|
|
assert!(segment.contains(5), "Should contain 5");
|
|
assert!(segment.contains(10), "Should contain 10");
|
|
assert!(!segment.contains(0), "Should not contain 0");
|
|
assert!(!segment.contains(3), "Should not contain 3");
|
|
assert!(!segment.contains(15), "Should not contain 15");
|
|
|
|
// Test empty segment
|
|
let segment = U64Segment::Range(0..0);
|
|
assert!(
|
|
!segment.contains(0),
|
|
"Empty segment should not contain anything"
|
|
);
|
|
assert!(
|
|
!segment.contains(5),
|
|
"Empty segment should not contain anything"
|
|
);
|
|
}
|
|
}
|