use std::collections::HashSet;
use std::ops::Bound;
use std::time::Instant;
use ulid::Ulid;
use crate::document::{Document, Value};
use crate::engine::{ReadTxn, ScanFlow};
use crate::error::TalaDbError;
use crate::fts::{fts_table_name, fts_token_range, tokenize, ulid_from_fts_key};
use crate::index::{
CompoundIndexDef, IndexDef, compound_table_name, docs_table_name, index_table_name,
ulid_from_index_key,
};
use crate::query::filter::Filter;
use crate::query::planner::QueryPlan;
const FETCH_CHUNK: usize = 256;
pub fn execute(
plan: &QueryPlan,
filter: &Filter,
txn: &dyn ReadTxn,
collection: &str,
deadline: Option<Instant>,
) -> Result<Vec<Document>, TalaDbError> {
execute_limited(plan, filter, txn, collection, deadline, None)
}
pub(crate) fn matching_ids(
plan: &QueryPlan,
filter: &Filter,
txn: &dyn ReadTxn,
collection: &str,
indexes: &[IndexDef],
compounds: &[CompoundIndexDef],
) -> Result<HashSet<[u8; 16]>, TalaDbError> {
Ok(matching_ids_limited(
plan,
filter,
txn,
collection,
indexes,
compounds,
usize::MAX,
)?
.expect("an unlimited ID collection cannot request streaming"))
}
pub(crate) fn matching_ids_limited(
plan: &QueryPlan,
filter: &Filter,
txn: &dyn ReadTxn,
collection: &str,
indexes: &[IndexDef],
compounds: &[CompoundIndexDef],
limit: usize,
) -> Result<Option<HashSet<[u8; 16]>>, TalaDbError> {
let matcher = Matcher::new(filter)?;
let covered =
match super::index_filter::resolve(filter, indexes, compounds, txn, collection, limit) {
Ok(Some(covered)) if covered.exact => return Ok(Some(covered.ids)),
Ok(covered) => covered,
Err(super::index_filter::ResolveError::Limit) => return Ok(None),
Err(super::index_filter::ResolveError::Database(error)) => return Err(error),
};
let mut ids = HashSet::new();
let fields = super::filter_document::fields(filter);
let table = docs_table_name(collection);
let mut overflow = false;
let mut accept = |bytes: &[u8]| -> Result<bool, TalaDbError> {
let doc = super::filter_document::decode(bytes, &fields)?;
if matcher.matches(&doc)? {
if ids.len() >= limit && !ids.contains(&doc.id.to_bytes()) {
overflow = true;
return Ok(false);
}
ids.insert(doc.id.to_bytes());
}
Ok(true)
};
if let Some(covered) = covered {
for id in covered.ids {
if let Some(bytes) = txn.get(&table, &id)?
&& !accept(&bytes)?
{
break;
}
}
} else if let QueryPlan::ById { ids } = plan {
for id in ids {
if let Some(bytes) = txn.get(&table, &id.to_bytes())?
&& !accept(&bytes)?
{
break;
}
}
} else if limit != usize::MAX {
visit_plan_documents(plan, txn, collection, &mut accept)?;
} else if matches!(plan, QueryPlan::FullScan) {
txn.scan(
&table,
Bound::Unbounded,
Bound::Unbounded,
&mut |_, bytes| {
Ok(if accept(bytes)? {
ScanFlow::Continue
} else {
ScanFlow::Stop
})
},
)?;
} else {
for id in collect_ulids(plan, txn, collection, None)? {
if let Some(bytes) = txn.get(&table, &id)? {
accept(&bytes)?;
}
}
}
Ok((!overflow).then_some(ids))
}
pub fn execute_limited(
plan: &QueryPlan,
filter: &Filter,
txn: &dyn ReadTxn,
collection: &str,
deadline: Option<Instant>,
limit: Option<usize>,
) -> Result<Vec<Document>, TalaDbError> {
check_deadline(deadline)?;
if limit == Some(0) {
return Ok(vec![]);
}
let matcher = Matcher::new(filter)?;
let mut results: Vec<Document> = Vec::new();
let wants_more = |results: &Vec<Document>| limit.is_none_or(|n| results.len() < n);
match plan {
QueryPlan::FullScan => {
let table = docs_table_name(collection);
let mut err: Option<TalaDbError> = None;
txn.scan(&table, Bound::Unbounded, Bound::Unbounded, &mut |_, v| {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
err = Some(TalaDbError::QueryTimeout);
return Ok(ScanFlow::Stop);
}
let doc: Document = match postcard::from_bytes(v) {
Ok(d) => d,
Err(e) => {
err = Some(e.into());
return Ok(ScanFlow::Stop);
}
};
match matcher.matches(&doc) {
Ok(true) => results.push(doc),
Ok(false) => {}
Err(e) => {
err = Some(e);
return Ok(ScanFlow::Stop);
}
}
Ok(if wants_more(&results) {
ScanFlow::Continue
} else {
ScanFlow::Stop
})
})?;
if let Some(e) = err {
return Err(e);
}
}
QueryPlan::ById { ids } => {
fetch_filtered(
txn,
collection,
ids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::IndexEq { field, start, end } => {
let scan_cap = if plan_is_exact_for(plan, filter) {
limit
} else {
None
};
let ulids = index_range_scan(
txn,
collection,
field,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
scan_cap,
)?;
check_deadline(deadline)?;
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::IndexRange { field, start, end } => {
let start_ref = bound_as_ref(start);
let end_ref = bound_as_ref(end);
let mut ulids = index_range_scan(txn, collection, field, start_ref, end_ref, None)?;
let mut seen: HashSet<Ulid> = HashSet::with_capacity(ulids.len());
ulids.retain(|u| seen.insert(*u));
check_deadline(deadline)?;
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::IndexIn { field, ranges } => {
let mut ulids: Vec<Ulid> = Vec::new();
for (start, end) in ranges {
check_deadline(deadline)?;
let mut batch = index_range_scan(
txn,
collection,
field,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
None,
)?;
ulids.append(&mut batch);
}
let mut seen: HashSet<Ulid> = HashSet::with_capacity(ulids.len());
ulids.retain(|u| seen.insert(*u));
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::FtsSearch { field, tokens } => {
if tokens.is_empty() {
return Ok(vec![]);
}
let table = fts_table_name(collection, field);
let mut ulid_sets: Vec<HashSet<[u8; 16]>> = Vec::with_capacity(tokens.len());
for token in tokens {
check_deadline(deadline)?;
let (start, end) = fts_token_range(token);
let mut set: HashSet<[u8; 16]> = HashSet::new();
txn.scan(
&table,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
&mut |k, _| {
if let Some(u) = ulid_from_fts_key(k) {
set.insert(u.to_bytes());
}
Ok(ScanFlow::Continue)
},
)?;
if set.is_empty() {
return Ok(vec![]);
}
ulid_sets.push(set);
}
ulid_sets.sort_by_key(std::collections::HashSet::len);
let intersection = ulid_sets
.into_iter()
.reduce(|a, b| a.into_iter().filter(|u| b.contains(u)).collect())
.unwrap_or_default();
let ulids: Vec<Ulid> = intersection.into_iter().map(Ulid::from_bytes).collect();
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::CompoundIndexEq { fields, start, end } => {
let field_refs: Vec<&str> = fields.iter().map(std::string::String::as_str).collect();
let table = compound_table_name(collection, &field_refs);
let ulids = table_range_scan(
txn,
&table,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
)?;
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::IndexOr { plans } => {
let mut seen: HashSet<[u8; 16]> = HashSet::new();
for sub_plan in plans {
check_deadline(deadline)?;
let ulids = collect_ulids(sub_plan, txn, collection, deadline)?;
for id_bytes in ulids {
seen.insert(id_bytes);
}
}
let ulids: Vec<Ulid> = seen.into_iter().map(Ulid::from_bytes).collect();
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
QueryPlan::IndexAnd { plans } => {
let mut sets: Vec<HashSet<[u8; 16]>> = Vec::with_capacity(plans.len());
for sub_plan in plans {
check_deadline(deadline)?;
let set: HashSet<[u8; 16]> = collect_ulids(sub_plan, txn, collection, deadline)?
.into_iter()
.collect();
if set.is_empty() {
return Ok(vec![]);
}
sets.push(set);
}
sets.sort_by_key(std::collections::HashSet::len);
let intersection = sets
.into_iter()
.reduce(|a, b| a.into_iter().filter(|u| b.contains(u)).collect())
.unwrap_or_default();
let ulids: Vec<Ulid> = intersection.into_iter().map(Ulid::from_bytes).collect();
fetch_filtered(
txn,
collection,
&ulids,
&matcher,
deadline,
limit,
&mut results,
)?;
}
}
Ok(results)
}
pub(crate) enum Matcher<'a> {
Contains {
field: &'a str,
tokens: HashSet<String>,
},
General {
filter: &'a Filter,
regexes: std::collections::HashMap<String, regex::Regex>,
},
}
impl<'a> Matcher<'a> {
pub(crate) fn new(filter: &'a Filter) -> Result<Self, TalaDbError> {
if let Filter::Contains(field, query) = filter {
return Ok(Matcher::Contains {
field,
tokens: tokenize(query).into_iter().collect(),
});
}
Ok(Matcher::General {
filter,
regexes: filter.compile_regex_cache()?,
})
}
pub(crate) fn matches(&self, doc: &Document) -> Result<bool, TalaDbError> {
match self {
Matcher::Contains { field, tokens } => {
if tokens.is_empty() {
return Ok(true);
}
Ok(match doc.get(field) {
Some(Value::Str(text)) => {
let doc_tokens: HashSet<String> = tokenize(text).into_iter().collect();
tokens.iter().all(|t| doc_tokens.contains(t))
}
_ => false,
})
}
Matcher::General { filter, regexes } => filter.matches_with_cache(doc, regexes),
}
}
}
#[allow(clippy::too_many_arguments)]
fn fetch_filtered(
txn: &dyn ReadTxn,
collection: &str,
ulids: &[Ulid],
matcher: &Matcher<'_>,
deadline: Option<Instant>,
limit: Option<usize>,
out: &mut Vec<Document>,
) -> Result<(), TalaDbError> {
let table = docs_table_name(collection);
let key_store: Vec<[u8; 16]> = ulids.iter().map(ulid::Ulid::to_bytes).collect();
for chunk in key_store.chunks(FETCH_CHUNK) {
check_deadline(deadline)?;
let keys: Vec<&[u8]> = chunk.iter().map(<[u8; 16]>::as_slice).collect();
for bytes in txn.get_many(&table, &keys)?.into_iter().flatten() {
let doc: Document = postcard::from_bytes(&bytes)?;
if matcher.matches(&doc)? {
out.push(doc);
if limit.is_some_and(|n| out.len() >= n) {
return Ok(());
}
}
}
}
Ok(())
}
fn collect_ulids(
plan: &QueryPlan,
txn: &dyn ReadTxn,
collection: &str,
deadline: Option<Instant>,
) -> Result<Vec<[u8; 16]>, TalaDbError> {
match plan {
QueryPlan::ById { ids } => Ok(ids.iter().map(ulid::Ulid::to_bytes).collect()),
QueryPlan::IndexEq { field, start, end } => {
let ulids = index_range_scan(
txn,
collection,
field,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
None,
)?;
Ok(ulids.into_iter().map(|u| u.to_bytes()).collect())
}
QueryPlan::IndexRange { field, start, end } => {
let start_ref = bound_as_ref(start);
let end_ref = bound_as_ref(end);
let ulids = index_range_scan(txn, collection, field, start_ref, end_ref, None)?;
Ok(ulids.into_iter().map(|u| u.to_bytes()).collect())
}
QueryPlan::IndexIn { field, ranges } => {
let mut result: Vec<[u8; 16]> = Vec::new();
for (start, end) in ranges {
let batch = index_range_scan(
txn,
collection,
field,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
None,
)?;
result.extend(batch.into_iter().map(|u| u.to_bytes()));
}
Ok(result)
}
QueryPlan::CompoundIndexEq { fields, start, end } => {
let field_refs: Vec<&str> = fields.iter().map(std::string::String::as_str).collect();
let table = compound_table_name(collection, &field_refs);
let ulids = table_range_scan(
txn,
&table,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
)?;
Ok(ulids.into_iter().map(|u| u.to_bytes()).collect())
}
QueryPlan::IndexOr { plans } => {
let mut ids = HashSet::new();
for plan in plans {
ids.extend(collect_ulids(plan, txn, collection, deadline)?);
}
Ok(ids.into_iter().collect())
}
QueryPlan::IndexAnd { plans } => {
let mut ids: Option<HashSet<[u8; 16]>> = None;
for plan in plans {
let next: HashSet<_> = collect_ulids(plan, txn, collection, deadline)?
.into_iter()
.collect();
if let Some(current) = &mut ids {
current.retain(|id| next.contains(id));
} else {
ids = Some(next);
}
}
Ok(ids.unwrap_or_default().into_iter().collect())
}
_ => {
let docs = execute(plan, &Filter::All, txn, collection, deadline)?;
Ok(docs.into_iter().map(|d| d.id.to_bytes()).collect())
}
}
}
pub fn count_matching(
plan: &QueryPlan,
filter: &Filter,
txn: &dyn ReadTxn,
collection: &str,
) -> Result<u64, TalaDbError> {
if plan_is_exact_for(plan, filter) {
return count_plan_entries(plan, txn, collection);
}
Ok(execute(plan, filter, txn, collection, None)?.len() as u64)
}
fn plan_is_exact_for(plan: &QueryPlan, filter: &Filter) -> bool {
match (plan, filter) {
(QueryPlan::IndexEq { field, .. }, Filter::Eq(f, v)) => {
if field != f {
return false;
}
match v {
Value::Array(_) | Value::Object(_) => false,
Value::Float(f) if f.is_nan() => false,
_ => true,
}
}
(QueryPlan::CompoundIndexEq { .. }, Filter::And(_)) => false,
_ => false,
}
}
fn count_plan_entries(
plan: &QueryPlan,
txn: &dyn ReadTxn,
collection: &str,
) -> Result<u64, TalaDbError> {
match plan {
QueryPlan::IndexEq { field, start, end } => {
let table = index_table_name(collection, field);
let mut n = 0u64;
txn.scan(
&table,
Bound::Included(start.as_slice()),
Bound::Included(end.as_slice()),
&mut |_, _| {
n += 1;
Ok(ScanFlow::Continue)
},
)?;
Ok(n)
}
_ => Ok(execute(plan, &Filter::All, txn, collection, None)?.len() as u64),
}
}
#[inline]
fn check_deadline(deadline: Option<Instant>) -> Result<(), TalaDbError> {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
return Err(TalaDbError::QueryTimeout);
}
Ok(())
}
fn bound_as_ref(b: &Bound<Vec<u8>>) -> Bound<&[u8]> {
match b {
Bound::Included(v) => Bound::Included(v.as_slice()),
Bound::Excluded(v) => Bound::Excluded(v.as_slice()),
Bound::Unbounded => Bound::Unbounded,
}
}
fn index_range_scan(
txn: &dyn ReadTxn,
collection: &str,
field: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
cap: Option<usize>,
) -> Result<Vec<Ulid>, TalaDbError> {
let table = index_table_name(collection, field);
table_range_scan_capped(txn, &table, start, end, cap)
}
pub(crate) struct IndexEntry {
pub value_prefix: Vec<u8>,
pub id: Ulid,
}
pub(crate) fn index_ordered_entries(
txn: &dyn ReadTxn,
collection: &str,
field: &str,
) -> Result<Vec<IndexEntry>, TalaDbError> {
let table = index_table_name(collection, field);
let mut out = Vec::new();
txn.scan(&table, Bound::Unbounded, Bound::Unbounded, &mut |k, _| {
if let Some(id) = ulid_from_index_key(k)
&& let Some(split) = k.len().checked_sub(16)
{
out.push(IndexEntry {
value_prefix: k[..split].to_vec(),
id,
});
}
Ok(ScanFlow::Continue)
})?;
Ok(out)
}
pub(crate) fn fetch_documents(
txn: &dyn ReadTxn,
collection: &str,
ulids: Vec<Ulid>,
) -> Result<Vec<Document>, TalaDbError> {
fetch_by_ulids(txn, collection, &ulids)
}
fn table_range_scan(
txn: &dyn ReadTxn,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<Vec<Ulid>, TalaDbError> {
table_range_scan_capped(txn, table, start, end, None)
}
fn table_range_scan_capped(
txn: &dyn ReadTxn,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
cap: Option<usize>,
) -> Result<Vec<Ulid>, TalaDbError> {
if cap == Some(0) {
return Ok(vec![]);
}
let mut ulids = Vec::new();
txn.scan(table, start, end, &mut |k, _| {
if let Some(u) = ulid_from_index_key(k) {
ulids.push(u);
}
Ok(match cap {
Some(n) if ulids.len() >= n => ScanFlow::Stop,
_ => ScanFlow::Continue,
})
})?;
Ok(ulids)
}
pub(crate) fn fetch_by_ulids(
txn: &dyn ReadTxn,
collection: &str,
ulids: &[Ulid],
) -> Result<Vec<Document>, TalaDbError> {
let table = docs_table_name(collection);
let key_store: Vec<[u8; 16]> = ulids.iter().map(ulid::Ulid::to_bytes).collect();
let mut docs = Vec::with_capacity(ulids.len());
for chunk in key_store.chunks(FETCH_CHUNK) {
let keys: Vec<&[u8]> = chunk.iter().map(<[u8; 16]>::as_slice).collect();
for bytes in txn.get_many(&table, &keys)?.into_iter().flatten() {
docs.push(postcard::from_bytes(&bytes)?);
}
}
Ok(docs)
}
pub(crate) fn visit_matching_ids(
plan: &QueryPlan,
filter: &Filter,
txn: &dyn ReadTxn,
collection: &str,
indexes: &[IndexDef],
compounds: &[CompoundIndexDef],
accept: &mut dyn FnMut([u8; 16]) -> Result<(), TalaDbError>,
) -> Result<(), TalaDbError> {
let matcher = Matcher::new(filter)?;
if super::index_filter::visit_unique(filter, indexes, compounds, txn, collection, accept)? {
return Ok(());
}
let fields = super::filter_document::fields(filter);
let mut accept_doc = |bytes: &[u8]| -> Result<bool, TalaDbError> {
let doc = super::filter_document::decode(bytes, &fields)?;
if matcher.matches(&doc)? {
accept(doc.id.to_bytes())?;
}
Ok(true)
};
if let QueryPlan::FtsSearch { field, tokens } = plan {
return visit_fts_documents(txn, collection, field, tokens, &mut accept_doc).map(|_| ());
}
let table = docs_table_name(collection);
let mut accept_id = |id: [u8; 16]| -> Result<(), TalaDbError> {
if let Some(bytes) = txn.get(&table, &id)? {
accept_doc(&bytes)?;
}
Ok(())
};
if visit_index_seed(filter, indexes, compounds, txn, collection, &mut accept_id)? {
return Ok(());
}
txn.scan(
&table,
Bound::Unbounded,
Bound::Unbounded,
&mut |_, bytes| {
accept_doc(bytes)?;
Ok(ScanFlow::Continue)
},
)
}
fn visit_index_seed(
filter: &Filter,
indexes: &[IndexDef],
compounds: &[CompoundIndexDef],
txn: &dyn ReadTxn,
collection: &str,
accept: &mut dyn FnMut([u8; 16]) -> Result<(), TalaDbError>,
) -> Result<bool, TalaDbError> {
if let Filter::And(children) = filter {
for child in children {
if super::index_filter::visit_unique(
child, indexes, compounds, txn, collection, accept,
)? || visit_index_seed(child, indexes, compounds, txn, collection, accept)?
{
return Ok(true);
}
}
}
Ok(false)
}
fn visit_plan_documents(
plan: &QueryPlan,
txn: &dyn ReadTxn,
collection: &str,
accept: &mut impl FnMut(&[u8]) -> Result<bool, TalaDbError>,
) -> Result<bool, TalaDbError> {
match plan {
QueryPlan::FtsSearch { field, tokens } => {
visit_fts_documents(txn, collection, field, tokens, accept)
}
QueryPlan::ById { ids } => {
let table = docs_table_name(collection);
for id in ids {
if let Some(bytes) = txn.get(&table, &id.to_bytes())?
&& !accept(&bytes)?
{
return Ok(false);
}
}
Ok(true)
}
QueryPlan::IndexOr { plans } => {
for plan in plans {
if !visit_plan_documents(plan, txn, collection, accept)? {
return Ok(false);
}
}
Ok(true)
}
QueryPlan::IndexAnd { plans } if !plans.is_empty() => {
visit_plan_documents(&plans[0], txn, collection, accept)
}
_ => {
let docs = docs_table_name(collection);
let mut scan = |table: &str, start: Bound<&[u8]>, end: Bound<&[u8]>, indexed: bool| {
let mut more = true;
txn.scan(table, start, end, &mut |key, value| {
let bytes = if indexed {
ulid_from_index_key(key)
.map(|id| txn.get(&docs, &id.to_bytes()))
.transpose()?
.flatten()
} else {
None
};
if !indexed {
more = accept(value)?;
} else if let Some(bytes) = bytes {
more = accept(&bytes)?;
}
Ok(if more {
ScanFlow::Continue
} else {
ScanFlow::Stop
})
})?;
Ok(more)
};
match plan {
QueryPlan::IndexEq { field, start, end } => scan(
&index_table_name(collection, field),
Bound::Included(start),
Bound::Included(end),
true,
),
QueryPlan::IndexRange { field, start, end } => scan(
&index_table_name(collection, field),
start.as_ref().map(Vec::as_slice),
end.as_ref().map(Vec::as_slice),
true,
),
QueryPlan::IndexIn { field, ranges } => {
let table = index_table_name(collection, field);
for (start, end) in ranges {
if !scan(&table, Bound::Included(start), Bound::Included(end), true)? {
return Ok(false);
}
}
Ok(true)
}
QueryPlan::CompoundIndexEq { fields, start, end } => {
let table = crate::index::compound_table_name(
collection,
&fields.iter().map(String::as_str).collect::<Vec<_>>(),
);
scan(&table, Bound::Included(start), Bound::Included(end), true)
}
_ => scan(&docs, Bound::Unbounded, Bound::Unbounded, false),
}
}
}
}
fn visit_fts_documents(
txn: &dyn ReadTxn,
collection: &str,
field: &str,
tokens: &[String],
accept: &mut impl FnMut(&[u8]) -> Result<bool, TalaDbError>,
) -> Result<bool, TalaDbError> {
let Some(mut seed) = tokens.first() else {
return Ok(true);
};
let table = fts_table_name(collection, field);
if tokens.len() > 1 {
let mut smallest = usize::MAX;
for token in tokens {
let (start, end) = fts_token_range(token);
let mut count = 0;
txn.scan(
&table,
Bound::Included(&start),
Bound::Included(&end),
&mut |_, _| {
count += 1;
Ok(if count >= 64 {
ScanFlow::Stop
} else {
ScanFlow::Continue
})
},
)?;
if count == 0 {
return Ok(true);
}
if count < smallest {
smallest = count;
seed = token;
}
}
}
let (start, end) = fts_token_range(seed);
let docs = docs_table_name(collection);
let mut more = true;
txn.scan(
&table,
Bound::Included(&start),
Bound::Included(&end),
&mut |key, _| {
if let Some(id) = ulid_from_fts_key(key)
&& let Some(bytes) = txn.get(&docs, &id.to_bytes())?
&& !accept(&bytes)?
{
more = false;
return Ok(ScanFlow::Stop);
}
Ok(ScanFlow::Continue)
},
)?;
Ok(more)
}