Skip to main content

uqa_storage/mvcc/
receipts.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Explicit terminal receipt ownership and database-wide admission bounds.
8
9use super::{
10    CommitReceipt, CommitStatus, SerializableParticipant, SerializableTransactionId,
11    StorageTransactionId, VersionError, VersionResult,
12};
13
14/// Default maximum number of durable receipt entries, including pending allocations. Providers persist this database-wide limit and expose configuration independently of session byte allowances. A receipt has a fixed-size identity, outcome and ownership flag; physical page and index overhead remain provider-owned.
15pub const DEFAULT_RECEIPT_RETENTION_LIMIT: u64 = 65_536;
16
17/// A separate liveness namespace for physical receipt owners, reusing the common retained-participant transport without admitting an SSI transaction.
18pub fn receipt_lease_id(transaction: StorageTransactionId) -> SerializableTransactionId {
19    SerializableTransactionId::new(
20        transaction.database(),
21        *b"UQAReceiptLease1",
22        transaction.allocation(),
23    )
24    .expect("receipt allocation and namespace are nonzero")
25}
26
27/// Keeps a managed allocation's resolution owner alive through publication and acknowledgement retries. Dropping the final lease permits provider recovery, but never establishes a physical outcome by itself. The conservative untracked form preserves legacy providers' manually retained receipt semantics.
28pub struct RetainedTransactionAllocation {
29    transaction: StorageTransactionId,
30    _lease: Option<SerializableParticipant>,
31}
32
33impl RetainedTransactionAllocation {
34    pub fn untracked(transaction: StorageTransactionId) -> Self {
35        Self {
36            transaction,
37            _lease: None,
38        }
39    }
40
41    pub fn retain(
42        transaction: StorageTransactionId,
43        lease: SerializableParticipant,
44    ) -> VersionResult<Self> {
45        if lease.id() != receipt_lease_id(transaction) {
46            return Err(VersionError::InvalidEncoding(
47                "receipt owner lease identity mismatch",
48            ));
49        }
50        Ok(Self {
51            transaction,
52            _lease: Some(lease),
53        })
54    }
55
56    pub fn transaction(&self) -> StorageTransactionId {
57        self.transaction
58    }
59}
60
61/// Release the caller's right to resolve one confirmed physical outcome. After successful acknowledgement, lookup or replay may return Unknown; acknowledgement itself remains idempotent. Providers retain every SSI publication reference independently and must never infer acknowledgement from age or a snapshot horizon.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ReceiptAcknowledgement {
64    Committed(CommitReceipt),
65    Aborted(StorageTransactionId),
66}
67
68impl ReceiptAcknowledgement {
69    pub fn transaction(self) -> StorageTransactionId {
70        match self {
71            Self::Committed(receipt) => receipt.transaction,
72            Self::Aborted(transaction) => transaction,
73        }
74    }
75
76    /// A missing previously allocated receipt is an idempotent acknowledgement retry. Providers must first verify the database incarnation and allocation watermark. Pending or changed outcomes cannot be released.
77    pub fn validate(self, status: CommitStatus) -> VersionResult<()> {
78        match (self, status) {
79            (_, CommitStatus::Unknown) | (Self::Aborted(_), CommitStatus::Aborted) => Ok(()),
80            (Self::Committed(expected), CommitStatus::Committed(actual)) if expected == actual => {
81                Ok(())
82            }
83            (_, CommitStatus::Pending) => Err(VersionError::TransactionSealed),
84            (_, CommitStatus::Committed(receipt)) => Err(VersionError::AlreadyCommitted(receipt)),
85            (_, CommitStatus::Aborted) => Err(VersionError::AlreadyAborted(self.transaction())),
86        }
87    }
88}