Skip to main content

uqa_storage/mvcc/serializable/
publication.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Explicit binding between a sealed SSI participant and an independently allocated durable commit.
8
9#[cfg(test)]
10mod tests;
11
12use super::{SerializableGraph, SerializableTransactionId, VersionError, VersionResult};
13use crate::{
14    mvcc::{CommitFingerprint, CommitReceipt, CommitSequence, CommitStatus, StorageTransactionId},
15    read_control::StorageReadControl,
16};
17
18/// A sealed association with one physical transaction and its logical mutation fingerprint. Providers must retain the prepared SSI state and this association under shared admission before attempting physical publication. Neither possession of this token nor loss of its process proves a durable outcome.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct SerializablePublication {
21    participant: SerializableTransactionId,
22    transaction: StorageTransactionId,
23    fingerprint: CommitFingerprint,
24}
25
26#[derive(Clone, Copy)]
27pub(super) struct PreparedPublication {
28    pub(super) allocation: u64,
29    pub(super) fingerprint: CommitFingerprint,
30    pub(super) outcome: PublicationOutcome,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq)]
34pub(super) enum PublicationOutcome {
35    Prepared,
36    Committed(CommitSequence),
37    Aborted,
38}
39
40impl PreparedPublication {
41    pub(super) fn committed(self) -> bool {
42        matches!(self.outcome, PublicationOutcome::Committed(_))
43    }
44
45    pub(super) fn aborted(self) -> bool {
46        self.outcome == PublicationOutcome::Aborted
47    }
48
49    fn matches(self, publication: SerializablePublication) -> bool {
50        self.allocation == publication.transaction.allocation()
51            && self.fingerprint == publication.fingerprint
52    }
53}
54
55impl SerializablePublication {
56    pub const fn participant(self) -> SerializableTransactionId {
57        self.participant
58    }
59
60    pub const fn transaction(self) -> StorageTransactionId {
61        self.transaction
62    }
63
64    pub const fn fingerprint(self) -> CommitFingerprint {
65        self.fingerprint
66    }
67}
68
69impl SerializableGraph {
70    /// Whether the retained checkpoint still needs this exact physical receipt, including prepared outcomes, committed conflict history and live terminal completion leases. Receipt reclamation must check the durably published graph under the provider's exclusive SSI admission.
71    pub fn retains_transaction_receipt(&self, transaction: StorageTransactionId) -> bool {
72        transaction.database() == self.database
73            && self.transactions.iter().any(|entry| {
74                entry
75                    .publication
76                    .is_some_and(|publication| publication.allocation == transaction.allocation())
77            })
78    }
79
80    /// Validate terminal outcomes restored from a provider checkpoint before exposing its graph or invoking admission. Every retained committed or aborted publication must still have the same authoritative physical receipt; these receipts outlive record-history reclamation. A missing receipt rejects the persisted checkpoint rather than letting a stale backup branch borrow its confirmed outcome. Prepared publications keep their ordinary reconciliation path. This read-only check borrows retained entries, allocates no payload and leaves live-session terminal outcome precedence unchanged.
81    pub fn validate_persisted_publications(
82        &self,
83        control: &StorageReadControl,
84        mut lookup: impl FnMut(StorageTransactionId) -> VersionResult<CommitStatus>,
85    ) -> VersionResult<()> {
86        control.cancellation().check()?;
87        for entry in &*self.transactions {
88            control.cancellation().check()?;
89            let Some(publication) = entry.publication else {
90                continue;
91            };
92            let transaction = StorageTransactionId::new(self.database, publication.allocation)?;
93            let expected = match publication.outcome {
94                PublicationOutcome::Committed(sequence) => CommitStatus::Committed(CommitReceipt {
95                    transaction,
96                    sequence,
97                    fingerprint: publication.fingerprint,
98                }),
99                PublicationOutcome::Aborted => CommitStatus::Aborted,
100                PublicationOutcome::Prepared => continue,
101            };
102            match lookup(transaction)? {
103                CommitStatus::Unknown => return Err(VersionError::UnknownTransaction),
104                actual if actual != expected => return Err(VersionError::CommitMismatch),
105                _ => {}
106            }
107        }
108        Ok(())
109    }
110
111    /// Seal the participant against its caller's already allocated durable transaction. Repeated preparation must preserve both identities and the original fingerprint, including during physical candidate re-preparation. A logically read-only participant can publish maintenance records outside logical predicate spaces; logical write observations remain forbidden. Participants without physical changes finish directly through the graph without allocating a durable transaction.
112    pub fn prepare_publication(
113        &mut self,
114        participant: SerializableTransactionId,
115        transaction: StorageTransactionId,
116        fingerprint: CommitFingerprint,
117        control: &StorageReadControl,
118    ) -> VersionResult<SerializablePublication> {
119        control.check()?;
120        let position = self.position(participant)?;
121        if transaction.database() != self.database {
122            return Err(VersionError::WrongDatabase);
123        }
124        let publication = SerializablePublication {
125            participant,
126            transaction,
127            fingerprint,
128        };
129        if let Some(previous) = self.transactions[position].publication {
130            if !previous.matches(publication) {
131                return Err(VersionError::CommitMismatch);
132            }
133        } else {
134            self.check_active(participant)?;
135            for entry in &*self.transactions {
136                control.check()?;
137                if entry
138                    .publication
139                    .is_some_and(|previous| previous.allocation == transaction.allocation())
140                {
141                    return Err(VersionError::CommitMismatch);
142                }
143            }
144        }
145        self.prepare_commit(participant, control)?;
146        self.checkpoint_changed |= self.transactions[position].publication.is_none();
147        self.transactions[position].publication = Some(PreparedPublication {
148            allocation: transaction.allocation(),
149            fingerprint,
150            outcome: PublicationOutcome::Prepared,
151        });
152        Ok(publication)
153    }
154
155    /// Retrieve the physical receipt identity needed for recovery of a retained prepared participant. The provider queries this exact durable transaction; the SSI participant allocation is never a receipt key.
156    pub fn publication(
157        &self,
158        participant: SerializableTransactionId,
159    ) -> VersionResult<Option<SerializablePublication>> {
160        self.transactions[self.position(participant)?]
161            .publication
162            .map(|publication| {
163                Ok(SerializablePublication {
164                    participant,
165                    transaction: StorageTransactionId::new(self.database, publication.allocation)?,
166                    fingerprint: publication.fingerprint,
167                })
168            })
169            .transpose()
170    }
171
172    /// Reconcile unresolved physical publications under shared admission before capturing another data snapshot. A lost commit reply may leave committed data behind a prepared SSI participant; admitting a snapshot first would misclassify that visible writer. Confirmed pending allocations may remain prepared, but a missing authoritative receipt prevents admission and never proves abort. The provider retains confirmed resolutions even if a later lookup fails.
173    pub fn reconcile_publications(
174        &mut self,
175        control: &StorageReadControl,
176        mut lookup: impl FnMut(StorageTransactionId) -> VersionResult<CommitStatus>,
177    ) -> VersionResult<()> {
178        for index in 0..self.transactions.len() {
179            control.check()?;
180            let entry = self.transactions[index];
181            let Some(prepared) = entry.publication else {
182                continue;
183            };
184            if prepared.outcome != PublicationOutcome::Prepared {
185                continue;
186            }
187            let participant =
188                SerializableTransactionId::new(self.database, self.coordinator, entry.id)?;
189            let transaction = StorageTransactionId::new(self.database, prepared.allocation)?;
190            let publication = SerializablePublication {
191                participant,
192                transaction,
193                fingerprint: prepared.fingerprint,
194            };
195            let status = lookup(transaction)?;
196            if self.resolve_publication(publication, status)? == CommitStatus::Unknown {
197                return Err(VersionError::UnknownTransaction);
198            }
199        }
200        Ok(())
201    }
202
203    /// Apply an authoritative status obtained for this token's physical transaction. Pending/unknown outcomes keep an unresolved participant prepared; a committed receipt must match the transaction and fingerprint. Known terminal outcomes cannot be downgraded by later pending/unknown status. Terminal completion neither allocates nor observes statement cancellation. The provider must retain the resolved state before releasing shared publication admission.
204    pub fn resolve_publication(
205        &mut self,
206        publication: SerializablePublication,
207        status: CommitStatus,
208    ) -> VersionResult<CommitStatus> {
209        let position = self.position(publication.participant)?;
210        let Some(prepared) = self.transactions[position].publication else {
211            return Err(VersionError::CommitMismatch);
212        };
213        if !prepared.matches(publication) {
214            return Err(VersionError::CommitMismatch);
215        }
216        match prepared.outcome {
217            PublicationOutcome::Committed(sequence) => {
218                let receipt = CommitReceipt {
219                    transaction: publication.transaction,
220                    sequence,
221                    fingerprint: publication.fingerprint,
222                };
223                return match status {
224                    CommitStatus::Aborted => Err(VersionError::AlreadyCommitted(receipt)),
225                    CommitStatus::Committed(actual) if actual != receipt => {
226                        Err(VersionError::AlreadyCommitted(receipt))
227                    }
228                    _ => Ok(CommitStatus::Committed(receipt)),
229                };
230            }
231            PublicationOutcome::Aborted => {
232                return if matches!(status, CommitStatus::Committed(_)) {
233                    Err(VersionError::AlreadyAborted(publication.transaction))
234                } else {
235                    Ok(CommitStatus::Aborted)
236                };
237            }
238            PublicationOutcome::Prepared => {}
239        }
240        match status {
241            CommitStatus::Committed(receipt) => {
242                if receipt.transaction != publication.transaction
243                    || receipt.fingerprint != publication.fingerprint
244                {
245                    return Err(VersionError::CommitMismatch);
246                }
247                self.checkpoint_changed = true;
248                self.transactions[position]
249                    .publication
250                    .as_mut()
251                    .expect("validated publication")
252                    .outcome = PublicationOutcome::Committed(receipt.sequence);
253                self.commit(publication.participant)?;
254            }
255            CommitStatus::Aborted => {
256                self.checkpoint_changed = true;
257                self.transactions[position]
258                    .publication
259                    .as_mut()
260                    .expect("validated publication")
261                    .outcome = PublicationOutcome::Aborted;
262                self.rollback(publication.participant)?;
263            }
264            CommitStatus::Pending | CommitStatus::Unknown => {}
265        }
266        Ok(status)
267    }
268}