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    /// Select autonomous sequence publication whose WAL synchronization is completed by the consuming transaction. Unsupported providers keep ordinary durable commits. Implementations may defer only recognized sequence value replacements, must retain `owner.with_publication` admission across physical publication, and must implement `synchronize_sequence_values` on ordinary sessions over the same database. An expired owner requires ordinary synchronous publication.
163    fn sequence_value_persistence(
164        &self,
165        _owner: super::SequencePublicationLease,
166    ) -> Option<std::sync::Arc<dyn VersionedPersistence>> {
167        None
168    }
169
170    /// Make all currently published sequence values durable without changing record visibility. A caller retains this obligation across savepoint undo and retries completion after failure. Ordinary synchronous providers have no deferred work.
171    fn synchronize_sequence_values(&self, _control: &StorageReadControl) -> VersionResult<()> {
172        Ok(())
173    }
174
175    /// Whether this exact receipt's native catalog generations changed only data, column statistics or maintenance. Implementations retain bounded completion evidence and must not read a newer snapshot to establish this property. Missing evidence requires ordinary cache refresh.
176    fn commit_preserves_catalog_definitions(&self, _receipt: CommitReceipt) -> bool {
177        false
178    }
179
180    fn database_id(&self) -> DatabaseId;
181
182    /// Exclusive resource ownership for this physical database, separate from SQL participants. Wrappers must forward the capability.
183    fn resource_leases(&self) -> Option<&dyn super::ResourceLeaseProvider> {
184        None
185    }
186
187    /// Shared participant admission and recovery for this exact physical database. Wrappers must forward the capability; its presence alone does not enable public SQL SSI.
188    fn serializable_coordinator(&self) -> Option<&dyn super::SerializableCoordinator> {
189        None
190    }
191
192    /// 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.
193    fn identifier_watermark(
194        &self,
195        namespace: &[u8],
196        control: &StorageReadControl,
197    ) -> VersionResult<Option<u64>>;
198
199    /// 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.
200    fn allocate_identifiers(
201        &self,
202        namespace: &[u8],
203        request: super::IdentifierRequest,
204        control: &StorageReadControl,
205    ) -> VersionResult<super::IdentifierAllocation>;
206
207    /// 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.
208    fn private_revision_scope(&self) -> Option<super::PrivateRevisionScope> {
209        None
210    }
211
212    fn graph_record_layout(&self) -> Option<&dyn super::GraphRecordLayout> {
213        None
214    }
215
216    /// Common Key/Value addressing is the default. Native record adapters and their wrappers must return their own physical occurrence layout.
217    fn occurrence_record_layout(&self) -> &dyn super::OccurrenceRecordLayout {
218        &crate::key_value::KeyValueOccurrenceRecords
219    }
220
221    fn notification_record_layout(&self) -> &dyn super::NotificationRecordLayout {
222        &crate::key_value::KeyValueNotificationRecords
223    }
224
225    fn maintenance_record_layout(&self) -> &dyn super::MaintenanceRecordLayout {
226        &crate::key_value::KeyValueMaintenanceRecords
227    }
228
229    fn ivf_record_layout(&self) -> &dyn super::IVFRecordLayout {
230        &crate::key_value::KeyValueIVFRecords
231    }
232
233    /// Physical population records and canonical adapters for this provider. An unsupported provider must reject typed population changes rather than publish unresolved counts.
234    fn diskann_population_record_layout(
235        &self,
236    ) -> Option<&dyn super::DiskANNPopulationRecordLayout> {
237        None
238    }
239
240    /// Native adapters must override this when their physical node and edge families differ.
241    fn hnsw_record_layout(&self) -> Option<&dyn super::HNSWRecordLayout> {
242        Some(&crate::key_value::KeyValueHNSWRecords)
243    }
244
245    /// Persist a new transaction allocation before returning it, without advancing record visibility.
246    fn allocate_transaction(
247        &self,
248        control: &StorageReadControl,
249    ) -> VersionResult<StorageTransactionId>;
250
251    /// 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.
252    fn allocate_managed_transaction(
253        &self,
254        control: &StorageReadControl,
255    ) -> VersionResult<super::RetainedTransactionAllocation> {
256        self.allocate_transaction(control)
257            .map(super::RetainedTransactionAllocation::untracked)
258    }
259
260    fn snapshot(
261        &self,
262        control: &StorageReadControl,
263    ) -> VersionResult<Arc<dyn CommittedRecordSnapshot>>;
264
265    /// 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.
266    fn commit_monitor_version(&self) -> VersionResult<Option<u64>> {
267        Ok(None)
268    }
269
270    /// 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.
271    fn reclaim_versions(&self, control: &StorageReadControl) -> VersionResult<u64>;
272
273    /// Physical encoding of shared vector-field coordination records. Native providers and wrappers must preserve the original layout selection.
274    fn vector_field_guard_layout(&self) -> &dyn super::VectorFieldGuardLayout {
275        &crate::key_value::KeyValueVectorFieldGuards
276    }
277
278    /// 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.
279    fn reclaim_diskann_tombstones(&self, control: &StorageReadControl) -> VersionResult<()> {
280        super::reclaim_key_value_diskann_tombstones(self, control)
281    }
282
283    /// 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.
284    fn reclaim_tombstones(
285        &self,
286        _request: &super::TombstoneReclamationRequest<'_>,
287        _control: &StorageReadControl,
288    ) -> VersionResult<super::TombstoneReclamationStep> {
289        Err(VersionError::InvalidEncoding(
290            "scoped tombstone reclamation is not implemented by this provider",
291        ))
292    }
293
294    /// 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.
295    fn commit(
296        &self,
297        transaction: StorageTransactionId,
298        prepared: &PreparedRecordCommit,
299        control: &StorageReadControl,
300    ) -> CommitResult;
301
302    fn commit_status(
303        &self,
304        transaction: StorageTransactionId,
305        control: &StorageReadControl,
306    ) -> VersionResult<CommitStatus>;
307
308    /// 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.
309    fn acknowledge_transaction(
310        &self,
311        _acknowledgement: super::ReceiptAcknowledgement,
312        _control: &StorageReadControl,
313    ) -> VersionResult<()> {
314        Ok(())
315    }
316
317    /// Confirm a retained owner's terminal outcome before the caller drops it. Providers with durable managed-owner metadata may rely on final lease release and their existing abandoned-owner recovery instead of writing a separate acknowledgement. They must validate the same exact outcome, preserve live owners and SSI references, and never infer commit or abort from lease death. Errors leave the owner available for retry. The default keeps explicit acknowledgement for untracked and legacy providers.
318    fn acknowledge_retained_transaction(
319        &self,
320        owner: &super::RetainedTransactionAllocation,
321        acknowledgement: super::ReceiptAcknowledgement,
322        control: &StorageReadControl,
323    ) -> VersionResult<()> {
324        if owner.transaction() != acknowledgement.transaction() {
325            return Err(VersionError::InvalidEncoding(
326                "receipt acknowledgement owner mismatch",
327            ));
328        }
329        self.acknowledge_transaction(acknowledgement, control)
330    }
331
332    /// Reclaim acknowledged receipts only after their final durable SSI publication reference has disappeared. Providers may also recover managed owners whose final lease is authoritatively absent, using the durable physical outcome rather than inferring it from owner death. Live or unknown owners and unacknowledged manual receipts survive. This is separate from record-history reclamation and does not advance allocation or visibility.
333    fn reclaim_transaction_receipts(&self, _control: &StorageReadControl) -> VersionResult<u64> {
334        Ok(0)
335    }
336
337    /// Atomically end a pending allocation. Return an existing committed receipt unchanged; abort must never overwrite a commit.
338    fn abort(
339        &self,
340        transaction: StorageTransactionId,
341        control: &StorageReadControl,
342    ) -> VersionResult<CommitStatus>;
343}
344
345/// Resolve an idempotent commit under the provider's exclusive commit boundary before validating or changing records.
346pub fn resolve_prepared_receipt(
347    status: CommitStatus,
348    transaction: StorageTransactionId,
349    fingerprint: CommitFingerprint,
350) -> VersionResult<Option<CommitReceipt>> {
351    match status {
352        CommitStatus::Pending => Ok(None),
353        CommitStatus::Committed(receipt)
354            if receipt.transaction == transaction && receipt.fingerprint == fingerprint =>
355        {
356            Ok(Some(receipt))
357        }
358        CommitStatus::Committed(_) => Err(VersionError::CommitMismatch),
359        CommitStatus::Aborted => Err(VersionError::TransactionFinished),
360        CommitStatus::Unknown => Err(VersionError::UnknownTransaction),
361    }
362}