Skip to main content

uqa_storage/key_value/vector_index/
guards.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Field transitions fence vector writers while ordinary document writers remain mergeable.
8
9use super::{KeyValueBatch, KeyValueVectorIndex, StorageBackendResult};
10use uqa_core::memory::{BudgetedVec, MemoryError};
11
12pub(crate) const ROOT: &[u8] = b"\0uqa-vector-field-guards-v1\0";
13
14pub(crate) fn reference_key(
15    vectors: &[u8],
16    control: &crate::read_control::StorageReadControl,
17) -> StorageBackendResult<BudgetedVec<u8>> {
18    control.check()?;
19    let mut key = BudgetedVec::new(control.memory());
20    key.extend_from_slice(ROOT)?;
21    key.push(1)?;
22    key.extend_from_slice(vectors)?;
23    Ok(key)
24}
25
26pub struct KeyValueVectorFieldGuards;
27
28impl crate::mvcc::VectorFieldGuardLayout for KeyValueVectorFieldGuards {
29    fn prefix(
30        &self,
31        control: &crate::read_control::StorageReadControl,
32    ) -> crate::mvcc::VersionResult<BudgetedVec<u8>> {
33        let mut prefix = BudgetedVec::new(control.memory());
34        prefix.extend_from_slice(ROOT)?;
35        Ok(prefix)
36    }
37
38    fn reference(
39        &self,
40        key: &[u8],
41        control: &crate::read_control::StorageReadControl,
42    ) -> crate::mvcc::VersionResult<Option<crate::mvcc::VectorFieldGuard>> {
43        use crate::key_value::codec::read_segment;
44        use crate::mvcc::{VectorFieldGuard, VersionError};
45
46        control.check()?;
47        let suffix = key
48            .strip_prefix(ROOT)
49            .ok_or(VersionError::InvalidEncoding("foreign vector field guard"))?;
50        let (&kind, vectors) = suffix.split_first().ok_or(VersionError::InvalidEncoding(
51            "truncated vector field guard",
52        ))?;
53        if kind > 1 || vectors.first() != Some(&crate::key_value::TAG_VECTOR) {
54            return Err(VersionError::InvalidEncoding("invalid vector field guard"));
55        }
56        let mut offset = 1;
57        for _ in 0..2 {
58            std::str::from_utf8(read_segment(vectors, &mut offset)?).map_err(|_| {
59                VersionError::InvalidEncoding("vector field guard name is not UTF-8")
60            })?;
61        }
62        if offset != vectors.len() {
63            return Err(VersionError::InvalidEncoding(
64                "vector field guard has trailing bytes",
65            ));
66        }
67        if kind == 0 {
68            return Ok(None);
69        }
70        let copy = |bytes: &[u8]| -> crate::mvcc::VersionResult<BudgetedVec<u8>> {
71            let mut output = BudgetedVec::new(control.memory());
72            output.extend_from_slice(bytes)?;
73            Ok(output)
74        };
75        let mut lifetime = copy(key)?;
76        lifetime[ROOT.len()] = 0;
77        Ok(Some(VectorFieldGuard {
78            lifetime,
79            references: copy(key)?,
80            reference_value: copy(&[1])?,
81            vectors: copy(vectors)?,
82        }))
83    }
84}
85
86impl KeyValueVectorIndex {
87    pub(super) fn coordinate_field(
88        &self,
89        batch: &mut dyn KeyValueBatch,
90        structural: bool,
91    ) -> StorageBackendResult<()> {
92        if !self.store.transaction_model().is_versioned() {
93            return Ok(());
94        }
95        let control = self.store.retention_control().ok_or_else(|| {
96            super::other_error("versioned vector owner has no retention allowance")
97        })?;
98        control.check()?;
99        let bytes = self
100            .table
101            .len()
102            .checked_add(self.field.len())
103            .and_then(|size| size.checked_add(32))
104            .and_then(|size| size.checked_mul(2))
105            .ok_or(MemoryError::SizeOverflow)?;
106        let _workspace = control.memory().reserve(bytes)?;
107        let prefix = super::vector_field_prefix(&self.table, &self.field)?;
108        let mut lifetime = BudgetedVec::new(control.memory());
109        lifetime.extend_from_slice(ROOT)?;
110        lifetime.push(0)?;
111        lifetime.extend_from_slice(&prefix)?;
112        let references = reference_key(&prefix, &control)?;
113        if structural {
114            batch.fence_record(&lifetime)?;
115            batch.fence_record(&references)
116        } else {
117            batch.require_unchanged(&lifetime)?;
118            batch.touch_marker(&references, &[1])
119        }
120    }
121}