Skip to main content

uqa_storage/document_store/
identifiers.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Document identity allocation and migration of the legacy per-name watermark.
8
9use std::num::NonZeroU64;
10
11use crate::mvcc::{IdentifierAllocator, IdentifierRequest, ObservedIdentifier};
12use crate::{CatalogFacade, KeyValueBatch, StorageBackendError, StorageBackendResult};
13
14pub mod conformance;
15
16#[cfg(test)]
17mod tests;
18
19/// The durable object and storage generation of a table, which name the namespace of its document identities. A claim about a table's identities, such as that one was never used, holds for the namespace its watermark was read in and for no other.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct DocumentIdNamespace {
22    pub object: [u8; 16],
23    pub generation: [u8; 16],
24}
25
26/// A document namespace follows a table's durable object and storage generation through renames. A missing durable allocator retains the caller's serialized in-memory watermark contract.
27pub struct DocumentIdAllocator<'a> {
28    durable: Option<&'a dyn IdentifierAllocator>,
29    namespace: [u8; 41],
30}
31
32impl<'a> DocumentIdAllocator<'a> {
33    pub fn new(
34        durable: Option<&'a dyn IdentifierAllocator>,
35        object: [u8; 16],
36        generation: [u8; 16],
37    ) -> StorageBackendResult<Self> {
38        if durable.is_some() {
39            validate_identity(object, generation)?;
40        }
41        let namespace = document_namespace(object, generation);
42        Ok(Self { durable, namespace })
43    }
44
45    pub fn is_durable(&self) -> bool {
46        self.durable.is_some()
47    }
48
49    /// Reserve one identity at or above the restored local floor. The local cache changes only after successful durable reservation; private rollback cannot reclaim a returned identity.
50    pub fn allocate(&self, next: &mut u128) -> StorageBackendResult<u64> {
51        let minimum = u64::try_from(*next)
52            .map_err(|_| StorageBackendError::Other("document id space is exhausted".into()))?;
53        let id = match self.durable {
54            Some(allocator) => allocator
55                .allocate_identifiers(
56                    &self.namespace,
57                    IdentifierRequest::Reserve {
58                        minimum,
59                        maximum: u64::MAX,
60                        count: NonZeroU64::MIN,
61                    },
62                )?
63                .watermark(),
64            None => minimum,
65        };
66        *next = u128::from(id) + 1;
67        Ok(id)
68    }
69
70    /// Advance the local floor past a supplied document identity. The row's own write observes the identity durably in the batch that publishes it ([`observe_document_id`]), so a supplied identity costs no separate durable transaction; a reservation reads the durable watermark itself, so the local floor need not include other sessions' reservations. The restored floor is retained so deleting older rows cannot make their identities available again.
71    pub fn observe(next: &mut u128, id: u64) -> StorageBackendResult<()> {
72        watermark_identity(*next)?;
73        *next = (*next).max(u128::from(id) + 1);
74        Ok(())
75    }
76
77    /// Observe a supplied document identity durably now, ahead of the rows that will observe it when they are written. A statement that supplies ascending identities observes the greatest one this way, which covers the observation of each row. The answer tells which identities no document of this table ever had; a table without a durable allocator cannot tell.
78    pub fn observe_durably(&self, id: u64) -> StorageBackendResult<ObservedIdentifier> {
79        match self.durable {
80            Some(allocator) => allocator.observe_identifier(&self.namespace, id),
81            None => Ok(ObservedIdentifier::Covered),
82        }
83    }
84
85    /// Seed existing data and legacy reservations before exposing a migrated table to new sessions. The one-past-last representation preserves the exhausted full-width domain.
86    pub fn synchronize(&self, next: &mut u128) -> StorageBackendResult<()> {
87        let observed = watermark_identity(*next)?;
88        if let Some(allocator) = self.durable {
89            let allocated = allocator
90                .allocate_identifiers(&self.namespace, IdentifierRequest::Observe(observed))?;
91            *next = u128::from(allocated.watermark()) + 1;
92        }
93        Ok(())
94    }
95
96    /// Persist the restored/allocation floor. Durable allocators retire the legacy name-bound value in the catalog transaction after seeding its independent namespace; serialized backends retain that legacy format.
97    pub fn persist(
98        &self,
99        catalog: &dyn CatalogFacade,
100        table: &str,
101        next: &mut u128,
102    ) -> StorageBackendResult<()> {
103        self.synchronize(next)?;
104        let key = legacy_document_id_metadata_key(table);
105        if self.is_durable() {
106            if catalog
107                .get_metadata(&key)?
108                .is_some_and(|value| !value.is_empty())
109            {
110                catalog.set_metadata(&key, "")?;
111            }
112            Ok(())
113        } else {
114            catalog.set_metadata(&key, &next.to_string())
115        }
116    }
117}
118
119/// The last identity a one-past-last local floor covers; a floor outside `1..=u64::MAX + 1` is invalid.
120fn watermark_identity(next: u128) -> StorageBackendResult<u64> {
121    next.checked_sub(1)
122        .and_then(|value| u64::try_from(value).ok())
123        .ok_or_else(|| StorageBackendError::Other("invalid document id watermark".into()))
124}
125
126/// Include a supplied identity in the batch evaluated by its document owner. The batch makes the observation durable before its rows can be published, without reentering the selected storage session.
127pub fn observe_document_id(
128    batch: &mut dyn KeyValueBatch,
129    object: [u8; 16],
130    generation: [u8; 16],
131    id: u64,
132) -> StorageBackendResult<()> {
133    validate_identity(object, generation)?;
134    batch.observe_identifier(&document_namespace(object, generation), id)
135}
136
137/// Preserve already reserved document identities when table data adopts a different object or storage generation. Observations happen before the evaluated rows are published and survive private rollback.
138pub fn inherit_document_ids(
139    batch: &mut dyn KeyValueBatch,
140    from: ([u8; 16], [u8; 16]),
141    to: ([u8; 16], [u8; 16]),
142) -> StorageBackendResult<()> {
143    validate_identity(from.0, from.1)?;
144    validate_identity(to.0, to.1)?;
145    batch.inherit_identifiers(
146        &document_namespace(from.0, from.1),
147        &document_namespace(to.0, to.1),
148    )
149}
150
151fn validate_identity(object: [u8; 16], generation: [u8; 16]) -> StorageBackendResult<()> {
152    if object == [0; 16] || generation == [0; 16] {
153        return Err(StorageBackendError::Other(
154            "document allocation requires nonzero table and storage identities".into(),
155        ));
156    }
157    Ok(())
158}
159
160fn document_namespace(object: [u8; 16], generation: [u8; 16]) -> [u8; 41] {
161    let mut namespace = [0; 41];
162    namespace[..9].copy_from_slice(b"document\x01");
163    namespace[9..25].copy_from_slice(&object);
164    namespace[25..].copy_from_slice(&generation);
165    namespace
166}
167
168pub fn legacy_document_id_metadata_key(table: &str) -> String {
169    format!("uqa.table_next_id.v1:{table}")
170}
171
172/// Combine visible rows with a persisted or retained reservation floor without losing the full-width exhaustion sentinel.
173pub fn restored_document_id_watermark(maximum: u64, reserved_next: Option<u128>) -> u128 {
174    reserved_next.unwrap_or(1).max(u128::from(maximum) + 1)
175}
176
177pub fn load_legacy_document_id_watermark(
178    catalog: &dyn CatalogFacade,
179    table: &str,
180) -> StorageBackendResult<Option<u128>> {
181    let Some(value) = catalog.get_metadata(&legacy_document_id_metadata_key(table))? else {
182        return Ok(None);
183    };
184    if value.is_empty() {
185        return Ok(None);
186    }
187    value.parse::<u128>().map(Some).map_err(|error| {
188        StorageBackendError::Other(format!(
189            "invalid persisted next id for table `{table}`: {error}"
190        ))
191    })
192}