Skip to main content

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 branch: Option<Arc<BranchReadView>>,
22    pub snapshot: super::snapshot::Snapshot,
23    pub manager: Arc<super::snapshot::SnapshotManager>,
24    pub own_xids: std::collections::HashSet<super::snapshot::Xid>,
25    pub requires_index_fallback: bool,
26    pub serializable_reader: Option<super::snapshot::Xid>,
27}
28
29/// Each inherited epoch is frozen at its fork/merge snapshot. Only the current
30/// writer epoch follows the connection's live transaction snapshot.
31#[derive(Clone, Debug)]
32pub struct BranchReadView {
33    pub writer_epoch: u64,
34    pub epochs: std::collections::BTreeMap<u64, super::snapshot::Snapshot>,
35}
36
37/// Unique keys reserve pending writes in the current epoch, but ancestor
38/// epochs keep their frozen fork view. A sibling branch never reserves a key.
39pub fn entity_reserves_unique_key(
40    ctx: &SnapshotContext,
41    entity: &crate::storage::entity::UnifiedEntity,
42) -> bool {
43    let (Some(view), Some(version)) = (&ctx.branch, entity.branch_version()) else {
44        return ctx
45            .manager
46            .row_reserves_unique_key(entity.xmin, entity.xmax, &ctx.own_xids);
47    };
48    let inserted = if version.origin == view.writer_epoch {
49        ctx.manager
50            .row_reserves_unique_key(entity.xmin, 0, &ctx.own_xids)
51    } else {
52        view.epochs.get(&version.origin).is_some_and(|snapshot| {
53            !ctx.manager.is_aborted(entity.xmin) && snapshot.sees(entity.xmin, 0)
54        })
55    };
56    if !inserted {
57        return false;
58    }
59    let deleted = |epoch, xid| {
60        if xid == 0 {
61            return false;
62        }
63        if epoch == view.writer_epoch {
64            !ctx.manager.row_reserves_unique_key(0, xid, &ctx.own_xids)
65        } else {
66            view.epochs
67                .get(&epoch)
68                .is_some_and(|snapshot| !ctx.manager.is_aborted(xid) && snapshot.sees(xid, 0))
69        }
70    };
71    !deleted(version.origin, entity.xmax)
72        && !version
73            .deletions
74            .iter()
75            .any(|&(epoch, xid)| deleted(epoch, xid))
76}
77
78pub fn entity_visibility_check(
79    ctx: &SnapshotContext,
80    entity: &crate::storage::entity::UnifiedEntity,
81) -> bool {
82    let (Some(view), Some(version)) = (&ctx.branch, entity.branch_version()) else {
83        return visibility_check(ctx, entity.xmin, entity.xmax);
84    };
85    let visible_xid = |epoch, xid| {
86        view.epochs.get(&epoch).is_some_and(|snapshot| {
87            xid == 0
88                || (!ctx.manager.is_aborted(xid)
89                    && (ctx.own_xids.contains(&xid) || snapshot.sees(xid, 0)))
90        })
91    };
92    visible_xid(version.origin, entity.xmin)
93        && (entity.xmax == 0 || !visible_xid(version.origin, entity.xmax))
94        && !version
95            .deletions
96            .iter()
97            .any(|&(epoch, xid)| visible_xid(epoch, xid))
98}
99
100#[cfg(test)]
101mod branch_tests {
102    use super::*;
103    use crate::storage::entity::{EntityId, UnifiedEntity};
104    use crate::transaction::snapshot::{Snapshot, SnapshotManager};
105    use std::collections::{BTreeMap, HashSet};
106
107    fn context(epoch: u64, inherited: u64) -> SnapshotContext {
108        let mut epochs = BTreeMap::from([(
109            0,
110            Snapshot {
111                xid: inherited,
112                in_progress: HashSet::new(),
113            },
114        )]);
115        epochs.insert(
116            epoch,
117            Snapshot {
118                xid: 100,
119                in_progress: HashSet::new(),
120            },
121        );
122        SnapshotContext {
123            branch: Some(Arc::new(BranchReadView {
124                writer_epoch: epoch,
125                epochs,
126            })),
127            snapshot: Snapshot {
128                xid: 100,
129                in_progress: HashSet::new(),
130            },
131            manager: Arc::new(SnapshotManager::new()),
132            own_xids: HashSet::new(),
133            requires_index_fallback: true,
134            serializable_reader: None,
135        }
136    }
137
138    #[test]
139    fn sibling_updates_and_deletes_have_independent_visibility() {
140        let mut base = UnifiedEntity::table_row(EntityId::new(1), "items", 1, vec![]);
141        base.set_xmin(10);
142        base.set_branch_origin(0);
143        base.set_branch_xmax(0, 30);
144        base.set_branch_xmax(1, 40);
145        let main = context(0, 100);
146        let first = context(1, 20);
147        let second = context(2, 20);
148        assert!(!entity_visibility_check(&main, &base));
149        assert!(!entity_visibility_check(&first, &base));
150        assert!(entity_visibility_check(&second, &base));
151        base.set_branch_xmax(2, 50);
152        base.set_branch_xmax(1, 0);
153        assert!(
154            entity_visibility_check(&first, &base),
155            "rollback only revives its branch"
156        );
157        assert!(!entity_visibility_check(&second, &base));
158        let mut replacement = base.clone();
159        replacement.set_branch_origin(1);
160        replacement.set_xmin(40);
161        replacement.set_xmax(0);
162        assert!(entity_visibility_check(&first, &replacement));
163        assert!(!entity_visibility_check(&main, &replacement));
164        assert!(!entity_visibility_check(&second, &replacement));
165    }
166
167    #[test]
168    fn active_and_aborted_branch_deletes_remain_invisible() {
169        let mut entity = UnifiedEntity::table_row(EntityId::new(1), "items", 1, vec![]);
170        entity.set_branch_origin(0);
171        entity.set_xmin(10);
172        entity.set_branch_xmax(1, 40);
173        let mut reader = context(1, 20);
174        Arc::make_mut(reader.branch.as_mut().expect("view"))
175            .epochs
176            .get_mut(&1)
177            .expect("writer epoch")
178            .in_progress
179            .insert(40);
180        assert!(entity_visibility_check(&reader, &entity));
181        reader.own_xids.insert(40);
182        assert!(!entity_visibility_check(&reader, &entity));
183        reader.manager.rollback(40);
184        assert!(entity_visibility_check(&reader, &entity));
185    }
186}
187
188/// Record an admitted table-row read after the host has applied visibility and
189/// policy gates. SSI keys use logical identity, shared by all physical versions.
190pub fn record_serializable_read(
191    ctx: &SnapshotContext,
192    entity: &crate::storage::entity::UnifiedEntity,
193) {
194    let Some(reader) = ctx.serializable_reader else {
195        return;
196    };
197    if !matches!(
198        &entity.kind,
199        crate::storage::entity::EntityKind::TableRow { .. }
200    ) {
201        return;
202    }
203    ctx.manager
204        .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
205}
206
207/// Apply captured MVCC visibility, including own/aborted savepoint writers.
208/// This predicate supplies no authorization, tenant or moderation policy.
209#[inline]
210pub fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
211    // Writer aborted → tuple never existed from any future reader's view.
212    // Checked *before* the own-xids fast path so an aborted own-sub-xid
213    // (rolled-back savepoint) stays hidden from the parent.
214    if xmin != 0 && ctx.manager.is_aborted(xmin) {
215        return false;
216    }
217    // Deleter aborted → treat xmax as unset; fall back to xmin-only check.
218    let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
219        0
220    } else {
221        xmax
222    };
223    // Phase 2.3.2e: own-tx writes are always visible to the connection
224    // that stamped them, even when xmin/xmax exceed `snapshot.xid` (as
225    // happens for sub-xids allocated by SAVEPOINT after BEGIN).
226    let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
227    let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
228    if own_xmax {
229        // This connection deleted the row via this xid — hide it from self.
230        return false;
231    }
232    if own_xmin {
233        return true;
234    }
235    ctx.snapshot.sees(xmin, effective_xmax)
236}
237
238/// Candidate access for the canonical table-row version selector. The store
239/// supplies only physical versions of the requested collection/logical identity.
240pub trait TableRowVersionStore {
241    fn table_row_versions_by_logical_id(
242        &self,
243        collection: &str,
244        logical_id: crate::storage::entity::EntityId,
245    ) -> Vec<crate::storage::entity::UnifiedEntity>;
246
247    fn get_table_row_by_logical_id(
248        &self,
249        collection: &str,
250        logical_id: crate::storage::entity::EntityId,
251    ) -> Option<crate::storage::entity::UnifiedEntity>;
252}
253
254/// Select the newest visible physical version, or use the current-row lookup
255/// when the host has no snapshot requiring history. The host's predicate retains
256/// table-kind, authorization and moderation gates before materialization.
257pub fn resolve_logical_id(
258    store: &impl TableRowVersionStore,
259    collection: &str,
260    logical_id: crate::storage::entity::EntityId,
261    select_versions: bool,
262    mut visible: impl FnMut(&crate::storage::entity::UnifiedEntity) -> bool,
263) -> Option<crate::storage::entity::UnifiedEntity> {
264    if select_versions {
265        return store
266            .table_row_versions_by_logical_id(collection, logical_id)
267            .into_iter()
268            .filter(|entity| visible(entity))
269            .max_by_key(|entity| entity.xmin);
270    }
271    store
272        .get_table_row_by_logical_id(collection, logical_id)
273        .filter(|entity| visible(entity))
274}
275
276/// Fallible version access streams candidates instead of retaining a history Vec.
277/// Hosts supply only versions of the requested collection/logical identity.
278pub trait FallibleTableRowVersionStore {
279    type Error;
280
281    fn try_get_table_row_by_logical_id(
282        &self,
283        collection: &str,
284        logical_id: crate::storage::entity::EntityId,
285    ) -> Result<Option<crate::storage::entity::UnifiedEntity>, Self::Error>;
286
287    fn try_for_each_table_row_version(
288        &self,
289        collection: &str,
290        logical_id: crate::storage::entity::EntityId,
291        visit: impl FnMut(&crate::storage::entity::UnifiedEntity) -> bool,
292    ) -> Result<(), Self::Error>;
293}
294
295/// Retain one selected version; a later read failure invalidates the selection.
296pub fn try_resolve_logical_id<S: FallibleTableRowVersionStore>(
297    store: &S,
298    collection: &str,
299    logical_id: crate::storage::entity::EntityId,
300    select_versions: bool,
301    mut visible: impl FnMut(&crate::storage::entity::UnifiedEntity) -> bool,
302) -> Result<Option<crate::storage::entity::UnifiedEntity>, S::Error> {
303    if !select_versions {
304        return store
305            .try_get_table_row_by_logical_id(collection, logical_id)
306            .map(|entity| entity.filter(|entity| visible(entity)));
307    }
308    let mut selected: Option<crate::storage::entity::UnifiedEntity> = None;
309    store.try_for_each_table_row_version(collection, logical_id, |entity| {
310        if visible(entity) && selected.as_ref().is_none_or(|old| entity.xmin >= old.xmin) {
311            selected = Some(entity.clone());
312        }
313        true
314    })?;
315    Ok(selected)
316}