use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use uqa_core::memory::Budgeted;
#[test]
fn index_field_selection_borrows_names_and_retains_its_bounded_projection() {
let index = MemoryInvertedIndex::new(uqa_analysis::whitespace_analyzer());
let fields = ["z".repeat(16 << 10), "a".into(), "a".into()];
let mut schema = schema(&[], &index);
schema.text_fields = &fields;
let dimensions = BTreeMap::from([("v".into(), 2), ("a".into(), 2)]);
schema.vector_dimensions = &dimensions;
let control = StorageReadControl::with_limit(1024);
let selected = index_fields(&schema, &control).unwrap();
assert_eq!(&*selected, &["a", "v", fields[0].as_str()]);
assert!(std::ptr::eq(selected[2], fields[0].as_str()));
let retained = control.memory().used();
assert!(retained > 0);
let occupied = control
.memory()
.reserve(control.memory().limit() - retained)
.unwrap();
let error = index_fields(&schema, &control).unwrap_err();
assert_eq!(error.sqlstate(), Some("53200"));
assert_eq!(control.memory().used(), control.memory().limit());
assert_eq!(&*selected, &["a", "v", fields[0].as_str()]);
drop(occupied);
control.cancellation().cancel();
assert_eq!(
index_fields(&schema, &control).unwrap_err().sqlstate(),
Some("57014")
);
control.cancellation().reset();
assert_eq!(control.memory().used(), retained);
drop(selected);
assert_eq!(control.memory().used(), 0);
let names: Vec<_> = (0..128).map(|index| format!("field_{index}")).collect();
schema.text_fields = &names;
assert_eq!(
index_fields(&schema, &control).unwrap_err().sqlstate(),
Some("53200")
);
assert_eq!(control.memory().used(), 0);
}
#[derive(Clone)]
struct DecodingSource {
rows: Arc<MemoryDocumentStore>,
control: StorageReadControl,
scratch: Option<usize>,
headroom: usize,
visited: Arc<AtomicUsize>,
cancel_after_stop: bool,
}
impl DocumentStore for DecodingSource {
fn snapshot(&self) -> StorageBackendResult<Arc<dyn DocumentStore>> {
Ok(Arc::new(self.clone()))
}
fn put_stored(&mut self, _: DocId, _: StoredDocument) -> StorageBackendResult<()> {
panic!("immutable source")
}
fn delete(&mut self, _: DocId) -> StorageBackendResult<()> {
panic!("immutable source")
}
fn clear(&mut self) -> StorageBackendResult<()> {
panic!("immutable source")
}
fn get_stored(&self, _: DocId) -> StorageBackendResult<Option<StoredDocument>> {
panic!("index reconstruction must use the borrowed projection")
}
fn doc_ids(&self) -> StorageBackendResult<Vec<DocId>> {
panic!("index reconstruction must page identities")
}
fn next_doc_ids(&self, after: Option<DocId>, limit: usize) -> StorageBackendResult<Vec<DocId>> {
self.rows.next_doc_ids(after, limit)
}
fn next_doc_ids_controlled(
&self,
after: Option<DocId>,
limit: usize,
control: &StorageReadControl,
) -> StorageBackendResult<uqa_core::memory::BudgetedVec<DocId>> {
self.rows.next_doc_ids_controlled(after, limit, control)
}
fn len(&self) -> StorageBackendResult<usize> {
self.rows.len()
}
fn field_presence_controlled(
&self,
ids: &[DocId],
fields: &[&str],
control: &StorageReadControl,
) -> StorageBackendResult<uqa_core::memory::BudgetedVec<bool>> {
self.rows.field_presence_controlled(ids, fields, control)
}
fn for_each_fields_multi_ref_with_presence(
&self,
ids: &[DocId],
fields: &[&str],
visitor: &mut dyn FnMut(DocId, bool, &[&Value]) -> bool,
) -> StorageBackendResult<()> {
self.control.check()?;
let bytes = self.scratch.unwrap_or_else(|| {
self.control.memory().limit() - self.control.memory().used() - self.headroom
});
let reservation = self.control.memory().reserve(bytes)?;
let _scratch = Budgeted::new(vec![0_u8; bytes], reservation);
self.rows.for_each_fields_multi_ref_with_presence(
ids,
fields,
&mut |id, present, values| {
self.visited.fetch_add(1, Ordering::Relaxed);
let keep_going = visitor(id, present, values);
if !keep_going && self.cancel_after_stop {
self.control.cancellation().cancel();
}
keep_going
},
)?;
self.control.check()
}
}
fn source(
control: &StorageReadControl,
scratch: Option<usize>,
values: &[Value],
) -> DecodingSource {
let mut rows = MemoryDocumentStore::new();
for (id, value) in values.iter().enumerate() {
rows.put_stored(
u64::try_from(id + 1).unwrap(),
document(&[("value", value.clone())], 41),
)
.unwrap();
}
DecodingSource {
rows: Arc::new(rows),
control: control.clone(),
scratch,
headroom: 256,
visited: Arc::new(AtomicUsize::new(0)),
cancel_after_stop: false,
}
}
#[test]
fn provider_projection_allowance_remains_live_during_index_reconstruction() {
for vector in [false, true] {
let control = StorageReadControl::with_limit(64 * 1024);
let columns = columns(if vector {
"CREATE TABLE t (value VECTOR(1024))"
} else {
"CREATE TABLE t (value TEXT)"
});
let index = MemoryInvertedIndex::new(uqa_analysis::whitespace_analyzer());
let mut schema = schema(&columns, &index);
let text_fields = ["value".into()];
let dimensions = BTreeMap::from([("value".into(), 1024)]);
let value = if vector {
schema.vector_dimensions = &dimensions;
Value::List(vec![Value::Float(1.0); 1024])
} else {
schema.text_fields = &text_fields;
Value::Str("retained input ".repeat(16))
};
let source = source(&control, None, &[value]);
let visited = Arc::clone(&source.visited);
let result = retain(
Arc::new(source),
&columns,
&schema,
DocumentChanges::default(),
&control,
);
let error = result
.err()
.expect("provider and consumer share one allowance");
assert_eq!(error.sqlstate(), Some("53200"), "{error}");
assert_eq!(visited.load(Ordering::Relaxed), 1);
assert_eq!(control.memory().used(), 0);
}
}
#[test]
fn index_reconstruction_stops_at_its_first_error_before_later_provider_cancellation() {
for (headroom, defaults) in [(0, false), (256, false), (0, true), (256, true)] {
let control = StorageReadControl::with_limit(64 * 1024);
let mut columns = columns("CREATE TABLE t (value TEXT)");
if defaults {
columns[0].missing_value = Some(Value::Str("missing".into()));
}
let index = MemoryInvertedIndex::new(uqa_analysis::whitespace_analyzer());
let mut schema = schema(&columns, &index);
let text_fields = ["value".into()];
schema.text_fields = &text_fields;
let mut source = source(
&control,
None,
&[Value::Str("first".into()), Value::Str("second".into())],
);
source.headroom = headroom;
source.cancel_after_stop = true;
let visited = Arc::clone(&source.visited);
let result = retain(
Arc::new(source),
&columns,
&schema,
DocumentChanges::default(),
&control,
);
let error = result.err().expect("consumer cannot exceed the allowance");
assert_eq!(error.sqlstate(), Some("53200"), "{error}");
assert_eq!(visited.load(Ordering::Relaxed), 1);
assert!(control.cancellation().is_cancelled());
assert_eq!(control.memory().used(), 0);
}
}
#[test]
fn successful_reconstruction_releases_decode_scratch_and_retains_index_output() {
let control = StorageReadControl::with_limit(64 * 1024);
let columns = columns("CREATE TABLE t (value TEXT)");
let index = MemoryInvertedIndex::new(uqa_analysis::whitespace_analyzer());
let mut schema = schema(&columns, &index);
let text_fields = ["value".into(), "value".into()];
schema.text_fields = &text_fields;
let source = source(
&control,
Some(16 * 1024),
&[Value::Str("first".into()), Value::Str("second".into())],
);
let visited = Arc::clone(&source.visited);
let view = retain(
Arc::new(source),
&columns,
&schema,
DocumentChanges::default(),
&control,
)
.unwrap();
assert_eq!(visited.load(Ordering::Relaxed), 2);
assert_eq!(view.document_count, 2);
assert!(control.memory().used() < 16 * 1024);
for (id, term) in [(1, "first"), (2, "second")] {
assert_eq!(
view.text
.get_occurrences(id, "value", &uqa_storage::TokenTermKey::from_text(term))
.unwrap()
.len(),
1
);
}
drop(view);
assert_eq!(control.memory().used(), 0);
}