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    fn graph_record_layout(&self) -> Option<&dyn super::GraphRecordLayout> {
190        None
191    }
192
193    /// Common Key/Value addressing is the default. Native record adapters and their wrappers must return their own physical occurrence layout.
194    fn occurrence_record_layout(&self) -> &dyn super::OccurrenceRecordLayout {
195        &crate::key_value::KeyValueOccurrenceRecords
196    }
197
198    fn notification_record_layout(&self) -> &dyn super::NotificationRecordLayout {
199        &crate::key_value::KeyValueNotificationRecords
200    }
201
202    fn maintenance_record_layout(&self) -> &dyn super::MaintenanceRecordLayout {
203        &crate::key_value::KeyValueMaintenanceRecords
204    }
205
206    fn ivf_record_layout(&self) -> &dyn super::IVFRecordLayout {
207        &crate::key_value::KeyValueIVFRecords
208    }
209
210    /// Physical population records and canonical adapters for this provider. An unsupported provider must reject typed population changes rather than publish unresolved counts.
211    fn diskann_population_record_layout(
212        &self,
213    ) -> Option<&dyn super::DiskANNPopulationRecordLayout> {
214        None
215    }
216
217    /// Native adapters must override this when their physical node and edge families differ.
218    fn hnsw_record_layout(&self) -> Option<&dyn super::HNSWRecordLayout> {
219        Some(&crate::key_value::KeyValueHNSWRecords)
220    }
221
222    /// Persist a new transaction allocation before returning it, without advancing record visibility.
223    fn allocate_transaction(
224        &self,
225        control: &StorageReadControl,
226    ) -> VersionResult<StorageTransactionId>;
227
228    /// 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.
229    fn allocate_managed_transaction(
230        &self,
231        control: &StorageReadControl,
232    ) -> VersionResult<super::RetainedTransactionAllocation> {
233        self.allocate_transaction(control)
234            .map(super::RetainedTransactionAllocation::untracked)
235    }
236
237    fn snapshot(
238        &self,
239        control: &StorageReadControl,
240    ) -> VersionResult<Arc<dyn CommittedRecordSnapshot>>;
241
242    /// 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.
243    fn commit_monitor_version(&self) -> VersionResult<Option<u64>> {
244        Ok(None)
245    }
246
247    /// 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.
248    fn reclaim_versions(&self, control: &StorageReadControl) -> VersionResult<u64>;
249
250    /// Physical encoding of shared vector-field coordination records. Native providers and wrappers must preserve the original layout selection.
251    fn vector_field_guard_layout(&self) -> &dyn super::VectorFieldGuardLayout {
252        &crate::key_value::KeyValueVectorFieldGuards
253    }
254
255    /// 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.
256    fn reclaim_diskann_tombstones(&self, control: &StorageReadControl) -> VersionResult<()> {
257        super::reclaim_key_value_diskann_tombstones(self, control)
258    }
259
260    /// 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.
261    fn reclaim_tombstones(
262        &self,
263        _request: &super::TombstoneReclamationRequest<'_>,
264        _control: &StorageReadControl,
265    ) -> VersionResult<super::TombstoneReclamationStep> {
266        Err(VersionError::InvalidEncoding(
267            "scoped tombstone reclamation is not implemented by this provider",
268        ))
269    }
270
271    /// 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.
272    fn commit(
273        &self,
274        transaction: StorageTransactionId,
275        prepared: &PreparedRecordCommit,
276        control: &StorageReadControl,
277    ) -> CommitResult;
278
279    fn commit_status(
280        &self,
281        transaction: StorageTransactionId,
282        control: &StorageReadControl,
283    ) -> VersionResult<CommitStatus>;
284
285    /// 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.
286    fn acknowledge_transaction(
287        &self,
288        _acknowledgement: super::ReceiptAcknowledgement,
289        _control: &StorageReadControl,
290    ) -> VersionResult<()> {
291        Ok(())
292    }
293
294    /// 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.
295    fn reclaim_transaction_receipts(&self, _control: &StorageReadControl) -> VersionResult<u64> {
296        Ok(0)
297    }
298
299    /// Atomically end a pending allocation. Return an existing committed receipt unchanged; abort must never overwrite a commit.
300    fn abort(
301        &self,
302        transaction: StorageTransactionId,
303        control: &StorageReadControl,
304    ) -> VersionResult<CommitStatus>;
305}
306
307/// Resolve an idempotent commit under the provider's exclusive commit boundary before validating or changing records.
308pub fn resolve_prepared_receipt(
309    status: CommitStatus,
310    transaction: StorageTransactionId,
311    fingerprint: CommitFingerprint,
312) -> VersionResult<Option<CommitReceipt>> {
313    match status {
314        CommitStatus::Pending => Ok(None),
315        CommitStatus::Committed(receipt)
316            if receipt.transaction == transaction && receipt.fingerprint == fingerprint =>
317        {
318            Ok(Some(receipt))
319        }
320        CommitStatus::Committed(_) => Err(VersionError::CommitMismatch),
321        CommitStatus::Aborted => Err(VersionError::TransactionFinished),
322        CommitStatus::Unknown => Err(VersionError::UnknownTransaction),
323    }
324}