use gnitz_wire::{image_mask, KeyRange};
use crate::{BatchView, ColumnLocator, ExprValidateErr, SchemaFacts};
pub(crate) struct RangeMembership {
null_mask: u64,
bounds: Vec<(ColumnLocator, u128, u128)>,
}
impl RangeMembership {
pub(crate) fn new(range: &KeyRange, schema: &dyn SchemaFacts) -> Result<Self, ExprValidateErr> {
let bad = |e: String| ExprValidateErr::BadWalk(format!("range walk: {e}"));
let cols = range.cols();
let n = schema.num_columns();
if let Some(&c) = cols.as_slice().iter().find(|&&c| c as usize >= n) {
return Err(bad(format!("column {c} is out of range for a {n}-column schema")));
}
let locs: Vec<ColumnLocator> = cols.as_slice().iter().map(|&c| schema.locate(c as usize)).collect();
if let Some(l) = locs.iter().find(|l| !l.type_code().is_pk_eligible()) {
return Err(bad(format!("column type {} has no key order", l.type_code())));
}
let range_col = locs[range.eq_vals().len()];
let null_mask = locs.iter().fold(0, |m, l| match *l {
ColumnLocator::Payload { slot, .. } => m | 1 << slot,
ColumnLocator::Pk { .. } => m,
});
let mask = image_mask(range_col.size());
let (start, end) = (range.start.image & mask, range.end.image & mask);
let lo = if range.start.after {
start.checked_add(1)
} else {
Some(start)
};
let hi = if range.end.after { Some(end) } else { end.checked_sub(1) };
let bounds = match lo.zip(hi).filter(|(lo, hi)| lo <= hi) {
Some((lo, hi)) => locs
.iter()
.zip(range.eq_vals())
.map(|(&l, &v)| (l, v & image_mask(l.size()), 0))
.chain([(range_col, lo, hi - lo)])
.collect(),
None => Vec::new(),
};
Ok(RangeMembership { null_mask, bounds })
}
pub(crate) fn and_into(&self, mb: &dyn BatchView, words: &mut [u64]) {
debug_assert_eq!(words.len(), mb.row_count().div_ceil(64), "and_into: one bit per row");
if self.bounds.is_empty() {
words.fill(0);
return;
}
if self.null_mask != 0 {
let level = crate::simd::level();
for (words, rows) in words.chunks_mut(4).zip(mb.null_bmp().chunks(4 * 64 * 8)) {
let mut nulls = [0u64; 4];
crate::simd::null_bits(level, rows, self.null_mask, &mut nulls[..words.len()]);
for (w, n) in words.iter_mut().zip(nulls) {
*w &= !n;
}
}
}
for &(loc, lo, span) in &self.bounds {
match loc.size() {
1 => and_column::<u8, 1>(words, mb, loc, lo, span),
2 => and_column::<u16, 2>(words, mb, loc, lo, span),
4 => and_column::<u32, 4>(words, mb, loc, lo, span),
8 => and_column::<u64, 8>(words, mb, loc, lo, span),
16 => and_column::<u128, 16>(words, mb, loc, lo, span),
w => unreachable!("RangeMembership: a key column is 1/2/4/8/16 bytes, not {w}"),
}
}
}
}
trait KeyCell<const W: usize>: Copy + PartialOrd {
fn from_le(b: [u8; W]) -> Self;
fn from_be(b: [u8; W]) -> Self;
fn low(v: u128) -> Self;
fn sub(self, o: Self) -> Self;
}
macro_rules! key_cell {
($($t:ty),*) => {$(
impl KeyCell<{ size_of::<$t>() }> for $t {
fn from_le(b: [u8; size_of::<$t>()]) -> Self { <$t>::from_le_bytes(b) }
fn from_be(b: [u8; size_of::<$t>()]) -> Self { <$t>::from_be_bytes(b) }
fn low(v: u128) -> Self { v as $t }
fn sub(self, o: Self) -> Self { self.wrapping_sub(o) }
}
)*};
}
key_cell!(u8, u16, u32, u64, u128);
fn and_column<C: KeyCell<W>, const W: usize>(
words: &mut [u64],
mb: &dyn BatchView,
loc: ColumnLocator,
lo: u128,
span: u128,
) {
let span = C::low(span);
match loc {
ColumnLocator::Pk { byte_off, .. } => {
let (region, stride) = mb.pk_region();
let lo = C::low(lo);
and_cells(words, region, stride, byte_off as usize, |c| {
C::from_be(c).sub(lo) <= span
});
}
ColumnLocator::Payload { slot, type_code, .. } => {
let col = mb.col_data(slot as usize, W);
let lo = C::low(lo.wrapping_sub(gnitz_wire::opk_bias(type_code)));
and_cells(words, col, W, 0, |c| C::from_le(c).sub(lo) <= span);
}
}
}
#[inline(always)]
fn and_cells<const W: usize>(
words: &mut [u64],
region: &[u8],
stride: usize,
off: usize,
keep: impl Fn([u8; W]) -> bool,
) {
for (word, rows) in words.iter_mut().zip(region.chunks(64 * stride)) {
let mut bits = 0u64;
for (i, row) in rows.chunks_exact(stride).enumerate() {
bits |= (keep(row[off..off + W].try_into().unwrap()) as u64) << i;
}
*word &= bits;
}
}
#[cfg(test)]
#[path = "tests/range.rs"]
mod tests;