uqa-storage 0.3.7

Document store, inverted index, IVF/HNSW vectors, B-tree, R*Tree, catalog
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Batch atomicity, untouched allocation ownership, and ordered update equivalence.

use super::*;
use crate::InvertedIndex;
use proptest::prelude::*;
use uqa_analysis::whitespace_analyzer;

fn fields(text: &str) -> BTreeMap<String, String> {
    BTreeMap::from([("body".into(), text.into())])
}

fn seeded() -> MemoryInvertedIndex {
    let mut index = MemoryInvertedIndex::new(whitespace_analyzer());
    index.add_document(1, fields("old shared")).unwrap();
    index.add_document(2, fields("other shared")).unwrap();
    index.add_document(99, fields("unrelated shared")).unwrap();
    index
}

fn assert_state(left: &MemoryInvertedIndex, right: &MemoryInvertedIndex) {
    assert_eq!(left.state.doc_count, right.state.doc_count);
    assert_eq!(left.state.total_length, right.state.total_length);
    assert_eq!(left.state.field_doc_counts, right.state.field_doc_counts);
    assert_eq!(left.state.doc_fields, right.state.doc_fields);
    assert_eq!(left.state.doc_terms, right.state.doc_terms);
    assert_eq!(
        format!("{:?}", left.state.index),
        format!("{:?}", right.state.index)
    );
}

#[test]
fn batches_preserve_untouched_posting_allocations() {
    let mut index = seeded();
    let key = ("body".into(), crate::TokenTermKey::from_text("shared"));
    let occurrences = index.state.index[&key][&99].occurrences.as_ptr();
    let positions = index.state.index[&key][&99]
        .projection
        .payload
        .positions
        .as_ptr();
    index.try_add_documents(Vec::new()).unwrap();
    assert_eq!(
        index.state.index[&key][&99].occurrences.as_ptr(),
        occurrences
    );
    index
        .try_add_documents(vec![(1, fields("new shared")), (3, fields("new"))])
        .unwrap();
    assert_eq!(
        index.state.index[&key][&99].occurrences.as_ptr(),
        occurrences
    );
    assert_eq!(
        index.state.index[&key][&99]
            .projection
            .payload
            .positions
            .as_ptr(),
        positions
    );
    assert_eq!(index.doc_count().unwrap(), 4);
    assert_eq!(index.doc_freq("body", "old").unwrap(), 0);
    assert_eq!(index.doc_freq("body", "new").unwrap(), 2);
}

#[test]
fn late_document_and_field_counter_failures_publish_nothing() {
    for field_overflow in [false, true] {
        let mut index = seeded();
        if field_overflow {
            Arc::make_mut(&mut index.state)
                .total_length
                .insert("body".into(), u64::MAX - 1);
        } else {
            Arc::make_mut(&mut index.state).doc_count = u64::MAX - 1;
        }
        let before = index.clone();
        let error = index
            .try_add_documents(vec![(3, fields("next")), (4, fields("overflow"))])
            .unwrap_err();
        assert!(error.to_string().contains(if field_overflow {
            "total field length"
        } else {
            "document count"
        }));
        assert_state(&index, &before);
    }
}

#[test]
fn corrupt_affected_postings_or_reverse_metadata_publish_nothing() {
    for missing_field in [false, true] {
        let mut index = seeded();
        if missing_field {
            Arc::make_mut(&mut index.state).doc_fields.remove(&2);
        } else {
            Arc::make_mut(&mut index.state)
                .index
                .remove(&("body".into(), crate::TokenTermKey::from_text("other")));
        }
        let before = index.clone();
        assert!(index
            .try_add_documents(vec![(1, fields("new")), (2, fields("new"))])
            .is_err());
        assert_state(&index, &before);
    }
}

proptest! {
    #[test]
    fn repeated_ids_deletions_and_field_moves_match_ordered_point_updates(
        operations in prop::collection::vec((0_u8..12, 0_u8..4, 0_u8..8), 0..40)
    ) {
        let mut index = seeded();
        let mut expected = index.clone();
        let documents: Vec<_> = operations.into_iter().map(|(id, mask, length)| {
            let mut values = BTreeMap::new();
            if mask & 1 != 0 { values.insert("body".into(), "a b ".repeat(usize::from(length))); }
            if mask & 2 != 0 { values.insert("title".into(), "c ".repeat(usize::from(length))); }
            (u64::from(id), values)
        }).collect();
        for (id, fields) in &documents {
            expected.add_document(*id, fields.clone()).unwrap();
        }
        index.try_add_documents(documents).unwrap();
        assert_state(&index, &expected);
    }
}