Skip to main content

reddb_server/runtime/
mvcc_lifecycle.rs

1//! MVCC pending-action lifecycle & commit-time conflict detection.
2//!
3//! Extracted from `impl_core.rs` (impl_core slice 3/10, issue #1624). Houses
4//! the five MVCC families that PRD #1620 (TM v2) will extend:
5//!
6//! - **Pending write-set records** — deferred tombstones, versioned updates,
7//!   store-WAL actions, and event-emission gating for DML.
8//! - **First-committer-wins conflict checks** — snapshot conflict detection
9//!   and logical/table-row write-conflict guards.
10//! - **Commit/rollback finalization** — stamp restore, tombstone/versioned
11//!   update finalize/revive, and savepoint-scoped revival.
12//! - **Transactional side-effect queues** — queue wakes, claim-lock release,
13//!   and KV watch events flushed at commit / dropped at rollback.
14//! - **Snapshot / xid accessors** — the per-connection snapshot, xid, and
15//!   vacuum-cutoff surface used by transports and tests.
16//!
17//! Behaviour-preserving move: every item keeps its name, signature and
18//! visibility so `execute_query_inner` and sibling-file callers need no
19//! call-site edits.
20
21use super::execution_context::current_connection_id;
22use super::*;
23
24impl RedDBRuntime {
25    /// Record that the running transaction has marked `id` in `collection`
26    /// for deletion (Phase 2.3.2b MVCC tombstones). `stamper_xid` is the
27    /// xid that was written into `xmax` — either the parent txn xid or
28    /// the innermost savepoint sub-xid. Savepoint rollback filters by
29    /// this xid to revive only its own tombstones.
30    pub(crate) fn record_pending_tombstone(
31        &self,
32        conn_id: u64,
33        collection: &str,
34        id: crate::storage::unified::entity::EntityId,
35        stamper_xid: crate::storage::transaction::snapshot::Xid,
36        previous_xmax: crate::storage::transaction::snapshot::Xid,
37    ) {
38        self.inner
39            .pending_tombstones
40            .write()
41            .entry(conn_id)
42            .or_default()
43            .push((collection.to_string(), id, stamper_xid, previous_xmax));
44    }
45
46    pub(crate) fn record_pending_versioned_update(
47        &self,
48        conn_id: u64,
49        collection: &str,
50        old_id: crate::storage::unified::entity::EntityId,
51        new_id: crate::storage::unified::entity::EntityId,
52        stamper_xid: crate::storage::transaction::snapshot::Xid,
53        previous_xmax: crate::storage::transaction::snapshot::Xid,
54    ) {
55        self.inner
56            .pending_versioned_updates
57            .write()
58            .entry(conn_id)
59            .or_default()
60            .push((
61                collection.to_string(),
62                old_id,
63                new_id,
64                stamper_xid,
65                previous_xmax,
66            ));
67    }
68
69    pub(crate) fn record_pending_queue_dedup(
70        &self,
71        conn_id: u64,
72        queue: &str,
73        dedup_key: &str,
74        metadata_id: crate::storage::unified::entity::EntityId,
75    ) {
76        self.inner
77            .pending_queue_dedup
78            .write()
79            .entry(conn_id)
80            .or_default()
81            .push((queue.to_string(), dedup_key.to_string(), metadata_id));
82    }
83
84    pub(crate) fn with_deferred_store_wal_if_transaction<T>(
85        &self,
86        f: impl FnOnce() -> RedDBResult<T>,
87    ) -> RedDBResult<T> {
88        let conn_id = current_connection_id();
89        if !self.inner.transaction_state.in_transaction(conn_id) {
90            return f();
91        }
92
93        crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
94        let result = f();
95        let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
96        match result {
97            Ok(value) => {
98                self.record_pending_store_wal_actions(conn_id, captured);
99                Ok(value)
100            }
101            Err(err) => Err(err),
102        }
103    }
104
105    pub(crate) fn with_deferred_store_wal_for_dml<T>(
106        &self,
107        capture_autocommit_events: bool,
108        f: impl FnOnce() -> RedDBResult<T>,
109    ) -> RedDBResult<T> {
110        let conn_id = current_connection_id();
111        if self.inner.transaction_state.in_transaction(conn_id) {
112            return self.with_deferred_store_wal_if_transaction(f);
113        }
114        if !capture_autocommit_events {
115            return f();
116        }
117
118        crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
119        let result = f();
120        let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
121        self.inner
122            .db
123            .store()
124            .append_deferred_store_wal_actions(captured)
125            .map_err(|err| RedDBError::Internal(err.to_string()))?;
126        result
127    }
128
129    pub(crate) fn insert_may_emit_events(&self, query: &InsertQuery) -> bool {
130        !query.suppress_events
131            && self.collection_has_event_subscriptions_for_operation(
132                &query.table,
133                crate::catalog::SubscriptionOperation::Insert,
134            )
135    }
136
137    pub(crate) fn update_may_emit_events(&self, query: &UpdateQuery) -> bool {
138        !query.suppress_events
139            && self.collection_has_event_subscriptions_for_operation(
140                &query.table,
141                crate::catalog::SubscriptionOperation::Update,
142            )
143    }
144
145    pub(crate) fn delete_may_emit_events(&self, query: &DeleteQuery) -> bool {
146        !query.suppress_events
147            && self.collection_has_event_subscriptions_for_operation(
148                &query.table,
149                crate::catalog::SubscriptionOperation::Delete,
150            )
151    }
152
153    fn collection_has_event_subscriptions_for_operation(
154        &self,
155        collection: &str,
156        operation: crate::catalog::SubscriptionOperation,
157    ) -> bool {
158        let Some(contract) = self.db().collection_contract_arc(collection) else {
159            return false;
160        };
161        contract.subscriptions.iter().any(|subscription| {
162            subscription.enabled
163                && (subscription.ops_filter.is_empty()
164                    || subscription.ops_filter.contains(&operation))
165        })
166    }
167
168    fn record_pending_store_wal_actions(
169        &self,
170        conn_id: u64,
171        actions: crate::storage::unified::DeferredStoreWalActions,
172    ) {
173        if actions.is_empty() {
174            return;
175        }
176        let mut guard = self.inner.pending_store_wal_actions.write();
177        guard.entry(conn_id).or_default().extend(actions);
178    }
179
180    pub(crate) fn flush_pending_store_wal_actions(&self, conn_id: u64) -> RedDBResult<()> {
181        let Some(actions) = self
182            .inner
183            .pending_store_wal_actions
184            .write()
185            .remove(&conn_id)
186        else {
187            return Ok(());
188        };
189        self.inner
190            .db
191            .store()
192            .append_deferred_store_wal_actions(actions)
193            .map_err(|err| RedDBError::Internal(err.to_string()))
194    }
195
196    pub(crate) fn discard_pending_store_wal_actions(&self, conn_id: u64) {
197        self.inner
198            .pending_store_wal_actions
199            .write()
200            .remove(&conn_id);
201    }
202
203    fn xid_conflicts_with_snapshot(
204        &self,
205        xid: crate::storage::transaction::snapshot::Xid,
206        snapshot: &crate::storage::transaction::snapshot::Snapshot,
207        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
208    ) -> bool {
209        xid != 0
210            && !own_xids.contains(&xid)
211            && !self
212                .inner
213                .transaction_state
214                .snapshot_manager()
215                .is_aborted(xid)
216            && !self
217                .inner
218                .transaction_state
219                .snapshot_manager()
220                .is_active(xid)
221            && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
222    }
223
224    fn conflict_error(
225        collection: &str,
226        logical_id: crate::storage::unified::entity::EntityId,
227        xid: crate::storage::transaction::snapshot::Xid,
228    ) -> RedDBError {
229        RedDBError::Query(format!(
230            "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
231            logical_id.raw()
232        ))
233    }
234
235    fn check_logical_row_conflict(
236        &self,
237        collection: &str,
238        logical_id: crate::storage::unified::entity::EntityId,
239        excluded_ids: &[crate::storage::unified::entity::EntityId],
240        snapshot: &crate::storage::transaction::snapshot::Snapshot,
241        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
242    ) -> RedDBResult<()> {
243        let store = self.inner.db.store();
244        let Some(manager) = store.get_collection(collection) else {
245            return Ok(());
246        };
247
248        for candidate in manager.query_all(|_| true) {
249            if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
250                continue;
251            }
252            if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
253                return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
254            }
255            if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
256                return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
257            }
258        }
259        Ok(())
260    }
261
262    pub(crate) fn check_table_row_write_conflicts(
263        &self,
264        conn_id: u64,
265        snapshot: &crate::storage::transaction::snapshot::Snapshot,
266        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
267        isolation: crate::storage::transaction::IsolationLevel,
268    ) -> RedDBResult<()> {
269        let versioned_updates = self
270            .inner
271            .pending_versioned_updates
272            .read()
273            .get(&conn_id)
274            .cloned()
275            .unwrap_or_default();
276        let tombstones = self
277            .inner
278            .pending_tombstones
279            .read()
280            .get(&conn_id)
281            .cloned()
282            .unwrap_or_default();
283
284        let mut serializable_write_set = std::collections::HashSet::new();
285        let store = self.inner.db.store();
286        for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
287            let Some(manager) = store.get_collection(&collection) else {
288                continue;
289            };
290            let Some(old) = manager.get(old_id) else {
291                continue;
292            };
293            let logical_id = old.logical_id();
294            serializable_write_set.insert((collection.clone(), logical_id));
295            if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
296                return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
297            }
298            if old.xmax != xid && self.xid_conflicts_with_snapshot(old.xmax, snapshot, own_xids) {
299                return Err(Self::conflict_error(&collection, logical_id, old.xmax));
300            }
301            self.check_logical_row_conflict(
302                &collection,
303                logical_id,
304                &[old_id, new_id],
305                snapshot,
306                own_xids,
307            )?;
308        }
309
310        for (collection, id, xid, previous_xmax) in tombstones {
311            let Some(manager) = store.get_collection(&collection) else {
312                continue;
313            };
314            let Some(entity) = manager.get(id) else {
315                continue;
316            };
317            let logical_id = entity.logical_id();
318            serializable_write_set.insert((collection.clone(), logical_id));
319            if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
320                return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
321            }
322            if entity.xmax != xid
323                && self.xid_conflicts_with_snapshot(entity.xmax, snapshot, own_xids)
324            {
325                return Err(Self::conflict_error(&collection, logical_id, entity.xmax));
326            }
327            self.check_logical_row_conflict(&collection, logical_id, &[id], snapshot, own_xids)?;
328        }
329
330        if isolation == crate::storage::transaction::IsolationLevel::Serializable
331            && self
332                .inner
333                .transaction_state
334                .snapshot_manager()
335                .serializable_commit_would_be_dangerous(
336                    *own_xids.iter().min().unwrap_or(&0),
337                    &serializable_write_set,
338                )
339        {
340            return Err(RedDBError::Query(
341                "serialization conflict: serializable transaction would complete rw-antidependency dangerous structure".to_string(),
342            ));
343        }
344
345        Ok(())
346    }
347
348    pub(crate) fn check_queue_dedup_write_conflicts(
349        &self,
350        conn_id: u64,
351        snapshot: &crate::storage::transaction::snapshot::Snapshot,
352        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
353    ) -> RedDBResult<()> {
354        let pending = self
355            .inner
356            .pending_queue_dedup
357            .read()
358            .get(&conn_id)
359            .cloned()
360            .unwrap_or_default();
361        if pending.is_empty() {
362            return Ok(());
363        }
364
365        let store = self.inner.db.store();
366        let Some(manager) = store.get_collection(crate::runtime::impl_queue::QUEUE_META_COLLECTION)
367        else {
368            return Ok(());
369        };
370
371        for (queue, dedup_key, metadata_id) in pending {
372            for candidate in manager.query_all(|entity| {
373                entity.data.as_row().is_some_and(|row| {
374                    crate::runtime::impl_queue::row_text(row, "kind").as_deref()
375                        == Some(crate::runtime::impl_queue::KIND_QUEUE_PUSH_DEDUP)
376                        && crate::runtime::impl_queue::row_text(row, "queue").as_deref()
377                            == Some(&queue)
378                        && crate::runtime::impl_queue::row_text(row, "dedup_key").as_deref()
379                            == Some(&dedup_key)
380                })
381            }) {
382                if candidate.id == metadata_id
383                    || self
384                        .inner
385                        .transaction_state
386                        .snapshot_manager()
387                        .is_aborted(candidate.xmin)
388                {
389                    continue;
390                }
391                if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
392                    return Err(RedDBError::Query(format!(
393                        "serialization conflict: queue dedup key {queue}/{dedup_key} was modified by concurrent transaction {}",
394                        candidate.xmin
395                    )));
396                }
397            }
398        }
399        Ok(())
400    }
401
402    pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
403        let versioned_updates = self
404            .inner
405            .pending_versioned_updates
406            .read()
407            .get(&conn_id)
408            .cloned()
409            .unwrap_or_default();
410        let tombstones = self
411            .inner
412            .pending_tombstones
413            .read()
414            .get(&conn_id)
415            .cloned()
416            .unwrap_or_default();
417
418        let store = self.inner.db.store();
419        for (collection, old_id, _new_id, xid, _previous_xmax) in versioned_updates {
420            if let Some(manager) = store.get_collection(&collection) {
421                if let Some(mut entity) = manager.get(old_id) {
422                    entity.set_xmax(xid);
423                    let _ = manager.update(entity);
424                }
425            }
426        }
427        for (collection, id, xid, _previous_xmax) in tombstones {
428            if let Some(manager) = store.get_collection(&collection) {
429                if let Some(mut entity) = manager.get(id) {
430                    entity.set_xmax(xid);
431                    let _ = manager.update(entity);
432                }
433            }
434        }
435    }
436
437    pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
438        self.inner
439            .pending_versioned_updates
440            .write()
441            .remove(&conn_id);
442    }
443
444    pub(crate) fn finalize_pending_queue_dedup(&self, conn_id: u64) {
445        self.inner.pending_queue_dedup.write().remove(&conn_id);
446    }
447
448    pub(crate) fn discard_pending_queue_dedup(&self, conn_id: u64) {
449        let Some(pending) = self.inner.pending_queue_dedup.write().remove(&conn_id) else {
450            return;
451        };
452        let store = self.inner.db.store();
453        for (_queue, _dedup_key, metadata_id) in pending {
454            let _ = store.delete(
455                crate::runtime::impl_queue::QUEUE_META_COLLECTION,
456                metadata_id,
457            );
458        }
459    }
460
461    pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) -> RedDBResult<()> {
462        let Some(pending) = self
463            .inner
464            .pending_versioned_updates
465            .write()
466            .remove(&conn_id)
467        else {
468            return Ok(());
469        };
470        // Undo newest versions first so repeated writes restore the original chain.
471        for (collection, old_id, new_id, xid, previous_xmax) in pending.into_iter().rev() {
472            let topology_lock = self.index_store_ref().collection_topology_lock(&collection);
473            self.revive_versioned_update(
474                &collection,
475                old_id,
476                new_id,
477                xid,
478                previous_xmax,
479                topology_lock.read(),
480            )?;
481        }
482        Ok(())
483    }
484
485    pub(crate) fn revive_versioned_update(
486        &self,
487        collection: &str,
488        old_id: crate::storage::EntityId,
489        new_id: crate::storage::EntityId,
490        xid: u64,
491        previous_xmax: u64,
492        _topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
493    ) -> RedDBResult<()> {
494        let store = self.inner.db.store();
495        if let Some(new) = store.get(collection, new_id) {
496            let fields = crate::application::ports::entity_row_fields_snapshot(&new);
497            self.index_store_ref()
498                .index_entity_delete(collection, new_id, &fields)
499                .map_err(RedDBError::Internal)?;
500        }
501        store
502            .delete_batch(collection, &[new_id])
503            .map_err(|error| RedDBError::Internal(error.to_string()))?;
504        if let Some(manager) = store.get_collection(collection) {
505            if let Some(mut old) = manager.get(old_id) {
506                if old.xmax == xid {
507                    old.set_xmax(previous_xmax);
508                    manager
509                        .update(old.clone())
510                        .map_err(|error| RedDBError::Internal(error.to_string()))?;
511                    store.context_index().index_entity(collection, &old);
512                    let fields = crate::application::ports::entity_row_fields_snapshot(&old);
513                    self.index_store_ref()
514                        .index_entity_insert(collection, old_id, &fields)
515                        .map_err(RedDBError::Internal)?;
516                }
517            }
518        }
519        Ok(())
520    }
521
522    /// Flush tombstones on COMMIT. The xmax stamp is already the durable
523    /// delete marker; commit only drops the rollback journal and emits
524    /// side effects. Physical reclamation is left for VACUUM so old
525    /// snapshots can still resolve the pre-delete row version.
526    pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
527        let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
528            return;
529        };
530        if pending.is_empty() {
531            return;
532        }
533
534        let store = self.inner.db.store();
535        for (collection, id, _xid, _previous_xmax) in pending {
536            store.context_index().remove_entity(id);
537            self.cdc_emit(
538                crate::replication::cdc::ChangeOperation::Delete,
539                &collection,
540                id.raw(),
541                "entity",
542            );
543        }
544    }
545
546    /// Revive tombstones on ROLLBACK — reset `xmax` to 0 so the tuples
547    /// become visible again to future snapshots. Best-effort: a row
548    /// already reclaimed by a concurrent VACUUM stays gone, but VACUUM
549    /// never reclaims tuples whose xmax is still referenced by any
550    /// active snapshot, so this case is only reachable via external
551    /// storage corruption.
552    pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
553        let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
554            return;
555        };
556
557        let store = self.inner.db.store();
558        for (collection, id, xid, previous_xmax) in pending {
559            let Some(manager) = store.get_collection(&collection) else {
560                continue;
561            };
562            if let Some(mut entity) = manager.get(id) {
563                if entity.xmax == xid {
564                    entity.set_xmax(previous_xmax);
565                    let _ = manager.update(entity);
566                }
567            }
568        }
569    }
570
571    /// Slice C of PRD #718 — accessor for the local wait registry.
572    pub fn queue_wait_registry(
573        &self,
574    ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
575        self.inner.queue_wait_registry.clone()
576    }
577
578    /// Buffer a `(scope, queue)` wake on the current connection so it
579    /// fires post-COMMIT, or notify immediately if no transaction is
580    /// open (autocommit path). The wait registry only ever observes
581    /// notifies for committed work — rollback drops the buffer.
582    pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
583        if self.current_xid().is_some() {
584            let conn_id = current_connection_id();
585            self.inner
586                .pending_queue_wakes
587                .write()
588                .entry(conn_id)
589                .or_default()
590                .push((scope.to_string(), queue.to_string()));
591            return;
592        }
593        self.inner.queue_wait_registry.notify(scope, queue);
594    }
595
596    pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
597        let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
598            return;
599        };
600        for (scope, queue) in pending {
601            self.inner.queue_wait_registry.notify(&scope, &queue);
602        }
603    }
604
605    pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
606        self.inner.pending_queue_wakes.write().remove(&conn_id);
607    }
608
609    pub(crate) fn release_pending_claim_locks(&self, conn_id: u64) {
610        self.inner
611            .pending_claim_locks
612            .write()
613            .retain(|_, owner| *owner != conn_id);
614    }
615
616    pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
617        let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
618            return;
619        };
620        for event in pending {
621            self.cdc_emit_kv(
622                event.op,
623                &event.collection,
624                &event.key,
625                0,
626                event.before,
627                event.after,
628            );
629        }
630    }
631
632    pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
633        self.inner.pending_kv_watch_events.write().remove(&conn_id);
634    }
635
636    /// Phase 1.1 MVCC universal: post-save hook that stamps `xmin` on a
637    /// freshly-inserted entity when the current connection holds an
638    /// open transaction. Used by graph / queue / timeseries
639    /// write paths that go through the DevX builder API (`db.node(...)
640    /// .save()` and friends) — those live in the storage crate and
641    /// can't reach `current_xid()` without crossing layers, so the
642    /// application layer calls this helper right after `save()` to
643    /// finalise the MVCC stamp.
644    ///
645    /// Autocommit (outside BEGIN) is a no-op — no extra lookup or
646    /// write, so the non-transactional hot path stays untouched.
647    ///
648    /// Best-effort: if the collection or entity disappears between
649    /// the save and the stamp (concurrent DROP), we silently skip.
650    pub(crate) fn stamp_xmin_if_in_txn(
651        &self,
652        collection: &str,
653        id: crate::storage::unified::entity::EntityId,
654    ) {
655        let Some(xid) = self.current_xid() else {
656            return;
657        };
658        let store = self.inner.db.store();
659        let Some(manager) = store.get_collection(collection) else {
660            return;
661        };
662        if let Some(mut entity) = manager.get(id) {
663            entity.set_xmin(xid);
664            let _ = manager.update(entity);
665        }
666    }
667
668    /// Revive tombstones stamped by `stamper_xid` or any sub-xid
669    /// allocated after it (Phase 2.3.2e savepoint rollback). Any
670    /// pending entries with `xid < stamper_xid` stay queued because
671    /// they belong to the enclosing scope — they'll either flush on
672    /// COMMIT or revive on an outer ROLLBACK TO SAVEPOINT.
673    ///
674    /// Returns the number of tuples whose `xmax` was wiped back to 0.
675    pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
676        let mut guard = self.inner.pending_tombstones.write();
677        let Some(pending) = guard.get_mut(&conn_id) else {
678            return 0;
679        };
680
681        let store = self.inner.db.store();
682        let mut revived = 0usize;
683        pending.retain(|(collection, id, xid, previous_xmax)| {
684            if *xid < stamper_xid {
685                // Stamped before the savepoint — keep in queue.
686                return true;
687            }
688            if let Some(manager) = store.get_collection(collection) {
689                if let Some(mut entity) = manager.get(*id) {
690                    if entity.xmax == *xid {
691                        entity.set_xmax(*previous_xmax);
692                        let _ = manager.update(entity);
693                        revived += 1;
694                    }
695                }
696            }
697            false
698        });
699        if pending.is_empty() {
700            guard.remove(&conn_id);
701        }
702        revived
703    }
704
705    /// Return the snapshot the current connection should use for visibility
706    /// checks (Phase 2.3 PG parity).
707    ///
708    /// * If the connection is inside a READ COMMITTED transaction, capture
709    ///   a fresh statement snapshot while preserving the transaction's xid
710    ///   and write-set lifecycle.
711    /// * If the connection is inside a SNAPSHOT transaction, reuse the
712    ///   snapshot stored in its `TxnContext`.
713    /// * Otherwise (autocommit), capture a fresh snapshot tied to an
714    ///   implicit xid=0 — the read path treats pre-MVCC rows as always
715    ///   visible so this degrades to "see everything committed".
716    pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
717        self.inner
718            .transaction_state
719            .current_snapshot(current_connection_id())
720    }
721
722    /// Xid of the current connection's active transaction, or `None` when
723    /// running outside a BEGIN/COMMIT block. Write paths call this to
724    /// decide whether to stamp `xmin`/`xmax` on tuples.
725    /// Phase 2.3.2e: when a savepoint is open, `writer_xid` returns the
726    /// sub-xid so new writes can be selectively rolled back. Otherwise
727    /// the parent txn's xid is returned, matching pre-savepoint
728    /// behaviour. Callers that need the enclosing *transaction* xid
729    /// (e.g. VACUUM min-active calculations) should read `ctx.xid`
730    /// directly.
731    pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
732        self.inner
733            .transaction_state
734            .writer_xid(current_connection_id())
735    }
736
737    pub(crate) fn own_transaction_xids(&self) -> std::collections::HashSet<u64> {
738        self.inner
739            .transaction_state
740            .own_xids(current_connection_id())
741    }
742
743    /// `true` when the given connection id has an open `BEGIN`. Issue
744    /// #760 — `OpenStream` consults this to refuse output streams that
745    /// would otherwise collide with an interactive transaction (see
746    /// ADR 0029 "Transaction interaction"). HTTP requests pre-dating the
747    /// connection-id plumbing run with id `0`, which never carries a
748    /// transaction context, so this returns `false` on those paths.
749    pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
750        self.inner.transaction_state.in_transaction(conn_id)
751    }
752
753    /// Access the shared `SnapshotManager` — useful for VACUUM to compute
754    /// the oldest-active xid when reclaiming dead tuples.
755    pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
756        self.inner.transaction_state.snapshot_manager()
757    }
758
759    pub(crate) fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
760        let manager = self.inner.transaction_state.snapshot_manager();
761        let next_xid = manager.peek_next_xid();
762        let mut cutoff = next_xid;
763        if let Some(oldest_active) = manager.oldest_active_xid() {
764            cutoff = cutoff.min(oldest_active);
765        }
766        if let Some(oldest_pinned) = manager.oldest_pinned_xid() {
767            cutoff = cutoff.min(oldest_pinned);
768        }
769        let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
770        if retention_xids > 0 {
771            cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
772        }
773        cutoff
774    }
775
776    /// Own-tx xids (parent + open/released savepoints) for the current
777    /// connection. Transports + tests that build a `SnapshotContext`
778    /// manually (outside the `execute_query` scope) need this set so
779    /// the writer's own uncommitted tuples stay visible to self.
780    pub fn current_txn_own_xids(
781        &self,
782    ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
783        self.inner
784            .transaction_state
785            .own_xids(current_connection_id())
786    }
787}