use alloc::vec::Vec;
use crate::util::varint;
#[cfg(test)]
pub(crate) static INDEX_ROUTE_HITS: core::sync::atomic::AtomicUsize =
core::sync::atomic::AtomicUsize::new(0);
const MAIN_PREFIX: u8 = b'0';
pub(crate) const AVERAGES_ROWID: i64 = 1;
pub(crate) const STRUCTURE_ROWID: i64 = 10;
pub(crate) fn segment_leaf_rowid(segid: i64, pgno: i64) -> i64 {
(segid << 37) | pgno
}
fn put_varint(out: &mut Vec<u8>, v: u64) {
let mut buf = [0u8; varint::MAX_LEN];
let n = varint::encode(v, &mut buf);
out.extend_from_slice(&buf[..n]);
}
pub(crate) struct Posting {
pub rowid: i64,
pub cols: Vec<Vec<u32>>,
}
fn collist(positions: &[u32]) -> Vec<u8> {
let mut out = Vec::new();
let mut prev = 0u32;
for (i, &pos) in positions.iter().enumerate() {
put_varint(
&mut out,
((if i == 0 { pos } else { pos - prev }) as u64) + 2,
);
prev = pos;
}
out
}
fn poslist(p: &Posting) -> Vec<u8> {
let mut content = Vec::new();
for (c, positions) in p.cols.iter().enumerate() {
if positions.is_empty() {
continue;
}
if c != 0 {
content.push(0x01);
put_varint(&mut content, c as u64);
}
content.extend_from_slice(&collist(positions));
}
let mut out = Vec::new();
put_varint(&mut out, (content.len() as u64) * 2);
out.extend_from_slice(&content);
out
}
fn doclist(postings: &[Posting]) -> Vec<u8> {
let mut out = Vec::new();
let mut prev = 0i64;
for (i, p) in postings.iter().enumerate() {
put_varint(
&mut out,
(if i == 0 { p.rowid } else { p.rowid - prev }) as u64,
);
out.extend_from_slice(&poslist(p));
prev = p.rowid;
}
out
}
fn term_key(term: &[u8]) -> Vec<u8> {
let mut key = Vec::with_capacity(term.len() + 1);
key.push(MAIN_PREFIX);
key.extend_from_slice(term);
key
}
fn pgidx(offsets: &[usize]) -> Vec<u8> {
let mut out = Vec::new();
let mut prev = 0usize;
for (i, &off) in offsets.iter().enumerate() {
put_varint(&mut out, (if i == 0 { off } else { off - prev }) as u64);
prev = off;
}
out
}
fn separator(prev_last: &[u8], first: &[u8]) -> Vec<u8> {
let mut i = 0;
while i < prev_last.len() && i < first.len() && prev_last[i] == first[i] {
i += 1;
}
first[..=i.min(first.len() - 1)].to_vec()
}
pub(crate) struct IdxRow {
pub segid: i64,
pub term: Vec<u8>,
pub pgno: i64,
}
struct SegWriter {
pgsz: usize,
segid: i64,
leaves: Vec<Vec<u8>>,
idx: Vec<IdxRow>,
body: Vec<u8>,
term_offsets: Vec<usize>,
first_rowid_off: usize,
prev_term_key: Option<Vec<u8>>,
prev_rowid: i64,
leaf_first_term: Option<Vec<u8>>,
leaf_last_term: Option<Vec<u8>>,
prev_leaf_last_term: Option<Vec<u8>>,
pgno: i64,
}
impl SegWriter {
fn new(pgsz: usize, segid: i64) -> Self {
SegWriter {
pgsz,
segid,
leaves: Vec::new(),
idx: Vec::new(),
body: Vec::new(),
term_offsets: Vec::new(),
first_rowid_off: 0,
prev_term_key: None,
prev_rowid: 0,
leaf_first_term: None,
leaf_last_term: None,
prev_leaf_last_term: None,
pgno: 1,
}
}
fn leaf_size(&self) -> usize {
4 + self.body.len() + pgidx(&self.term_offsets).len()
}
fn finish_leaf(&self) -> Vec<u8> {
let footer_off = 4 + self.body.len();
let mut leaf = Vec::new();
leaf.extend_from_slice(&(self.first_rowid_off as u16).to_be_bytes());
leaf.extend_from_slice(&(footer_off as u16).to_be_bytes());
leaf.extend_from_slice(&self.body);
leaf.extend_from_slice(&pgidx(&self.term_offsets));
leaf
}
fn flush(&mut self) {
self.leaves.push(self.finish_leaf());
if let Some(ft) = self.leaf_first_term.take() {
let term = match &self.prev_leaf_last_term {
Some(p) => separator(p, &ft),
None => Vec::new(),
};
self.idx.push(IdxRow {
segid: self.segid,
term,
pgno: self.pgno << 1,
});
}
if let Some(lt) = self.leaf_last_term.take() {
self.prev_leaf_last_term = Some(lt);
}
self.body.clear();
self.term_offsets.clear();
self.first_rowid_off = 0;
self.prev_term_key = None;
self.prev_rowid = 0;
self.pgno += 1;
}
fn term_record(&self, key: &[u8]) -> Vec<u8> {
let mut rec = Vec::new();
match &self.prev_term_key {
None => {
put_varint(&mut rec, key.len() as u64);
rec.extend_from_slice(key);
}
Some(prev) => {
let n_common = key
.iter()
.zip(prev.iter())
.take_while(|(a, b)| a == b)
.count();
put_varint(&mut rec, n_common as u64);
put_varint(&mut rec, (key.len() - n_common) as u64);
rec.extend_from_slice(&key[n_common..]);
}
}
rec
}
fn pgidx_with(&self) -> usize {
let mut probe = self.term_offsets.clone();
probe.push(4 + self.body.len());
pgidx(&probe).len()
}
fn add_term(&mut self, term: &[u8], postings: &[Posting]) {
let key = term_key(term);
let dl = doclist(postings);
let rec = self.term_record(&key);
if !self.body.is_empty()
&& 4 + self.body.len() + rec.len() + dl.len() + self.pgidx_with() >= self.pgsz
{
self.flush();
}
let rec = self.term_record(&key);
let fits_whole =
4 + self.body.len() + rec.len() + dl.len() + self.pgidx_with() <= self.pgsz;
self.term_offsets.push(4 + self.body.len());
if self.leaf_first_term.is_none() {
self.leaf_first_term = Some(key.clone());
}
self.leaf_last_term = Some(key.clone());
self.body.extend_from_slice(&rec);
self.prev_term_key = Some(key);
if fits_whole {
self.body.extend_from_slice(&dl);
return;
}
self.prev_rowid = 0;
for p in postings {
if self.leaf_size() > self.pgsz && !self.body.is_empty() {
self.flush();
}
if self.term_offsets.is_empty() && self.first_rowid_off == 0 {
self.first_rowid_off = 4 + self.body.len();
}
let pl = poslist(p);
let size_len = varint::decode(&pl).map(|(_, n)| n).unwrap_or(1);
put_varint(&mut self.body, (p.rowid - self.prev_rowid) as u64);
self.body.extend_from_slice(&pl[..size_len]);
self.prev_rowid = p.rowid;
for &b in &pl[size_len..] {
if self.leaf_size() >= self.pgsz {
self.flush();
}
self.body.push(b);
}
}
}
fn finish(mut self) -> (Vec<Vec<u8>>, Vec<IdxRow>) {
self.flush();
(self.leaves, self.idx)
}
}
fn structure(n_leaves: i64, cookie: u32) -> Vec<u8> {
let mut out = cookie.to_be_bytes().to_vec();
if n_leaves == 0 {
out.extend_from_slice(&[0, 0, 0]); return out;
}
for v in [1, 1, n_leaves as u64, 0, 1, 1, 1, n_leaves as u64] {
put_varint(&mut out, v);
}
out
}
pub(crate) struct Segment {
pub data: Vec<(i64, Vec<u8>)>,
pub idx: Vec<IdxRow>,
pub docsize: Vec<(i64, Vec<u8>)>,
}
pub(crate) fn build_segment(
terms: &[(Vec<u8>, Vec<Posting>)],
n_docs: u64,
col_totals: &[u64],
doc_sizes: &[(i64, Vec<u64>)],
pgsz: usize,
cookie: u32,
) -> Segment {
let segid = 1;
let (leaves, idx) = {
let mut w = SegWriter::new(pgsz.max(16), segid);
for (term, postings) in terms {
w.add_term(term, postings);
}
if terms.is_empty() {
(Vec::new(), Vec::new())
} else {
w.finish()
}
};
let mut data: Vec<(i64, Vec<u8>)> = Vec::new();
let mut avg = Vec::new();
if n_docs > 0 {
put_varint(&mut avg, n_docs);
for &t in col_totals {
put_varint(&mut avg, t);
}
}
data.push((AVERAGES_ROWID, avg));
data.push((STRUCTURE_ROWID, structure(leaves.len() as i64, cookie)));
for (i, leaf) in leaves.iter().enumerate() {
data.push((segment_leaf_rowid(segid, i as i64 + 1), leaf.clone()));
}
let docsize = doc_sizes
.iter()
.map(|(rowid, sizes)| {
let mut sz = Vec::new();
for &s in sizes {
put_varint(&mut sz, s);
}
(*rowid, sz)
})
.collect();
Segment { data, idx, docsize }
}
fn read_varint(buf: &[u8], pos: &mut usize) -> Option<u64> {
let (v, n) = varint::decode(buf.get(*pos..)?)?;
*pos += n;
Some(v)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DecodedPosting {
pub rowid: i64,
pub cols: Vec<Vec<u32>>,
}
fn decode_poslist(buf: &[u8], pos: &mut usize) -> Option<Vec<Vec<u32>>> {
let size2 = read_varint(buf, pos)?;
if size2 & 1 != 0 {
return None;
}
let content_len = (size2 / 2) as usize;
let end = pos.checked_add(content_len)?;
if end > buf.len() {
return None;
}
let mut cols: Vec<Vec<u32>> = Vec::new();
let mut col = 0usize;
let mut p = *pos;
cols.push(Vec::new());
while p < end {
if buf[p] == 0x01 {
p += 1;
let c = read_varint(buf, &mut p)? as usize;
col = c;
while cols.len() <= col {
cols.push(Vec::new());
}
} else {
let raw = read_varint(buf, &mut p)?;
if raw < 2 {
return None;
}
let delta = (raw - 2) as u32;
let next = if cols[col].is_empty() {
delta
} else {
cols[col].last().copied()?.checked_add(delta)?
};
cols[col].push(next);
}
}
*pos = end;
Some(cols)
}
struct TermRec {
key: Vec<u8>,
rec_start: usize,
doclist_start: usize,
}
struct LeafView {
first_rowid_off: usize,
footer_off: usize,
terms: Vec<TermRec>,
}
fn parse_leaf(leaf: &[u8]) -> Option<LeafView> {
if leaf.len() < 4 {
return None;
}
let first_rowid_off = u16::from_be_bytes([leaf[0], leaf[1]]) as usize;
let footer_off = u16::from_be_bytes([leaf[2], leaf[3]]) as usize;
if footer_off < 4 || footer_off > leaf.len() {
return None;
}
if first_rowid_off != 0 && (first_rowid_off < 4 || first_rowid_off > footer_off) {
return None;
}
let mut term_offs: Vec<usize> = Vec::new();
{
let mut p = footer_off;
let mut prev = 0usize;
let mut first = true;
while p < leaf.len() {
let d = read_varint(leaf, &mut p)? as usize;
let off = if first { d } else { prev.checked_add(d)? };
first = false;
if off >= footer_off || off < 4 {
return None;
}
term_offs.push(off);
prev = off;
}
}
let mut terms = Vec::with_capacity(term_offs.len());
let mut prev_key: Vec<u8> = Vec::new();
for (i, &off) in term_offs.iter().enumerate() {
let mut p = off;
let key = if i == 0 {
let keylen = read_varint(leaf, &mut p)? as usize;
let end = p.checked_add(keylen)?;
if end > footer_off {
return None;
}
let key = leaf.get(p..end)?.to_vec();
p = end;
key
} else {
let n_common = read_varint(leaf, &mut p)? as usize;
let n_new = read_varint(leaf, &mut p)? as usize;
let end = p.checked_add(n_new)?;
if end > footer_off || n_common > prev_key.len() {
return None;
}
let mut key = prev_key.get(..n_common)?.to_vec();
key.extend_from_slice(leaf.get(p..end)?);
p = end;
key
};
if p > footer_off {
return None;
}
terms.push(TermRec {
key: key.clone(),
rec_start: off,
doclist_start: p,
});
prev_key = key;
}
Some(LeafView {
first_rowid_off,
footer_off,
terms,
})
}
struct DoclistRun<'a> {
bytes: &'a [u8],
abs_start: bool,
}
fn decode_spanning_doclist(runs: &[DoclistRun]) -> Option<Vec<DecodedPosting>> {
let mut buf: Vec<u8> = Vec::new();
let mut abs_at: Vec<usize> = Vec::new();
for run in runs {
if run.abs_start && !run.bytes.is_empty() {
abs_at.push(buf.len());
}
buf.extend_from_slice(run.bytes);
}
let end = buf.len();
let mut pos = 0usize;
let mut out = Vec::new();
let mut rowid = 0i64;
let mut first = true;
while pos < end {
let absolute = first || abs_at.contains(&pos);
let d = read_varint(&buf, &mut pos)? as i64;
rowid = if absolute { d } else { rowid.wrapping_add(d) };
first = false;
let cols = decode_poslist(&buf, &mut pos)?;
if pos > end {
return None;
}
out.push(DecodedPosting { rowid, cols });
}
if pos != end {
return None;
}
Some(out)
}
fn gather_doclist_runs<'a>(
leaves: &'a [&'a [u8]],
start_leaf: usize,
start_ti: usize,
start_off: usize,
leaf_views: &[LeafView],
) -> Option<Vec<DoclistRun<'a>>> {
let mut runs: Vec<DoclistRun<'a>> = Vec::new();
let first_view = &leaf_views[start_leaf];
let first_next_term = first_view.terms.get(start_ti + 1).map(|r| r.rec_start);
let first_end = first_next_term.unwrap_or(first_view.footer_off);
if first_end < start_off || first_end > first_view.footer_off {
return None;
}
runs.push(DoclistRun {
bytes: leaves[start_leaf].get(start_off..first_end)?,
abs_start: true, });
if first_next_term.is_some() {
return Some(runs); }
let mut li = start_leaf + 1;
while li < leaves.len() {
let view = &leaf_views[li];
let next_term = view.terms.first().map(|r| r.rec_start);
let boundary = next_term.unwrap_or(view.footer_off);
let tail_end = if view.first_rowid_off == 0 {
boundary
} else {
view.first_rowid_off
};
if tail_end < 4 || tail_end > boundary || boundary > view.footer_off {
return None;
}
runs.push(DoclistRun {
bytes: leaves[li].get(4..tail_end)?,
abs_start: false,
});
if view.first_rowid_off != 0 {
runs.push(DoclistRun {
bytes: leaves[li].get(view.first_rowid_off..boundary)?,
abs_start: true,
});
}
if next_term.is_some() {
break;
}
li += 1;
}
Some(runs)
}
#[cfg(test)]
pub(crate) fn decode_term(leaves: &[&[u8]], term: &[u8]) -> Option<Vec<DecodedPosting>> {
let key = term_key(term);
let mut views: Vec<LeafView> = Vec::with_capacity(leaves.len());
for leaf in leaves {
views.push(parse_leaf(leaf)?);
}
for (li, view) in views.iter().enumerate() {
for (ti, rec) in view.terms.iter().enumerate() {
if rec.key != key {
continue;
}
let runs = gather_doclist_runs(leaves, li, ti, rec.doclist_start, &views)?;
return decode_spanning_doclist(&runs);
}
}
None
}
fn decode_prefix_strict(leaves: &[&[u8]], prefix: &[u8]) -> SegDecode {
let want = term_key(prefix);
let mut views: Vec<LeafView> = Vec::with_capacity(leaves.len());
for leaf in leaves {
match parse_leaf(leaf) {
Some(v) => views.push(v),
None => return SegDecode::Bail,
}
}
let mut out: Vec<DecodedPosting> = Vec::new();
for (li, view) in views.iter().enumerate() {
for (ti, rec) in view.terms.iter().enumerate() {
if !rec.key.starts_with(&want) {
continue;
}
match gather_doclist_runs(leaves, li, ti, rec.doclist_start, &views)
.and_then(|runs| decode_spanning_doclist(&runs))
{
Some(postings) => out.extend(postings),
None => return SegDecode::Bail, }
}
}
SegDecode::Postings(out)
}
struct SegmentLoc {
segid: i64,
pgno_first: i64,
pgno_last: i64,
}
fn all_segments(structure: &[u8]) -> Option<Vec<SegmentLoc>> {
let mut pos = 4usize; let n_level = read_varint(structure, &mut pos)?;
let n_segment = read_varint(structure, &mut pos)?;
let _n_write_counter = read_varint(structure, &mut pos)?;
if n_level == 0 || n_segment == 0 {
return None; }
let mut segs: Vec<SegmentLoc> = Vec::new();
for _ in 0..n_level {
let _n_merge = read_varint(structure, &mut pos)?;
let n_seg = read_varint(structure, &mut pos)?;
for _ in 0..n_seg {
let segid = read_varint(structure, &mut pos)? as i64;
let pgno_first = read_varint(structure, &mut pos)? as i64;
let pgno_last = read_varint(structure, &mut pos)? as i64;
if segid <= 0 || pgno_first < 1 || pgno_last < pgno_first {
return None;
}
segs.push(SegmentLoc {
segid,
pgno_first,
pgno_last,
});
}
}
if segs.len() as u64 != n_segment {
return None;
}
Some(segs)
}
fn segments_leaves(data: &[(i64, Vec<u8>)]) -> Option<Vec<Vec<&[u8]>>> {
let structure = &data.iter().find(|(id, _)| *id == STRUCTURE_ROWID)?.1;
let locs = all_segments(structure)?;
let mut out: Vec<Vec<&[u8]>> = Vec::with_capacity(locs.len());
for loc in &locs {
let mut leaves: Vec<&[u8]> = Vec::new();
for pgno in loc.pgno_first..=loc.pgno_last {
let rid = segment_leaf_rowid(loc.segid, pgno);
let blob = &data.iter().find(|(id, _)| *id == rid)?.1;
leaves.push(blob.as_slice());
}
out.push(leaves);
}
#[cfg(test)]
INDEX_ROUTE_HITS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
Some(out)
}
enum SegDecode {
Bail,
Postings(Vec<DecodedPosting>),
}
fn decode_term_strict(leaves: &[&[u8]], term: &[u8]) -> SegDecode {
let key = term_key(term);
let mut views: Vec<LeafView> = Vec::with_capacity(leaves.len());
for leaf in leaves {
match parse_leaf(leaf) {
Some(v) => views.push(v),
None => return SegDecode::Bail, }
}
for (li, view) in views.iter().enumerate() {
for (ti, rec) in view.terms.iter().enumerate() {
if rec.key != key {
continue;
}
return match gather_doclist_runs(leaves, li, ti, rec.doclist_start, &views)
.and_then(|runs| decode_spanning_doclist(&runs))
{
Some(postings) => SegDecode::Postings(postings),
None => SegDecode::Bail,
};
}
}
SegDecode::Postings(Vec::new()) }
fn merge_segments(
data: &[(i64, Vec<u8>)],
decode: impl Fn(&[&[u8]]) -> SegDecode,
) -> Option<Vec<DecodedPosting>> {
let segments = segments_leaves(data)?;
let mut all: Vec<DecodedPosting> = Vec::new();
let mut seen_segidx: Vec<(i64, usize)> = Vec::new(); for (si, leaves) in segments.iter().enumerate() {
match decode(leaves) {
SegDecode::Bail => return None,
SegDecode::Postings(postings) => {
for p in postings {
seen_segidx.push((p.rowid, si));
all.push(p);
}
}
}
}
seen_segidx.sort_unstable();
for w in seen_segidx.windows(2) {
if w[0].0 == w[1].0 && w[0].1 != w[1].1 {
return None;
}
}
all.sort_by_key(|p| p.rowid);
all.dedup_by_key(|p| p.rowid);
Some(all)
}
pub(crate) fn lookup_term_rowids(data: &[(i64, Vec<u8>)], term: &[u8]) -> Option<Vec<i64>> {
decode_term_in_data(data, term).map(|postings| postings.into_iter().map(|p| p.rowid).collect())
}
pub(crate) fn lookup_term_rowids_in_column(
data: &[(i64, Vec<u8>)],
term: &[u8],
column: usize,
) -> Option<Vec<i64>> {
decode_term_in_data(data, term).map(|postings| {
postings
.into_iter()
.filter(|p| p.cols.get(column).is_some_and(|c| !c.is_empty()))
.map(|p| p.rowid)
.collect()
})
}
fn prefix_rowids(
mut postings: Vec<DecodedPosting>,
keep: impl Fn(&DecodedPosting) -> bool,
) -> Vec<i64> {
let mut rowids: Vec<i64> = postings
.drain(..)
.filter(|p| keep(p))
.map(|p| p.rowid)
.collect();
rowids.sort_unstable();
rowids.dedup();
rowids
}
pub(crate) fn lookup_prefix_rowids(data: &[(i64, Vec<u8>)], prefix: &[u8]) -> Option<Vec<i64>> {
let postings = merge_segments(data, |leaves| decode_prefix_strict(leaves, prefix))?;
Some(prefix_rowids(postings, |_| true))
}
pub(crate) fn lookup_prefix_rowids_in_column(
data: &[(i64, Vec<u8>)],
prefix: &[u8],
column: usize,
) -> Option<Vec<i64>> {
let postings = merge_segments(data, |leaves| decode_prefix_strict(leaves, prefix))?;
Some(prefix_rowids(postings, |p| {
p.cols.get(column).is_some_and(|c| !c.is_empty())
}))
}
fn decode_term_in_data(data: &[(i64, Vec<u8>)], term: &[u8]) -> Option<Vec<DecodedPosting>> {
merge_segments(data, |leaves| decode_term_strict(leaves, term))
}
fn col(p: &DecodedPosting, c: usize) -> &[u32] {
p.cols.get(c).map(Vec::as_slice).unwrap_or(&[])
}
fn phrase_intersect(
postings_a: &[DecodedPosting],
postings_b: &[DecodedPosting],
adj: impl Fn(&DecodedPosting, &DecodedPosting) -> bool,
) -> Vec<i64> {
let mut out = Vec::new();
let (mut i, mut j) = (0usize, 0usize);
while i < postings_a.len() && j < postings_b.len() {
let (a, b) = (&postings_a[i], &postings_b[j]);
match a.rowid.cmp(&b.rowid) {
core::cmp::Ordering::Less => i += 1,
core::cmp::Ordering::Greater => j += 1,
core::cmp::Ordering::Equal => {
if adj(a, b) {
out.push(a.rowid);
}
i += 1;
j += 1;
}
}
}
out
}
fn phrase_run_matches(cols: &[Vec<&[u32]>], column: Option<usize>) -> bool {
debug_assert!(!cols.is_empty());
let k = cols.len();
let ncols = cols.iter().map(Vec::len).max().unwrap_or(0);
let in_col = |c: usize| -> bool {
let first = cols[0].get(c).copied().unwrap_or(&[]);
first.iter().any(|&p| {
(1..k).all(|i| {
let want = match p.checked_add(i as u32) {
Some(w) => w,
None => return false,
};
let list = cols[i].get(c).copied().unwrap_or(&[]);
list.binary_search(&want).is_ok()
})
})
};
match column {
Some(c) => in_col(c),
None => (0..ncols).any(in_col),
}
}
fn phrase_intersect_k(postings: &[Vec<DecodedPosting>], column: Option<usize>) -> Vec<i64> {
let k = postings.len();
if k == 0 || postings.iter().any(Vec::is_empty) {
return Vec::new();
}
let mut out = Vec::new();
let mut idx = alloc::vec![0usize; k];
loop {
let mut min_rowid = i64::MAX;
for (i, p) in postings.iter().enumerate() {
if idx[i] >= p.len() {
return out;
}
min_rowid = min_rowid.min(p[idx[i]].rowid);
}
let all_equal = postings
.iter()
.zip(&idx)
.all(|(p, &j)| p[j].rowid == min_rowid);
if all_equal {
let cols: Vec<Vec<&[u32]>> = postings
.iter()
.zip(&idx)
.map(|(p, &j)| p[j].cols.iter().map(Vec::as_slice).collect())
.collect();
if phrase_run_matches(&cols, column) {
out.push(min_rowid);
}
}
for (i, p) in postings.iter().enumerate() {
if p[idx[i]].rowid == min_rowid {
idx[i] += 1;
}
}
}
}
fn decode_terms_multiseg(
data: &[(i64, Vec<u8>)],
terms: &[&[u8]],
) -> Option<Vec<Vec<DecodedPosting>>> {
let segments = segments_leaves(data)?;
let mut per_term: Vec<Vec<DecodedPosting>> = alloc::vec![Vec::new(); terms.len()];
let mut seen_segidx: Vec<(i64, usize)> = Vec::new();
for (si, leaves) in segments.iter().enumerate() {
for (ti, term) in terms.iter().enumerate() {
match decode_term_strict(leaves, term) {
SegDecode::Bail => return None,
SegDecode::Postings(postings) => {
for p in postings {
seen_segidx.push((p.rowid, si));
per_term[ti].push(p);
}
}
}
}
}
seen_segidx.sort_unstable();
for w in seen_segidx.windows(2) {
if w[0].0 == w[1].0 && w[0].1 != w[1].1 {
return None;
}
}
for postings in &mut per_term {
postings.sort_by_key(|p| p.rowid);
}
Some(per_term)
}
pub(crate) fn lookup_phrase_rowids_k(data: &[(i64, Vec<u8>)], terms: &[&[u8]]) -> Option<Vec<i64>> {
if terms.len() < 2 {
return None;
}
let postings = decode_terms_multiseg(data, terms)?;
Some(phrase_intersect_k(&postings, None))
}
pub(crate) fn lookup_phrase_rowids_in_column_k(
data: &[(i64, Vec<u8>)],
terms: &[&[u8]],
column: usize,
) -> Option<Vec<i64>> {
if terms.len() < 2 {
return None;
}
let postings = decode_terms_multiseg(data, terms)?;
Some(phrase_intersect_k(&postings, Some(column)))
}
fn near_within_in_column(pa: &[u32], pb: &[u32], n: u32) -> bool {
let limit = n.saturating_add(1);
let (mut i, mut j) = (0usize, 0usize);
while i < pa.len() && j < pb.len() {
let (a, b) = (pa[i], pb[j]);
let gap = a.abs_diff(b);
if gap <= limit {
return true;
}
if a < b {
i += 1;
} else {
j += 1;
}
}
false
}
fn near_matches(a: &DecodedPosting, b: &DecodedPosting, n: u32) -> bool {
let ncols = a.cols.len().max(b.cols.len());
(0..ncols).any(|c| near_within_in_column(col(a, c), col(b, c), n))
}
pub(crate) fn lookup_near_rowids(
data: &[(i64, Vec<u8>)],
term_a: &[u8],
term_b: &[u8],
n: u32,
) -> Option<Vec<i64>> {
let terms: [&[u8]; 2] = [term_a, term_b];
let postings = decode_terms_multiseg(data, &terms)?;
Some(phrase_intersect(&postings[0], &postings[1], |a, b| {
near_matches(a, b, n)
}))
}
fn rowids_intersect(a: &[i64], b: &[i64]) -> Vec<i64> {
let mut out = Vec::new();
let (mut i, mut j) = (0usize, 0usize);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
core::cmp::Ordering::Less => i += 1,
core::cmp::Ordering::Greater => j += 1,
core::cmp::Ordering::Equal => {
out.push(a[i]);
i += 1;
j += 1;
}
}
}
out
}
fn rowids_union(a: &[i64], b: &[i64]) -> Vec<i64> {
let mut out = Vec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0usize, 0usize);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
core::cmp::Ordering::Less => {
out.push(a[i]);
i += 1;
}
core::cmp::Ordering::Greater => {
out.push(b[j]);
j += 1;
}
core::cmp::Ordering::Equal => {
out.push(a[i]);
i += 1;
j += 1;
}
}
}
out.extend_from_slice(&a[i..]);
out.extend_from_slice(&b[j..]);
out
}
fn rowids_difference(a: &[i64], b: &[i64]) -> Vec<i64> {
let mut out = Vec::new();
let (mut i, mut j) = (0usize, 0usize);
while i < a.len() {
if j >= b.len() {
out.extend_from_slice(&a[i..]);
break;
}
match a[i].cmp(&b[j]) {
core::cmp::Ordering::Less => {
out.push(a[i]);
i += 1;
}
core::cmp::Ordering::Greater => j += 1,
core::cmp::Ordering::Equal => {
i += 1;
j += 1;
}
}
}
out
}
#[cfg(feature = "fts5")]
fn eval_bool_tree(data: &[(i64, Vec<u8>)], tree: &crate::vtab::Fts5BoolTree) -> Option<Vec<i64>> {
use crate::vtab::{Fts5BoolOp, Fts5BoolTree};
match tree {
Fts5BoolTree::Leaf(term) => Some(
decode_term_in_data(data, term)?
.into_iter()
.map(|p| p.rowid)
.collect(),
),
Fts5BoolTree::Op(op, a, b) => {
let ra = eval_bool_tree(data, a)?;
let rb = eval_bool_tree(data, b)?;
Some(match op {
Fts5BoolOp::And => rowids_intersect(&ra, &rb),
Fts5BoolOp::Or => rowids_union(&ra, &rb),
Fts5BoolOp::Not => rowids_difference(&ra, &rb),
})
}
}
}
pub(crate) fn lookup_bool_tree_rowids(
data: &[(i64, Vec<u8>)],
tree: &crate::vtab::Fts5BoolTree,
) -> Option<Vec<i64>> {
eval_bool_tree(data, tree)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{format, string::ToString, vec};
fn p(rowid: i64, cols: &[&[u32]]) -> Posting {
Posting {
rowid,
cols: cols.iter().map(|c| c.to_vec()).collect(),
}
}
#[test]
fn empty_table_structure_and_averages() {
let seg = build_segment(&[], 0, &[0], &[], 1000, 0);
assert_eq!(seg.data[0], (AVERAGES_ROWID, Vec::new()));
assert_eq!(seg.data[1], (STRUCTURE_ROWID, vec![0, 0, 0, 0, 0, 0, 0]));
assert_eq!(seg.data.len(), 2); assert!(seg.idx.is_empty());
}
#[test]
fn single_term_single_doc_matches_known_bytes() {
let terms = vec![(b"a".to_vec(), vec![p(1, &[&[0]])])];
let seg = build_segment(&terms, 1, &[1], &[(1, vec![1])], 1000, 0);
let leaf = &seg
.data
.iter()
.find(|(id, _)| *id == segment_leaf_rowid(1, 1))
.unwrap()
.1;
assert_eq!(
leaf,
&vec![0, 0, 0, 0x0A, 0x02, 0x30, 0x61, 0x01, 0x02, 0x02, 0x04]
);
assert_eq!(seg.data[0].1, vec![0x01, 0x01]);
assert_eq!(seg.idx.len(), 1);
assert_eq!(seg.idx[0].pgno, 2);
assert!(seg.idx[0].term.is_empty());
assert_eq!(seg.docsize, vec![(1, vec![1])]);
}
#[test]
fn multi_column_poslist_bytes() {
let terms = vec![("hello".to_string().into_bytes(), vec![p(1, &[&[0], &[0]])])];
let seg = build_segment(&terms, 1, &[1, 1], &[(1, vec![1, 1])], 1000, 0);
let leaf = &seg
.data
.iter()
.find(|(id, _)| *id == segment_leaf_rowid(1, 1))
.unwrap()
.1;
let expected = vec![
0, 0, 0, 0x11, 0x06, 0x30, b'h', b'e', b'l', b'l', b'o', 0x01, 0x08, 0x02, 0x01, 0x01,
0x02, 0x04,
];
assert_eq!(leaf, &expected);
}
fn leaves_of(seg: &Segment) -> Vec<Vec<u8>> {
let mut out = Vec::new();
let mut pgno = 1i64;
loop {
let rid = segment_leaf_rowid(1, pgno);
match seg.data.iter().find(|(id, _)| *id == rid) {
Some((_, blob)) => out.push(blob.clone()),
None => break,
}
pgno += 1;
}
out
}
fn decode(seg: &Segment, term: &[u8]) -> Option<Vec<DecodedPosting>> {
let leaves = leaves_of(seg);
let refs: Vec<&[u8]> = leaves.iter().map(|l| l.as_slice()).collect();
decode_term(&refs, term)
}
fn dp(rowid: i64, cols: &[&[u32]]) -> DecodedPosting {
DecodedPosting {
rowid,
cols: cols.iter().map(|c| c.to_vec()).collect(),
}
}
#[test]
fn decode_single_term_single_doc() {
let terms = vec![(b"a".to_vec(), vec![p(1, &[&[0]])])];
let seg = build_segment(&terms, 1, &[1], &[(1, vec![1])], 1000, 0);
assert_eq!(decode(&seg, b"a"), Some(vec![dp(1, &[&[0]])]));
assert_eq!(decode(&seg, b"z"), None);
assert_eq!(decode(&seg, b""), None);
}
#[test]
fn decode_multi_doc_rowid_deltas() {
let terms = vec![(
b"cat".to_vec(),
vec![p(1, &[&[0]]), p(3, &[&[2]]), p(7, &[&[1]])],
)];
let seg = build_segment(
&terms,
3,
&[3],
&[(1, vec![1]), (3, vec![3]), (7, vec![2])],
1000,
0,
);
assert_eq!(
decode(&seg, b"cat"),
Some(vec![dp(1, &[&[0]]), dp(3, &[&[2]]), dp(7, &[&[1]])])
);
}
#[test]
fn decode_term_multiple_positions_one_doc() {
let terms = vec![(b"the".to_vec(), vec![p(1, &[&[0, 2]])])];
let seg = build_segment(&terms, 1, &[3], &[(1, vec![3])], 1000, 0);
assert_eq!(decode(&seg, b"the"), Some(vec![dp(1, &[&[0, 2]])]));
}
#[test]
fn decode_prefix_compressed_terms() {
let terms = vec![
(b"apple".to_vec(), vec![p(1, &[&[0]])]),
(b"apply".to_vec(), vec![p(2, &[&[0]])]),
];
let seg = build_segment(&terms, 2, &[2], &[(1, vec![1]), (2, vec![1])], 1000, 0);
assert_eq!(decode(&seg, b"apple"), Some(vec![dp(1, &[&[0]])]));
assert_eq!(decode(&seg, b"apply"), Some(vec![dp(2, &[&[0]])]));
assert_eq!(decode(&seg, b"appl"), None);
}
#[test]
fn decode_multi_column_positions() {
let terms = vec![
(b"hello".to_vec(), vec![p(1, &[&[0], &[0]])]),
(b"there".to_vec(), vec![p(1, &[&[], &[1]])]),
];
let seg = build_segment(&terms, 1, &[1, 2], &[(1, vec![1, 2])], 1000, 0);
assert_eq!(decode(&seg, b"hello"), Some(vec![dp(1, &[&[0], &[0]])]));
assert_eq!(decode(&seg, b"there"), Some(vec![dp(1, &[&[], &[1]])]));
}
#[test]
fn decode_many_terms_one_leaf() {
let words: &[&[u8]] = &[b"alpha", b"beta", b"delta", b"gamma", b"omega"];
let terms: Vec<(Vec<u8>, Vec<Posting>)> = words
.iter()
.enumerate()
.map(|(i, w)| (w.to_vec(), vec![p(i as i64 + 1, &[&[0]])]))
.collect();
let doc_sizes: Vec<(i64, Vec<u64>)> =
(1..=words.len() as i64).map(|r| (r, vec![1])).collect();
let seg = build_segment(
&terms,
words.len() as u64,
&[words.len() as u64],
&doc_sizes,
1000,
0,
);
for (i, w) in words.iter().enumerate() {
assert_eq!(
decode(&seg, w),
Some(vec![dp(i as i64 + 1, &[&[0]])]),
"{w:?}"
);
}
assert_eq!(decode(&seg, b"missing"), None);
}
fn leaf_count(seg: &Segment) -> usize {
leaves_of(seg).len()
}
#[test]
fn decode_multi_leaf_term_pagination() {
let n = 40usize;
let words: Vec<Vec<u8>> = (0..n).map(|i| format!("term{i:03}").into_bytes()).collect();
let terms: Vec<(Vec<u8>, Vec<Posting>)> = words
.iter()
.enumerate()
.map(|(i, w)| (w.clone(), vec![p(i as i64 + 1, &[&[0]])]))
.collect();
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n as i64).map(|r| (r, vec![1])).collect();
let seg = build_segment(&terms, n as u64, &[n as u64], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must split into many leaves");
for (i, w) in words.iter().enumerate() {
assert_eq!(
decode(&seg, w),
Some(vec![dp(i as i64 + 1, &[&[0]])]),
"term {w:?} on leaf pagination"
);
}
assert_eq!(decode(&seg, b"term999"), None);
}
#[test]
fn decode_doclist_spanning_leaves() {
let n = 40i64;
let postings: Vec<Posting> = (1..=n).map(|r| p(r, &[&[0]])).collect();
let terms = vec![(b"x".to_vec(), postings)];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n).map(|r| (r, vec![1])).collect();
let seg = build_segment(&terms, n as u64, &[n as u64], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must span the doclist");
let want: Vec<DecodedPosting> = (1..=n).map(|r| dp(r, &[&[0]])).collect();
assert_eq!(decode(&seg, b"x"), Some(want));
assert_eq!(decode(&seg, b"y"), None);
}
#[test]
fn decode_doclist_spanning_multi_position() {
let n = 30i64;
let postings: Vec<Posting> = (1..=n).map(|r| p(r, &[&[0, 3, 9, 15]])).collect();
let terms = vec![(b"w".to_vec(), postings)];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n).map(|r| (r, vec![16])).collect();
let seg = build_segment(&terms, n as u64, &[(16 * n) as u64], &doc_sizes, 48, 0);
assert!(leaf_count(&seg) > 1, "pgsz 48 must span the doclist");
let want: Vec<DecodedPosting> = (1..=n).map(|r| dp(r, &[&[0, 3, 9, 15]])).collect();
assert_eq!(decode(&seg, b"w"), Some(want));
}
#[test]
fn decode_mixed_pagination_and_spanning() {
let mut terms: Vec<(Vec<u8>, Vec<Posting>)> = Vec::new();
terms.push((b"heavy".to_vec(), (1..=25).map(|r| p(r, &[&[0]])).collect()));
for i in 0..20 {
let w = format!("light{i:02}").into_bytes();
terms.push((w, vec![p(100 + i as i64, &[&[1]])]));
}
terms.sort_by(|a, b| a.0.cmp(&b.0));
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=120).map(|r| (r, vec![1])).collect();
let seg = build_segment(&terms, 120, &[120], &doc_sizes, 56, 0);
assert!(leaf_count(&seg) > 2, "expected several leaves");
for (term, postings) in &terms {
let want: Vec<DecodedPosting> = postings
.iter()
.map(|p| DecodedPosting {
rowid: p.rowid,
cols: p.cols.clone(),
})
.collect();
assert_eq!(decode(&seg, term), Some(want), "term {term:?}");
}
assert_eq!(decode(&seg, b"absent"), None);
}
#[test]
fn lookup_rowids_single_segment_present_and_absent() {
let terms = vec![(
b"cat".to_vec(),
vec![p(1, &[&[0]]), p(3, &[&[2]]), p(7, &[&[1]])],
)];
let seg = build_segment(
&terms,
3,
&[3],
&[(1, vec![1]), (3, vec![3]), (7, vec![2])],
1000,
0,
);
assert_eq!(lookup_term_rowids(&seg.data, b"cat"), Some(vec![1, 3, 7]));
assert_eq!(lookup_term_rowids(&seg.data, b"dog"), Some(Vec::new()));
}
#[test]
fn lookup_rowids_empty_index_falls_back() {
let seg = build_segment(&[], 0, &[0], &[], 1000, 0);
assert_eq!(lookup_term_rowids(&seg.data, b"anything"), None);
}
#[test]
fn lookup_rowids_multi_leaf_segment() {
let n = 40i64;
let postings: Vec<Posting> = (1..=n).map(|r| p(r, &[&[0]])).collect();
let terms = vec![(b"x".to_vec(), postings)];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n).map(|r| (r, vec![1])).collect();
let seg = build_segment(&terms, n as u64, &[n as u64], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must span the doclist");
let want: Vec<i64> = (1..=n).collect();
assert_eq!(lookup_term_rowids(&seg.data, b"x"), Some(want));
assert_eq!(lookup_term_rowids(&seg.data, b"y"), Some(Vec::new()));
}
#[test]
fn lookup_bool_tree_n_operand_set_ops() {
use crate::vtab::{Fts5BoolOp, Fts5BoolTree};
use alloc::boxed::Box;
let terms = vec![
(
b"a".to_vec(),
(1..=5).map(|r| p(r, &[&[0]])).collect::<Vec<_>>(),
),
(
b"b".to_vec(),
vec![2, 4, 6, 8]
.into_iter()
.map(|r| p(r, &[&[1]]))
.collect(),
),
(
b"c".to_vec(),
vec![3, 4, 5, 6]
.into_iter()
.map(|r| p(r, &[&[2]]))
.collect(),
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=8).map(|r| (r, vec![3])).collect();
let seg = build_segment(&terms, 8, &[8], &doc_sizes, 1000, 0);
let leaf = |t: &[u8]| Fts5BoolTree::Leaf(t.to_vec());
let op = |o, l, r| Fts5BoolTree::Op(o, Box::new(l), Box::new(r));
let t = op(
Fts5BoolOp::And,
op(Fts5BoolOp::And, leaf(b"a"), leaf(b"b")),
leaf(b"c"),
);
assert_eq!(lookup_bool_tree_rowids(&seg.data, &t), Some(vec![4]));
let t = op(
Fts5BoolOp::Or,
op(Fts5BoolOp::Or, leaf(b"a"), leaf(b"b")),
leaf(b"c"),
);
assert_eq!(
lookup_bool_tree_rowids(&seg.data, &t),
Some(vec![1, 2, 3, 4, 5, 6, 8])
);
let t = op(
Fts5BoolOp::Or,
leaf(b"a"),
op(Fts5BoolOp::And, leaf(b"b"), leaf(b"c")),
);
assert_eq!(
lookup_bool_tree_rowids(&seg.data, &t),
Some(vec![1, 2, 3, 4, 5, 6])
);
let t = op(
Fts5BoolOp::Not,
op(Fts5BoolOp::Or, leaf(b"a"), leaf(b"b")),
leaf(b"c"),
);
assert_eq!(lookup_bool_tree_rowids(&seg.data, &t), Some(vec![1, 2, 8]));
let t = op(Fts5BoolOp::And, leaf(b"a"), leaf(b"missing"));
assert_eq!(lookup_bool_tree_rowids(&seg.data, &t), Some(Vec::new()));
assert_eq!(
lookup_bool_tree_rowids(&seg.data, &leaf(b"c")),
Some(vec![3, 4, 5, 6])
);
}
#[test]
fn lookup_bool_tree_empty_index_falls_back() {
use crate::vtab::Fts5BoolTree;
let seg = build_segment(&[], 0, &[0], &[], 1000, 0);
let t = Fts5BoolTree::Leaf(b"x".to_vec());
assert_eq!(lookup_bool_tree_rowids(&seg.data, &t), None);
}
#[test]
fn lookup_rowids_in_column_filters_by_column() {
let terms = vec![(
b"word".to_vec(),
vec![
p(1, &[&[0], &[]]),
p(2, &[&[], &[0]]),
p(3, &[&[0], &[1]]),
p(4, &[&[2], &[]]),
],
)];
let doc_sizes: Vec<(i64, Vec<u64>)> = vec![
(1, vec![1, 0]),
(2, vec![0, 1]),
(3, vec![1, 2]),
(4, vec![3, 0]),
];
let seg = build_segment(&terms, 4, &[3, 2], &doc_sizes, 1000, 0);
assert_eq!(
lookup_term_rowids_in_column(&seg.data, b"word", 0),
Some(vec![1, 3, 4])
);
assert_eq!(
lookup_term_rowids_in_column(&seg.data, b"word", 1),
Some(vec![2, 3])
);
assert_eq!(
lookup_term_rowids(&seg.data, b"word"),
Some(vec![1, 2, 3, 4])
);
assert_eq!(
lookup_term_rowids_in_column(&seg.data, b"missing", 0),
Some(Vec::new())
);
assert_eq!(
lookup_term_rowids_in_column(&seg.data, b"word", 9),
Some(Vec::new())
);
}
#[test]
fn lookup_rowids_in_column_multi_leaf() {
let n = 40i64;
let postings: Vec<Posting> = (1..=n)
.map(|r| {
if r % 2 == 0 {
p(r, &[&[0], &[]])
} else {
p(r, &[&[], &[0]])
}
})
.collect();
let terms = vec![(b"x".to_vec(), postings)];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n).map(|r| (r, vec![1, 1])).collect();
let seg = build_segment(&terms, n as u64, &[20, 20], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must span the doclist");
let even: Vec<i64> = (1..=n).filter(|r| r % 2 == 0).collect();
let odd: Vec<i64> = (1..=n).filter(|r| r % 2 == 1).collect();
assert_eq!(lookup_term_rowids_in_column(&seg.data, b"x", 0), Some(even));
assert_eq!(lookup_term_rowids_in_column(&seg.data, b"x", 1), Some(odd));
}
#[test]
fn lookup_prefix_rowids_unions_matching_terms() {
let terms = vec![
(b"apex".to_vec(), vec![p(3, &[&[1]])]),
(b"apple".to_vec(), vec![p(1, &[&[0]]), p(6, &[&[0]])]),
(b"apply".to_vec(), vec![p(4, &[&[0]]), p(6, &[&[1]])]),
(b"banana".to_vec(), vec![p(2, &[&[0]]), p(5, &[&[0]])]),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = vec![
(1, vec![1]),
(2, vec![1]),
(3, vec![2]),
(4, vec![1]),
(5, vec![1]),
(6, vec![2]),
];
let seg = build_segment(&terms, 6, &[8], &doc_sizes, 1000, 0);
assert_eq!(
lookup_prefix_rowids(&seg.data, b"ap"),
Some(vec![1, 3, 4, 6])
);
assert_eq!(
lookup_prefix_rowids(&seg.data, b"appl"),
Some(vec![1, 4, 6])
);
assert_eq!(lookup_prefix_rowids(&seg.data, b"apple"), Some(vec![1, 6]));
assert_eq!(lookup_prefix_rowids(&seg.data, b"ban"), Some(vec![2, 5]));
assert_eq!(lookup_prefix_rowids(&seg.data, b"zzz"), Some(Vec::new()));
let empty = build_segment(&[], 0, &[0], &[], 1000, 0);
assert_eq!(lookup_prefix_rowids(&empty.data, b"ap"), None);
}
#[test]
fn lookup_prefix_rowids_multi_leaf() {
let n = 40usize;
let terms: Vec<(Vec<u8>, Vec<Posting>)> = (0..n)
.map(|i| {
(
format!("word{i:03}").into_bytes(),
vec![p(i as i64 + 1, &[&[0]])],
)
})
.collect();
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n as i64).map(|r| (r, vec![1])).collect();
let seg = build_segment(&terms, n as u64, &[n as u64], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must split into many leaves");
assert_eq!(
lookup_prefix_rowids(&seg.data, b"word"),
Some((1..=n as i64).collect::<Vec<_>>())
);
assert_eq!(
lookup_prefix_rowids(&seg.data, b"word01"),
Some((11..=20).collect::<Vec<_>>())
);
assert_eq!(lookup_prefix_rowids(&seg.data, b"zzz"), Some(Vec::new()));
}
#[test]
fn lookup_prefix_rowids_in_column_filters() {
let terms = vec![
(b"fort".to_vec(), vec![p(3, &[&[0], &[]])]),
(
b"fox".to_vec(),
vec![p(1, &[&[0], &[]]), p(2, &[&[], &[0]]), p(4, &[&[], &[1]])],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = vec![
(1, vec![1, 0]),
(2, vec![0, 1]),
(3, vec![1, 0]),
(4, vec![0, 2]),
];
let seg = build_segment(&terms, 4, &[2, 3], &doc_sizes, 1000, 0);
assert_eq!(
lookup_prefix_rowids(&seg.data, b"fo"),
Some(vec![1, 2, 3, 4])
);
assert_eq!(
lookup_prefix_rowids_in_column(&seg.data, b"fo", 0),
Some(vec![1, 3])
);
assert_eq!(
lookup_prefix_rowids_in_column(&seg.data, b"fo", 1),
Some(vec![2, 4])
);
assert_eq!(
lookup_prefix_rowids_in_column(&seg.data, b"fo", 9),
Some(Vec::new())
);
}
#[test]
fn lookup_phrase_rowids_adjacency() {
let terms = vec![
(
b"a".to_vec(),
vec![
p(1, &[&[0]]),
p(2, &[&[0]]),
p(3, &[&[1]]),
p(4, &[&[0, 3]]),
p(5, &[&[2]]),
],
),
(
b"b".to_vec(),
vec![
p(1, &[&[1]]),
p(2, &[&[2]]),
p(3, &[&[0]]),
p(4, &[&[1, 5]]),
p(6, &[&[1]]),
],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=6).map(|r| (r, vec![8])).collect::<Vec<_>>();
let seg = build_segment(&terms, 6, &[40], &doc_sizes, 1000, 0);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"b"]),
Some(vec![1, 4])
);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"b", b"a"]),
Some(vec![3])
);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"zzz"]),
Some(Vec::new())
);
}
#[test]
fn lookup_phrase_repeated_word() {
let terms = vec![(b"a".to_vec(), vec![p(1, &[&[0, 1]]), p(2, &[&[0, 2]])])];
let seg = build_segment(&terms, 2, &[4], &[(1, vec![2]), (2, vec![3])], 1000, 0);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"a"]),
Some(vec![1])
);
}
#[test]
fn lookup_phrase_in_column_requires_same_column() {
let terms = vec![
(
b"a".to_vec(),
vec![
p(1, &[&[0], &[]]),
p(2, &[&[0], &[]]),
p(3, &[&[], &[2]]),
p(4, &[&[0], &[5]]),
],
),
(
b"b".to_vec(),
vec![
p(1, &[&[1], &[]]),
p(2, &[&[], &[1]]),
p(3, &[&[], &[3]]),
p(4, &[&[1], &[6]]),
],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=4).map(|r| (r, vec![8, 8])).collect::<Vec<_>>();
let seg = build_segment(&terms, 4, &[40, 40], &doc_sizes, 1000, 0);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"b"]),
Some(vec![1, 3, 4])
);
assert_eq!(
lookup_phrase_rowids_in_column_k(&seg.data, &[b"a", b"b"], 0),
Some(vec![1, 4])
);
assert_eq!(
lookup_phrase_rowids_in_column_k(&seg.data, &[b"a", b"b"], 1),
Some(vec![3, 4])
);
}
#[test]
fn lookup_phrase_multi_leaf() {
let n = 40i64;
let a_post: Vec<Posting> = (1..=n).map(|r| p(r, &[&[0]])).collect();
let b_post: Vec<Posting> = (1..=n)
.map(|r| {
if r % 2 == 0 {
p(r, &[&[1]])
} else {
p(r, &[&[3]])
}
})
.collect();
let terms = vec![(b"a".to_vec(), a_post), (b"b".to_vec(), b_post)];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n).map(|r| (r, vec![8])).collect();
let seg = build_segment(&terms, n as u64, &[8 * n as u64], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must span the doclists");
let even: Vec<i64> = (1..=n).filter(|r| r % 2 == 0).collect();
assert_eq!(lookup_phrase_rowids_k(&seg.data, &[b"a", b"b"]), Some(even));
}
#[test]
fn lookup_phrase_empty_index_falls_back() {
let seg = build_segment(&[], 0, &[0], &[], 1000, 0);
assert_eq!(lookup_phrase_rowids_k(&seg.data, &[b"a", b"b"]), None);
assert_eq!(
lookup_phrase_rowids_in_column_k(&seg.data, &[b"a", b"b"], 0),
None
);
}
#[test]
fn lookup_phrase_k_consecutive_run() {
let terms = vec![
(
b"a".to_vec(),
vec![
p(1, &[&[0]]),
p(2, &[&[0]]),
p(3, &[&[0]]),
p(4, &[&[0, 3]]),
p(5, &[&[0]]),
],
),
(
b"b".to_vec(),
vec![
p(1, &[&[1]]),
p(2, &[&[1]]),
p(3, &[&[2]]),
p(4, &[&[1, 4]]),
p(5, &[&[1]]),
],
),
(
b"c".to_vec(),
vec![
p(1, &[&[2]]),
p(2, &[&[3]]),
p(3, &[&[1]]),
p(4, &[&[2, 5]]),
],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=5).map(|r| (r, vec![8])).collect();
let seg = build_segment(&terms, 5, &[40], &doc_sizes, 1000, 0);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"b", b"c"]),
Some(vec![1, 4])
);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"b"]),
Some(vec![1, 2, 4, 5])
);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"b", b"zzz"]),
Some(Vec::new())
);
}
#[test]
fn lookup_phrase_k_repeated_word() {
let terms = vec![(
b"a".to_vec(),
vec![p(1, &[&[0, 1, 2]]), p(2, &[&[0, 1]]), p(3, &[&[0, 2, 4]])],
)];
let doc_sizes = [(1, vec![3]), (2, vec![2]), (3, vec![5])];
let seg = build_segment(&terms, 3, &[10], &doc_sizes, 1000, 0);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"a"]),
Some(vec![1, 2])
);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"a", b"a"]),
Some(vec![1])
);
}
#[test]
fn lookup_phrase_k_column_boundary() {
let terms = vec![
(
b"a".to_vec(),
vec![p(1, &[&[0], &[]]), p(2, &[&[0], &[]]), p(3, &[&[], &[3]])],
),
(
b"b".to_vec(),
vec![p(1, &[&[1], &[]]), p(2, &[&[1], &[]]), p(3, &[&[], &[4]])],
),
(
b"c".to_vec(),
vec![p(1, &[&[2], &[]]), p(2, &[&[], &[0]]), p(3, &[&[], &[5]])],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=3).map(|r| (r, vec![8, 8])).collect();
let seg = build_segment(&terms, 3, &[40, 40], &doc_sizes, 1000, 0);
assert_eq!(
lookup_phrase_rowids_k(&seg.data, &[b"a", b"b", b"c"]),
Some(vec![1, 3])
);
assert_eq!(
lookup_phrase_rowids_in_column_k(&seg.data, &[b"a", b"b", b"c"], 0),
Some(vec![1])
);
assert_eq!(
lookup_phrase_rowids_in_column_k(&seg.data, &[b"a", b"b", b"c"], 1),
Some(vec![3])
);
}
#[test]
fn lookup_near_rowids_distance_boundary() {
let terms = vec![
(
b"a".to_vec(),
vec![
p(1, &[&[0]]),
p(2, &[&[0]]),
p(3, &[&[0]]),
p(4, &[&[0]]),
p(5, &[&[0]]),
p(6, &[&[0]]),
],
),
(
b"b".to_vec(),
vec![
p(1, &[&[1]]),
p(2, &[&[2]]),
p(3, &[&[3]]),
p(4, &[&[4]]),
p(6, &[&[1]]),
],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=6).map(|r| (r, vec![8])).collect();
let seg = build_segment(&terms, 6, &[40], &doc_sizes, 1000, 0);
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"b", 0),
Some(vec![1, 6])
);
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"b", 1),
Some(vec![1, 2, 6])
);
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"b", 2),
Some(vec![1, 2, 3, 6])
);
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"b", 3),
Some(vec![1, 2, 3, 4, 6])
);
assert_eq!(
lookup_near_rowids(&seg.data, b"b", b"a", 0),
Some(vec![1, 6])
);
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"zzz", 10),
Some(Vec::new())
);
}
#[test]
fn lookup_near_rowids_requires_same_column() {
let terms = vec![
(
b"a".to_vec(),
vec![
p(1, &[&[0], &[]]),
p(2, &[&[0], &[]]),
p(3, &[&[], &[5]]),
p(4, &[&[0], &[]]),
],
),
(
b"b".to_vec(),
vec![
p(1, &[&[2], &[]]),
p(2, &[&[], &[0]]),
p(3, &[&[], &[7]]),
p(4, &[&[], &[9]]),
],
),
];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=4).map(|r| (r, vec![8, 12])).collect();
let seg = build_segment(&terms, 4, &[40, 60], &doc_sizes, 1000, 0);
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"b", 1),
Some(vec![1, 3])
);
}
#[test]
fn lookup_near_rowids_multi_leaf() {
let n = 40i64;
let a_post: Vec<Posting> = (1..=n).map(|r| p(r, &[&[0]])).collect();
let b_post: Vec<Posting> = (1..=n)
.map(|r| {
if r % 2 == 0 {
p(r, &[&[1]])
} else {
p(r, &[&[5]])
}
})
.collect();
let terms = vec![(b"a".to_vec(), a_post), (b"b".to_vec(), b_post)];
let doc_sizes: Vec<(i64, Vec<u64>)> = (1..=n).map(|r| (r, vec![8])).collect();
let seg = build_segment(&terms, n as u64, &[8 * n as u64], &doc_sizes, 64, 0);
assert!(leaf_count(&seg) > 1, "pgsz 64 must span the doclists");
let even: Vec<i64> = (1..=n).filter(|r| r % 2 == 0).collect();
assert_eq!(lookup_near_rowids(&seg.data, b"a", b"b", 0), Some(even));
assert_eq!(
lookup_near_rowids(&seg.data, b"a", b"b", 4),
Some((1..=n).collect::<Vec<_>>())
);
}
#[test]
fn lookup_near_empty_index_falls_back() {
let seg = build_segment(&[], 0, &[0], &[], 1000, 0);
assert_eq!(lookup_near_rowids(&seg.data, b"a", b"b", 10), None);
}
type SegSpec = (i64, Vec<(Vec<u8>, Vec<Posting>)>);
fn multiseg_data(specs: &[SegSpec]) -> Vec<(i64, Vec<u8>)> {
let mut data: Vec<(i64, Vec<u8>)> = Vec::new();
let mut struct_body: Vec<u8> = 0u32.to_be_bytes().to_vec(); put_varint(&mut struct_body, 1); put_varint(&mut struct_body, specs.len() as u64); put_varint(&mut struct_body, 0); put_varint(&mut struct_body, 0); put_varint(&mut struct_body, specs.len() as u64); for (segid, terms) in specs {
let n_docs = terms.iter().flat_map(|(_, ps)| ps.iter()).count() as u64;
let seg = build_segment(terms, n_docs.max(1), &[64], &[], 4096, 0);
let mut pgno = 1i64;
let mut n_leaves = 0i64;
loop {
let rid = segment_leaf_rowid(1, pgno);
match seg.data.iter().find(|(id, _)| *id == rid) {
Some((_, blob)) => {
data.push((segment_leaf_rowid(*segid, pgno), blob.clone()));
n_leaves += 1;
pgno += 1;
}
None => break,
}
}
put_varint(&mut struct_body, *segid as u64);
put_varint(&mut struct_body, 1);
put_varint(&mut struct_body, n_leaves as u64);
}
data.push((STRUCTURE_ROWID, struct_body));
data
}
#[test]
fn multiseg_bare_term_unions_across_segments_and_routes() {
let specs = vec![
(
1i64,
vec![(b"cat".to_vec(), vec![p(1, &[&[0]]), p(4, &[&[0]])])],
),
(
2i64,
vec![(b"cat".to_vec(), vec![p(2, &[&[0]]), p(7, &[&[0]])])],
),
(
3i64,
vec![
(b"cat".to_vec(), vec![p(5, &[&[0]])]),
(b"dog".to_vec(), vec![p(9, &[&[0]])]),
],
),
];
let data = multiseg_data(&specs);
let before = INDEX_ROUTE_HITS.load(core::sync::atomic::Ordering::Relaxed);
assert_eq!(lookup_term_rowids(&data, b"cat"), Some(vec![1, 2, 4, 5, 7]));
assert!(
INDEX_ROUTE_HITS.load(core::sync::atomic::Ordering::Relaxed) > before,
"the multi-segment bare term must take the index route"
);
assert_eq!(lookup_term_rowids(&data, b"dog"), Some(vec![9]));
assert_eq!(lookup_term_rowids(&data, b"zzz"), Some(Vec::new()));
}
#[test]
fn multiseg_overlapping_docid_bails_to_scan() {
let specs = vec![
(1i64, vec![(b"cat".to_vec(), vec![p(4, &[&[0]])])]),
(2i64, vec![(b"cat".to_vec(), vec![p(4, &[&[1]])])]),
];
let data = multiseg_data(&specs);
assert_eq!(lookup_term_rowids(&data, b"cat"), None);
}
#[test]
fn multiseg_boolean_tree_merges_across_segments() {
use crate::vtab::{Fts5BoolOp, Fts5BoolTree};
use alloc::boxed::Box;
let specs = vec![
(
1i64,
vec![(
b"a".to_vec(),
vec![p(1, &[&[0]]), p(2, &[&[0]]), p(3, &[&[0]])],
)],
),
(
2i64,
vec![(
b"b".to_vec(),
vec![p(2, &[&[0]]), p(3, &[&[0]]), p(4, &[&[0]])],
)],
),
(
3i64,
vec![(b"c".to_vec(), vec![p(3, &[&[0]]), p(5, &[&[0]])])],
),
];
let data = multiseg_data(&specs);
let leaf = |t: &[u8]| Fts5BoolTree::Leaf(t.to_vec());
let op = |o, l, r| Fts5BoolTree::Op(o, Box::new(l), Box::new(r));
let t = op(Fts5BoolOp::And, leaf(b"a"), leaf(b"b"));
assert_eq!(lookup_bool_tree_rowids(&data, &t), Some(vec![2, 3]));
let t = op(Fts5BoolOp::Or, leaf(b"a"), leaf(b"c"));
assert_eq!(lookup_bool_tree_rowids(&data, &t), Some(vec![1, 2, 3, 5]));
let t = op(
Fts5BoolOp::Not,
op(Fts5BoolOp::Or, leaf(b"a"), leaf(b"b")),
leaf(b"c"),
);
assert_eq!(lookup_bool_tree_rowids(&data, &t), Some(vec![1, 2, 4]));
}
#[test]
fn multiseg_boolean_tree_bails_when_a_leaf_overlaps() {
use crate::vtab::{Fts5BoolOp, Fts5BoolTree};
use alloc::boxed::Box;
let specs = vec![
(1i64, vec![(b"a".to_vec(), vec![p(1, &[&[0]])])]),
(2i64, vec![(b"b".to_vec(), vec![p(1, &[&[0]])])]),
(3i64, vec![(b"b".to_vec(), vec![p(1, &[&[1]])])]),
];
let data = multiseg_data(&specs);
let t = Fts5BoolTree::Op(
Fts5BoolOp::And,
Box::new(Fts5BoolTree::Leaf(b"a".to_vec())),
Box::new(Fts5BoolTree::Leaf(b"b".to_vec())),
);
assert_eq!(lookup_bool_tree_rowids(&data, &t), None);
}
#[test]
fn multiseg_prefix_unions_across_segments() {
let specs = vec![
(1i64, vec![(b"apple".to_vec(), vec![p(1, &[&[0]])])]),
(2i64, vec![(b"apply".to_vec(), vec![p(2, &[&[0]])])]),
(
3i64,
vec![
(b"apex".to_vec(), vec![p(3, &[&[0]])]),
(b"banana".to_vec(), vec![p(4, &[&[0]])]),
],
),
];
let data = multiseg_data(&specs);
assert_eq!(lookup_prefix_rowids(&data, b"ap"), Some(vec![1, 2, 3]));
assert_eq!(lookup_prefix_rowids(&data, b"appl"), Some(vec![1, 2]));
assert_eq!(lookup_prefix_rowids(&data, b"ban"), Some(vec![4]));
assert_eq!(lookup_prefix_rowids(&data, b"zzz"), Some(Vec::new()));
}
#[test]
fn multiseg_prefix_same_doc_two_terms_one_segment_is_not_overlap() {
let specs = vec![
(
1i64,
vec![
(b"apple".to_vec(), vec![p(1, &[&[0]])]),
(b"apply".to_vec(), vec![p(1, &[&[1]])]),
],
),
(2i64, vec![(b"apex".to_vec(), vec![p(2, &[&[0]])])]),
];
let data = multiseg_data(&specs);
assert_eq!(lookup_prefix_rowids(&data, b"ap"), Some(vec![1, 2]));
}
#[test]
fn delete_tombstone_in_poslist_bails() {
let mut body: Vec<u8> = Vec::new();
let key = term_key(b"cat");
put_varint(&mut body, key.len() as u64);
body.extend_from_slice(&key);
let term_off = 4; put_varint(&mut body, 1); put_varint(&mut body, 1); let footer_off = 4 + body.len();
let mut leaf: Vec<u8> = Vec::new();
leaf.extend_from_slice(&0u16.to_be_bytes()); leaf.extend_from_slice(&(footer_off as u16).to_be_bytes());
leaf.extend_from_slice(&body);
put_varint(&mut leaf, term_off as u64);
let mut struct_body: Vec<u8> = 0u32.to_be_bytes().to_vec();
for v in [1u64, 1, 0, 0, 1, 1, 1, 1] {
put_varint(&mut struct_body, v);
}
let data = vec![
(segment_leaf_rowid(1, 1), leaf),
(STRUCTURE_ROWID, struct_body),
];
assert_eq!(lookup_term_rowids(&data, b"cat"), None);
}
#[test]
fn multiseg_two_term_phrase_unions_across_segments_and_routes() {
let specs = vec![
(
1i64,
vec![
(b"a".to_vec(), vec![p(1, &[&[0]]), p(2, &[&[0]])]),
(b"b".to_vec(), vec![p(1, &[&[1]]), p(2, &[&[2]])]),
],
),
(
2i64,
vec![
(b"a".to_vec(), vec![p(4, &[&[0]]), p(5, &[&[1]])]),
(b"b".to_vec(), vec![p(4, &[&[1]]), p(5, &[&[0]])]),
],
),
(
3i64,
vec![
(b"a".to_vec(), vec![p(7, &[&[0, 3]]), p(9, &[&[5]])]),
(b"b".to_vec(), vec![p(7, &[&[1]])]),
],
),
];
let data = multiseg_data(&specs);
let before = INDEX_ROUTE_HITS.load(core::sync::atomic::Ordering::Relaxed);
assert_eq!(
lookup_phrase_rowids_k(&data, &[b"a", b"b"]),
Some(vec![1, 4, 7])
);
assert!(
INDEX_ROUTE_HITS.load(core::sync::atomic::Ordering::Relaxed) > before,
"the multi-segment phrase must take the index route"
);
assert_eq!(lookup_phrase_rowids_k(&data, &[b"b", b"a"]), Some(vec![5]));
assert_eq!(
lookup_phrase_rowids_k(&data, &[b"a", b"zzz"]),
Some(Vec::new())
);
}
#[test]
fn multiseg_three_term_phrase_across_segments() {
let specs = vec![
(
1i64,
vec![
(b"a".to_vec(), vec![p(1, &[&[0]]), p(2, &[&[0]])]),
(b"b".to_vec(), vec![p(1, &[&[1]]), p(2, &[&[1]])]),
(b"c".to_vec(), vec![p(1, &[&[2]]), p(2, &[&[3]])]),
],
),
(
2i64,
vec![
(b"a".to_vec(), vec![p(3, &[&[5]]), p(4, &[&[0]])]),
(b"b".to_vec(), vec![p(3, &[&[6]]), p(4, &[&[2]])]),
(b"c".to_vec(), vec![p(3, &[&[7]]), p(4, &[&[3]])]),
],
),
];
let data = multiseg_data(&specs);
assert_eq!(
lookup_phrase_rowids_k(&data, &[b"a", b"b", b"c"]),
Some(vec![1, 3])
);
}
#[test]
fn multiseg_phrase_in_column_across_segments() {
let specs = vec![
(
1i64,
vec![
(b"a".to_vec(), vec![p(1, &[&[0], &[]]), p(2, &[&[0], &[]])]),
(b"b".to_vec(), vec![p(1, &[&[1], &[]]), p(2, &[&[], &[1]])]),
],
),
(
2i64,
vec![
(b"a".to_vec(), vec![p(3, &[&[], &[2]]), p(4, &[&[0], &[5]])]),
(b"b".to_vec(), vec![p(3, &[&[], &[3]]), p(4, &[&[1], &[6]])]),
],
),
];
let data = multiseg_data(&specs);
assert_eq!(
lookup_phrase_rowids_in_column_k(&data, &[b"a", b"b"], 0),
Some(vec![1, 4])
);
assert_eq!(
lookup_phrase_rowids_in_column_k(&data, &[b"a", b"b"], 1),
Some(vec![3, 4])
);
}
#[test]
fn multiseg_near_unions_across_segments_and_routes() {
let specs = vec![
(
1i64,
vec![
(b"a".to_vec(), vec![p(1, &[&[0]]), p(2, &[&[0]])]),
(b"b".to_vec(), vec![p(1, &[&[1]]), p(2, &[&[5]])]),
],
),
(
2i64,
vec![
(b"a".to_vec(), vec![p(3, &[&[4]]), p(4, &[&[0]])]),
(b"b".to_vec(), vec![p(3, &[&[2]]), p(4, &[&[9]])]),
],
),
];
let data = multiseg_data(&specs);
let before = INDEX_ROUTE_HITS.load(core::sync::atomic::Ordering::Relaxed);
assert_eq!(lookup_near_rowids(&data, b"a", b"b", 1), Some(vec![1, 3]));
assert!(
INDEX_ROUTE_HITS.load(core::sync::atomic::Ordering::Relaxed) > before,
"the multi-segment NEAR must take the index route"
);
assert_eq!(
lookup_near_rowids(&data, b"a", b"b", 4),
Some(vec![1, 2, 3])
);
assert_eq!(
lookup_near_rowids(&data, b"a", b"zzz", 10),
Some(Vec::new())
);
}
#[test]
fn multiseg_phrase_overlapping_docid_bails_to_scan() {
let specs = vec![
(
1i64,
vec![
(b"a".to_vec(), vec![p(4, &[&[0]])]),
(b"b".to_vec(), vec![p(4, &[&[1]])]),
],
),
(
2i64,
vec![
(b"a".to_vec(), vec![p(4, &[&[0]])]),
(b"b".to_vec(), vec![p(4, &[&[1]])]),
],
),
];
let data = multiseg_data(&specs);
assert_eq!(lookup_phrase_rowids_k(&data, &[b"a", b"b"]), None);
assert_eq!(lookup_near_rowids(&data, b"a", b"b", 5), None);
}
#[test]
fn multiseg_phrase_cross_term_overlap_bails_to_scan() {
let specs = vec![
(1i64, vec![(b"a".to_vec(), vec![p(4, &[&[0]])])]),
(2i64, vec![(b"b".to_vec(), vec![p(4, &[&[1]])])]),
];
let data = multiseg_data(&specs);
assert_eq!(lookup_phrase_rowids_k(&data, &[b"a", b"b"]), None);
assert_eq!(lookup_near_rowids(&data, b"a", b"b", 5), None);
}
#[test]
fn multiseg_phrase_tombstone_bails_to_scan() {
let mut tomb_leaf: Vec<u8> = Vec::new();
let mut body: Vec<u8> = Vec::new();
let key = term_key(b"a");
put_varint(&mut body, key.len() as u64);
body.extend_from_slice(&key);
let term_off = 4;
put_varint(&mut body, 1); put_varint(&mut body, 1); let footer_off = 4 + body.len();
tomb_leaf.extend_from_slice(&0u16.to_be_bytes());
tomb_leaf.extend_from_slice(&(footer_off as u16).to_be_bytes());
tomb_leaf.extend_from_slice(&body);
put_varint(&mut tomb_leaf, term_off as u64);
let seg1 = build_segment(
&[
(b"a".to_vec(), vec![p(3, &[&[0]])]),
(b"b".to_vec(), vec![p(3, &[&[1]])]),
],
1,
&[64],
&[],
4096,
0,
);
let leaf1 = seg1
.data
.iter()
.find(|(id, _)| *id == segment_leaf_rowid(1, 1))
.unwrap()
.1
.clone();
let mut struct_body: Vec<u8> = 0u32.to_be_bytes().to_vec();
for v in [1u64, 2, 0, 0, 2, 1, 1, 1, 2, 1, 1] {
put_varint(&mut struct_body, v);
}
let data = vec![
(segment_leaf_rowid(1, 1), leaf1),
(segment_leaf_rowid(2, 1), tomb_leaf),
(STRUCTURE_ROWID, struct_body),
];
assert_eq!(lookup_phrase_rowids_k(&data, &[b"a", b"b"]), None);
assert_eq!(lookup_near_rowids(&data, b"a", b"b", 5), None);
}
}