reddb_engine/transaction/read.rs
1//! Explicit snapshot visibility for entity reads. Hosts retain auth, tenant,
2//! moderation and ambient statement-frame policy around these helpers.
3use std::sync::Arc;
4
5/// Snapshot + manager pair used for read-path visibility checks.
6///
7/// The manager is needed in addition to the snapshot because `aborted`
8/// state mutates after the snapshot is captured — a ROLLBACK by a
9/// committed-at-capture-time writer must still hide its tuples. Keeping
10/// the Arc around is O(pointer) and the RwLock reads on `is_aborted`
11/// are cheap (HashSet lookup under a parking_lot read guard).
12///
13/// `own_xids` (Phase 2.3.2e) lists the xids belonging to the current
14/// connection's transaction — the parent xid plus open and released
15/// savepoint sub-xids. The visibility rule promotes rows stamped with
16/// these xids to "always visible (unless aborted)" so the writer sees
17/// its own nested-savepoint writes even though their xids exceed
18/// `snapshot.xid`.
19#[derive(Clone)]
20pub struct SnapshotContext {
21 pub snapshot: super::snapshot::Snapshot,
22 pub manager: Arc<super::snapshot::SnapshotManager>,
23 pub own_xids: std::collections::HashSet<super::snapshot::Xid>,
24 pub requires_index_fallback: bool,
25 pub serializable_reader: Option<super::snapshot::Xid>,
26}
27
28/// Record an admitted table-row read after the host has applied visibility and
29/// policy gates. SSI keys use logical identity, shared by all physical versions.
30pub fn record_serializable_read(
31 ctx: &SnapshotContext,
32 entity: &crate::storage::entity::UnifiedEntity,
33) {
34 let Some(reader) = ctx.serializable_reader else {
35 return;
36 };
37 if !matches!(
38 &entity.kind,
39 crate::storage::entity::EntityKind::TableRow { .. }
40 ) {
41 return;
42 }
43 ctx.manager
44 .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
45}
46
47/// Apply captured MVCC visibility, including own/aborted savepoint writers.
48/// This predicate supplies no authorization, tenant or moderation policy.
49#[inline]
50pub fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
51 // Writer aborted → tuple never existed from any future reader's view.
52 // Checked *before* the own-xids fast path so an aborted own-sub-xid
53 // (rolled-back savepoint) stays hidden from the parent.
54 if xmin != 0 && ctx.manager.is_aborted(xmin) {
55 return false;
56 }
57 // Deleter aborted → treat xmax as unset; fall back to xmin-only check.
58 let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
59 0
60 } else {
61 xmax
62 };
63 // Phase 2.3.2e: own-tx writes are always visible to the connection
64 // that stamped them, even when xmin/xmax exceed `snapshot.xid` (as
65 // happens for sub-xids allocated by SAVEPOINT after BEGIN).
66 let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
67 let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
68 if own_xmax {
69 // This connection deleted the row via this xid — hide it from self.
70 return false;
71 }
72 if own_xmin {
73 return true;
74 }
75 ctx.snapshot.sees(xmin, effective_xmax)
76}
77
78/// Candidate access for the canonical table-row version selector. The store
79/// supplies only physical versions of the requested collection/logical identity.
80pub trait TableRowVersionStore {
81 fn table_row_versions_by_logical_id(
82 &self,
83 collection: &str,
84 logical_id: crate::storage::entity::EntityId,
85 ) -> Vec<crate::storage::entity::UnifiedEntity>;
86
87 fn get_table_row_by_logical_id(
88 &self,
89 collection: &str,
90 logical_id: crate::storage::entity::EntityId,
91 ) -> Option<crate::storage::entity::UnifiedEntity>;
92}
93
94/// Select the newest visible physical version, or use the current-row lookup
95/// when the host has no snapshot requiring history. The host's predicate retains
96/// table-kind, authorization and moderation gates before materialization.
97pub fn resolve_logical_id(
98 store: &impl TableRowVersionStore,
99 collection: &str,
100 logical_id: crate::storage::entity::EntityId,
101 select_versions: bool,
102 mut visible: impl FnMut(&crate::storage::entity::UnifiedEntity) -> bool,
103) -> Option<crate::storage::entity::UnifiedEntity> {
104 if select_versions {
105 return store
106 .table_row_versions_by_logical_id(collection, logical_id)
107 .into_iter()
108 .filter(|entity| visible(entity))
109 .max_by_key(|entity| entity.xmin);
110 }
111 store
112 .get_table_row_by_logical_id(collection, logical_id)
113 .filter(|entity| visible(entity))
114}