uqa-storage 0.5.3

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

//! Explicit source-rebuild requirement and retirement of legacy index namespaces.

use super::super::codec::{
    doc_length_key_prefix, field_stats_key_prefix, posting_cluster_positions_key_prefix,
    posting_cluster_score_key_prefix, posting_document_key_prefix, posting_key_prefix,
    reverse_posting_key_prefix,
};
use super::{keys, other_error, KeyValueBatch, OccurrenceRead, StorageBackendResult};

#[cfg(test)]
mod tests;

/// The namespace list is fixed; only encoded table bytes allocate, and each prefix uses the reader's canonical controlled encoder.
pub(super) fn retained_prefixes(
    table: &str,
    control: &crate::read_control::StorageReadControl,
) -> StorageBackendResult<[uqa_core::memory::BudgetedVec<u8>; 8]> {
    use crate::key_value::{
        TAG_DOC_LENGTH, TAG_FIELD_STATS, TAG_OCCURRENCE_INDEX, TAG_POSTING,
        TAG_POSTING_CLUSTER_POSITIONS, TAG_POSTING_CLUSTER_SCORE, TAG_POSTING_DOCUMENT,
        TAG_REVERSE_POSTING,
    };
    let tags = [
        TAG_POSTING,
        TAG_POSTING_CLUSTER_SCORE,
        TAG_POSTING_CLUSTER_POSITIONS,
        TAG_POSTING_DOCUMENT,
        TAG_DOC_LENGTH,
        TAG_FIELD_STATS,
        TAG_REVERSE_POSTING,
        TAG_OCCURRENCE_INDEX,
    ];
    let mut prefixes =
        std::array::from_fn(|_| uqa_core::memory::BudgetedVec::new(control.memory()));
    for (prefix, tag) in prefixes.iter_mut().zip(tags) {
        *prefix = keys::encoding::controlled(table, tag, None, &[], control)?;
    }
    Ok(prefixes)
}

pub(super) fn legacy_prefixes(table: &str) -> StorageBackendResult<Vec<Vec<u8>>> {
    Ok(vec![
        posting_key_prefix(table)?,
        posting_cluster_score_key_prefix(table)?,
        posting_cluster_positions_key_prefix(table)?,
        posting_document_key_prefix(table)?,
        doc_length_key_prefix(table)?,
        field_stats_key_prefix(table)?,
        reverse_posting_key_prefix(table)?,
    ])
}

impl OccurrenceRead<'_> {
    pub(super) fn needs_source_rebuild(&self) -> StorageBackendResult<bool> {
        self.store.control().check()?;
        if let Some(required) = self.format_requires_rebuild.get() {
            return Ok(required);
        }
        let required = self.read_source_rebuild_requirement()?;
        self.format_requires_rebuild.set(Some(required));
        Ok(required)
    }

    fn read_source_rebuild_requirement(&self) -> StorageBackendResult<bool> {
        let format = self
            .store
            .get(&keys::kind_prefix(self.table, keys::FORMAT)?)?;
        if format.as_deref() == Some(b"source-rebuild") {
            return Ok(true);
        }
        if format
            .as_deref()
            .is_some_and(|format| format != keys::FORMAT_NAME)
        {
            return Err(other_error("unsupported occurrence index format"));
        }
        for prefix in legacy_prefixes(self.table)? {
            if self
                .store
                .contains_prefix_budgeted(&prefix, self.store.control())?
            {
                return Ok(true);
            }
        }
        if format.is_none()
            && self
                .store
                .contains_prefix_budgeted(&keys::table_prefix(self.table)?, self.store.control())?
        {
            return Err(other_error("occurrence index format marker is missing"));
        }
        Ok(false)
    }

    pub(super) fn require_graph_format(&self) -> StorageBackendResult<()> {
        if self.needs_source_rebuild()? {
            return Err(other_error(
                "legacy positional data requires an atomic source rebuild",
            ));
        }
        Ok(())
    }

    pub(super) fn clear_index_batch(
        &self,
        batch: &mut dyn KeyValueBatch,
    ) -> StorageBackendResult<()> {
        batch.reset_occurrences(self.table)?;
        batch.delete_prefix(&keys::table_prefix(self.table)?)?;
        for prefix in legacy_prefixes(self.table)? {
            batch.delete_prefix(&prefix)?;
        }
        batch.put(
            &keys::kind_prefix(self.table, keys::FORMAT)?,
            keys::FORMAT_NAME,
        )
    }
}