Skip to main content

uqa_storage/mvcc/
persistence.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Atomic persistence and durable transaction outcomes, independent of physical drivers.
8
9use std::sync::Arc;
10
11use uqa_core::memory::BudgetedVec;
12
13use crate::read_control::StorageReadControl;
14use crate::StorageBackendError;
15
16use super::{
17    CommitSequence, CommittedRecordSnapshot, PreparedRecordCommit, ScannedRecord, VersionError,
18    VersionResult,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct DatabaseId([u8; 16]);
23
24impl DatabaseId {
25    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
26        Self(bytes)
27    }
28    pub const fn as_bytes(self) -> [u8; 16] {
29        self.0
30    }
31}
32
33/// A durable, non-reused allocation in one database incarnation; it is not a SQL-visible XID.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct StorageTransactionId {
36    database: DatabaseId,
37    allocation: u64,
38}
39
40impl StorageTransactionId {
41    pub fn new(database: DatabaseId, allocation: u64) -> VersionResult<Self> {
42        if allocation == 0 {
43            return Err(VersionError::InvalidTransactionId);
44        }
45        Ok(Self {
46            database,
47            allocation,
48        })
49    }
50    pub const fn database(self) -> DatabaseId {
51        self.database
52    }
53    pub const fn allocation(self) -> u64 {
54        self.allocation
55    }
56}
57
58/// Origin of one evaluated mutation in its actual publishing transaction. Revisions are never reused after statement or savepoint undo; the pair is provenance, not commit ordering or proof of visibility.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub struct StorageMutationOrigin {
61    transaction: StorageTransactionId,
62    revision: u64,
63}
64
65impl StorageMutationOrigin {
66    pub(in crate::mvcc) const fn new(transaction: StorageTransactionId, revision: u64) -> Self {
67        Self {
68            transaction,
69            revision,
70        }
71    }
72    pub const fn transaction(self) -> StorageTransactionId {
73        self.transaction
74    }
75    pub const fn revision(self) -> u64 {
76        self.revision
77    }
78}
79
80pub type CommitFingerprint = [u8; 32];
81pub type RecordPage = BudgetedVec<ScannedRecord>;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct CommitReceipt {
85    pub transaction: StorageTransactionId,
86    pub sequence: CommitSequence,
87    pub fingerprint: CommitFingerprint,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum CommitStatus {
92    /// No retained authoritative outcome exists. This is not proof of rollback.
93    Unknown,
94    Pending,
95    Aborted,
96    Committed(CommitReceipt),
97}
98
99/// Durable outcome information retained in a storage error, including through provider wrappers. Absence of this information does not prove that a transaction aborted.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CommitErrorOutcome {
102    Indeterminate(StorageTransactionId),
103    Committed(CommitReceipt),
104    Aborted(StorageTransactionId),
105}
106
107impl StorageBackendError {
108    /// Classify typed commit evidence without interpreting provider diagnostic text.
109    pub fn commit_outcome(&self) -> Option<CommitErrorOutcome> {
110        let mut cause: Option<&(dyn std::error::Error + 'static)> = Some(self);
111        while let Some(error) = cause {
112            if let Some(outcome) = error
113                .downcast_ref::<super::TransactionCompletionError>()
114                .and_then(|error| error.publication)
115                .or_else(|| error_commit_outcome(error))
116            {
117                return Some(outcome);
118            }
119            cause = error.source();
120        }
121        None
122    }
123}
124
125pub(super) fn error_commit_outcome(
126    error: &(dyn std::error::Error + 'static),
127) -> Option<CommitErrorOutcome> {
128    if let Some(CommitFailure::Indeterminate { transaction, .. }) =
129        error.downcast_ref::<CommitFailure>()
130    {
131        Some(CommitErrorOutcome::Indeterminate(*transaction))
132    } else if let Some(VersionError::AlreadyCommitted(receipt)) =
133        error.downcast_ref::<VersionError>()
134    {
135        Some(CommitErrorOutcome::Committed(*receipt))
136    } else if let Some(VersionError::AlreadyAborted(transaction)) =
137        error.downcast_ref::<VersionError>()
138    {
139        Some(CommitErrorOutcome::Aborted(*transaction))
140    } else {
141        None
142    }
143}
144
145#[derive(Debug, thiserror::Error)]
146pub enum CommitFailure {
147    /// The attempt published no changes; any prior successful attempt still has its receipt.
148    #[error(transparent)]
149    Rejected(#[from] VersionError),
150    #[error("commit outcome for {transaction:?} is indeterminate: {source}")]
151    Indeterminate {
152        transaction: StorageTransactionId,
153        #[source]
154        source: StorageBackendError,
155    },
156}
157
158pub type CommitResult = Result<CommitReceipt, CommitFailure>;
159
160/// Versioned record persistence. Every mutation of the current sequence, records and receipt must share one physical commit. Implementations retain no physical writer between calls and must distinguish rejection from an uncertain native commit outcome.
161pub trait VersionedPersistence: Send + Sync {
162    fn database_id(&self) -> DatabaseId;
163
164    /// Exclusive resource ownership for this physical database, separate from SQL participants. Wrappers must forward the capability.
165    fn resource_leases(&self) -> Option<&dyn super::ResourceLeaseProvider> {
166        None
167    }
168
169    /// Shared participant admission and recovery for this exact physical database. Wrappers must forward the capability; its presence alone does not enable public SQL SSI.
170    fn serializable_coordinator(&self) -> Option<&dyn super::SerializableCoordinator> {
171        None
172    }
173
174    /// Read the latest autonomous watermark in one physical read transaction. Absence must remain absent; this must not acquire write admission, create records or change allocation state. Validate the database incarnation and retain the caller's resource and cancellation controls.
175    fn identifier_watermark(
176        &self,
177        namespace: &[u8],
178        control: &StorageReadControl,
179    ) -> VersionResult<Option<u64>>;
180
181    /// Atomically observe or reserve identifiers under the database's physical admission. Commit the high watermark before returning. A reservation must be durable before it returns, because its identifiers can be used before any record commit; an observation must be durable no later than the next durable commit. An error may consume identifiers but must never permit their reuse. This operation advances neither record visibility nor transaction allocation, publishes no private records, and survives transaction/savepoint rollback. Implementations and wrappers must preserve the caller's resource/cancellation control and reject a different database incarnation.
182    fn allocate_identifiers(
183        &self,
184        namespace: &[u8],
185        request: super::IdentifierRequest,
186        control: &StorageReadControl,
187    ) -> VersionResult<super::IdentifierAllocation>;
188
189    /// Optional private cache-revision grouping. Wrappers should forward the provider selection; absence preserves the exact per-record scan. The classifier belongs to the physical key owner and must not read mutable state.
190    fn private_revision_scope(&self) -> Option<super::PrivateRevisionScope> {
191        None
192    }
193
194    fn graph_record_layout(&self) -> Option<&dyn super::GraphRecordLayout> {
195        None
196    }
197
198    /// Common Key/Value addressing is the default. Native record adapters and their wrappers must return their own physical occurrence layout.
199    fn occurrence_record_layout(&self) -> &dyn super::OccurrenceRecordLayout {
200        &crate::key_value::KeyValueOccurrenceRecords
201    }
202
203    fn notification_record_layout(&self) -> &dyn super::NotificationRecordLayout {
204        &crate::key_value::KeyValueNotificationRecords
205    }
206
207    fn maintenance_record_layout(&self) -> &dyn super::MaintenanceRecordLayout {
208        &crate::key_value::KeyValueMaintenanceRecords
209    }
210
211    fn ivf_record_layout(&self) -> &dyn super::IVFRecordLayout {
212        &crate::key_value::KeyValueIVFRecords
213    }
214
215    /// Physical population records and canonical adapters for this provider. An unsupported provider must reject typed population changes rather than publish unresolved counts.
216    fn diskann_population_record_layout(
217        &self,
218    ) -> Option<&dyn super::DiskANNPopulationRecordLayout> {
219        None
220    }
221
222    /// Native adapters must override this when their physical node and edge families differ.
223    fn hnsw_record_layout(&self) -> Option<&dyn super::HNSWRecordLayout> {
224        Some(&crate::key_value::KeyValueHNSWRecords)
225    }
226
227    /// Persist a new transaction allocation before returning it, without advancing record visibility.
228    fn allocate_transaction(
229        &self,
230        control: &StorageReadControl,
231    ) -> VersionResult<StorageTransactionId>;
232
233    /// Allocate a receipt with explicit managed resolution ownership. Publish its lease before the Pending row becomes durable under the same receipt admission used by recovery. Only an authoritatively absent managed lease permits abandoned-owner recovery; raw allocations retain manual ownership. The default preserves legacy indefinite receipt retention.
234    fn allocate_managed_transaction(
235        &self,
236        control: &StorageReadControl,
237    ) -> VersionResult<super::RetainedTransactionAllocation> {
238        self.allocate_transaction(control)
239            .map(super::RetainedTransactionAllocation::untracked)
240    }
241
242    fn snapshot(
243        &self,
244        control: &StorageReadControl,
245    ) -> VersionResult<Arc<dyn CommittedRecordSnapshot>>;
246
247    /// A value that changes whenever any session or process commits to this database, read without capturing a snapshot. Two equal values prove that no commit lies between the two reads. A provider with a monitor reads it before it captures a snapshot and reports that value through [`CommittedRecordSnapshot::commit_monitor`], which lets a session keep its snapshot instead of capturing an identical one. `None` when the provider has no monitor that may read at any time.
248    fn commit_monitor_version(&self) -> VersionResult<Option<u64>> {
249        Ok(None)
250    }
251
252    /// Atomically remove obsolete historical revisions under snapshot admission. Preserve the newest revision at/before every live snapshot, all newer revisions, head tombstones and every transaction receipt. Neither visibility nor transaction allocation advances. Wrappers must forward the capability and its resource/cancellation control.
253    fn reclaim_versions(&self, control: &StorageReadControl) -> VersionResult<u64>;
254
255    /// Physical encoding of shared vector-field coordination records. Native providers and wrappers must preserve the original layout selection.
256    fn vector_field_guard_layout(&self) -> &dyn super::VectorFieldGuardLayout {
257        &crate::key_value::KeyValueVectorFieldGuards
258    }
259
260    /// Retire physical `DiskANN` identities after ordinary history collection. The default owns the common Key/Value generation, canonical-origin, change-journal and field-guard namespaces; providers with another layout must override it. Wrappers must forward the original control and physical layout selection.
261    fn reclaim_diskann_tombstones(&self, control: &StorageReadControl) -> VersionResult<()> {
262        super::reclaim_key_value_diskann_tombstones(self, control)
263    }
264
265    /// Retire one bounded page of deleted record identities in an explicitly selected prefix. Requires no live record snapshots under shared admission, and atomically publishes a reclamation epoch with deletion. Commits touching an enrolled prefix must validate their original snapshot epoch after receipt resolution. Generic history reclamation keeps its existing tombstone contract. Wrappers must forward this capability and the original controls.
266    fn reclaim_tombstones(
267        &self,
268        _request: &super::TombstoneReclamationRequest<'_>,
269        _control: &StorageReadControl,
270    ) -> VersionResult<super::TombstoneReclamationStep> {
271        Err(VersionError::InvalidEncoding(
272            "scoped tombstone reclamation is not implemented by this provider",
273        ))
274    }
275
276    /// Retry the same sealed logical changes and fingerprint. Return a durable matching receipt before validating snapshots or record heads. After verifying a pending receipt under exclusive admission, call `PreparedRecordCommit::validate_snapshot` before validating heads; only its rejection permits common storage to materialize derived effects again. Canonical changes and application callbacks must never be replayed, and mismatched fingerprint reuse is rejected.
277    fn commit(
278        &self,
279        transaction: StorageTransactionId,
280        prepared: &PreparedRecordCommit,
281        control: &StorageReadControl,
282    ) -> CommitResult;
283
284    fn commit_status(
285        &self,
286        transaction: StorageTransactionId,
287        control: &StorageReadControl,
288    ) -> VersionResult<CommitStatus>;
289
290    /// Durably release caller resolution ownership only after preserving its confirmed terminal outcome. This never releases a retained SSI publication. The default keeps historical receipts indefinitely; providers with bounded receipts override it. Errors preserve the caller's completed retry state, and a retry after durable acknowledgement remains valid even after reclamation.
291    fn acknowledge_transaction(
292        &self,
293        _acknowledgement: super::ReceiptAcknowledgement,
294        _control: &StorageReadControl,
295    ) -> VersionResult<()> {
296        Ok(())
297    }
298
299    /// Reclaim explicitly acknowledged receipts only after their final durable SSI publication reference has disappeared. Pending and unacknowledged receipts survive, including unknown completion owners. This is separate from record-history reclamation and does not advance allocation or visibility.
300    fn reclaim_transaction_receipts(&self, _control: &StorageReadControl) -> VersionResult<u64> {
301        Ok(0)
302    }
303
304    /// Atomically end a pending allocation. Return an existing committed receipt unchanged; abort must never overwrite a commit.
305    fn abort(
306        &self,
307        transaction: StorageTransactionId,
308        control: &StorageReadControl,
309    ) -> VersionResult<CommitStatus>;
310}
311
312/// Resolve an idempotent commit under the provider's exclusive commit boundary before validating or changing records.
313pub fn resolve_prepared_receipt(
314    status: CommitStatus,
315    transaction: StorageTransactionId,
316    fingerprint: CommitFingerprint,
317) -> VersionResult<Option<CommitReceipt>> {
318    match status {
319        CommitStatus::Pending => Ok(None),
320        CommitStatus::Committed(receipt)
321            if receipt.transaction == transaction && receipt.fingerprint == fingerprint =>
322        {
323            Ok(Some(receipt))
324        }
325        CommitStatus::Committed(_) => Err(VersionError::CommitMismatch),
326        CommitStatus::Aborted => Err(VersionError::TransactionFinished),
327        CommitStatus::Unknown => Err(VersionError::UnknownTransaction),
328    }
329}