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.tx_contexts.read().contains_key(&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.tx_contexts.read().contains_key(&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.inner.snapshot_manager.is_aborted(xid)
212            && !self.inner.snapshot_manager.is_active(xid)
213            && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
214    }
215
216    fn conflict_error(
217        collection: &str,
218        logical_id: crate::storage::unified::entity::EntityId,
219        xid: crate::storage::transaction::snapshot::Xid,
220    ) -> RedDBError {
221        RedDBError::Query(format!(
222            "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
223            logical_id.raw()
224        ))
225    }
226
227    fn check_logical_row_conflict(
228        &self,
229        collection: &str,
230        logical_id: crate::storage::unified::entity::EntityId,
231        excluded_ids: &[crate::storage::unified::entity::EntityId],
232        snapshot: &crate::storage::transaction::snapshot::Snapshot,
233        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
234    ) -> RedDBResult<()> {
235        let store = self.inner.db.store();
236        let Some(manager) = store.get_collection(collection) else {
237            return Ok(());
238        };
239
240        for candidate in manager.query_all(|_| true) {
241            if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
242                continue;
243            }
244            if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
245                return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
246            }
247            if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
248                return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
249            }
250        }
251        Ok(())
252    }
253
254    pub(crate) fn check_table_row_write_conflicts(
255        &self,
256        conn_id: u64,
257        snapshot: &crate::storage::transaction::snapshot::Snapshot,
258        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
259        isolation: crate::storage::transaction::IsolationLevel,
260    ) -> RedDBResult<()> {
261        let versioned_updates = self
262            .inner
263            .pending_versioned_updates
264            .read()
265            .get(&conn_id)
266            .cloned()
267            .unwrap_or_default();
268        let tombstones = self
269            .inner
270            .pending_tombstones
271            .read()
272            .get(&conn_id)
273            .cloned()
274            .unwrap_or_default();
275
276        let mut serializable_write_set = std::collections::HashSet::new();
277        let store = self.inner.db.store();
278        for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
279            let Some(manager) = store.get_collection(&collection) else {
280                continue;
281            };
282            let Some(old) = manager.get(old_id) else {
283                continue;
284            };
285            let logical_id = old.logical_id();
286            serializable_write_set.insert((collection.clone(), logical_id));
287            if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
288                return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
289            }
290            if old.xmax != xid && self.xid_conflicts_with_snapshot(old.xmax, snapshot, own_xids) {
291                return Err(Self::conflict_error(&collection, logical_id, old.xmax));
292            }
293            self.check_logical_row_conflict(
294                &collection,
295                logical_id,
296                &[old_id, new_id],
297                snapshot,
298                own_xids,
299            )?;
300        }
301
302        for (collection, id, xid, previous_xmax) in tombstones {
303            let Some(manager) = store.get_collection(&collection) else {
304                continue;
305            };
306            let Some(entity) = manager.get(id) else {
307                continue;
308            };
309            let logical_id = entity.logical_id();
310            serializable_write_set.insert((collection.clone(), logical_id));
311            if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
312                return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
313            }
314            if entity.xmax != xid
315                && self.xid_conflicts_with_snapshot(entity.xmax, snapshot, own_xids)
316            {
317                return Err(Self::conflict_error(&collection, logical_id, entity.xmax));
318            }
319            self.check_logical_row_conflict(&collection, logical_id, &[id], snapshot, own_xids)?;
320        }
321
322        if isolation == crate::storage::transaction::IsolationLevel::Serializable
323            && self
324                .inner
325                .snapshot_manager
326                .serializable_commit_would_be_dangerous(
327                    *own_xids.iter().min().unwrap_or(&0),
328                    &serializable_write_set,
329                )
330        {
331            return Err(RedDBError::Query(
332                "serialization conflict: serializable transaction would complete rw-antidependency dangerous structure".to_string(),
333            ));
334        }
335
336        Ok(())
337    }
338
339    pub(crate) fn check_queue_dedup_write_conflicts(
340        &self,
341        conn_id: u64,
342        snapshot: &crate::storage::transaction::snapshot::Snapshot,
343        own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
344    ) -> RedDBResult<()> {
345        let pending = self
346            .inner
347            .pending_queue_dedup
348            .read()
349            .get(&conn_id)
350            .cloned()
351            .unwrap_or_default();
352        if pending.is_empty() {
353            return Ok(());
354        }
355
356        let store = self.inner.db.store();
357        let Some(manager) = store.get_collection(crate::runtime::impl_queue::QUEUE_META_COLLECTION)
358        else {
359            return Ok(());
360        };
361
362        for (queue, dedup_key, metadata_id) in pending {
363            for candidate in manager.query_all(|entity| {
364                entity.data.as_row().is_some_and(|row| {
365                    crate::runtime::impl_queue::row_text(row, "kind").as_deref()
366                        == Some(crate::runtime::impl_queue::KIND_QUEUE_PUSH_DEDUP)
367                        && crate::runtime::impl_queue::row_text(row, "queue").as_deref()
368                            == Some(&queue)
369                        && crate::runtime::impl_queue::row_text(row, "dedup_key").as_deref()
370                            == Some(&dedup_key)
371                })
372            }) {
373                if candidate.id == metadata_id
374                    || self.inner.snapshot_manager.is_aborted(candidate.xmin)
375                {
376                    continue;
377                }
378                if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
379                    return Err(RedDBError::Query(format!(
380                        "serialization conflict: queue dedup key {queue}/{dedup_key} was modified by concurrent transaction {}",
381                        candidate.xmin
382                    )));
383                }
384            }
385        }
386        Ok(())
387    }
388
389    pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
390        let versioned_updates = self
391            .inner
392            .pending_versioned_updates
393            .read()
394            .get(&conn_id)
395            .cloned()
396            .unwrap_or_default();
397        let tombstones = self
398            .inner
399            .pending_tombstones
400            .read()
401            .get(&conn_id)
402            .cloned()
403            .unwrap_or_default();
404
405        let store = self.inner.db.store();
406        for (collection, old_id, _new_id, xid, _previous_xmax) in versioned_updates {
407            if let Some(manager) = store.get_collection(&collection) {
408                if let Some(mut entity) = manager.get(old_id) {
409                    entity.set_xmax(xid);
410                    let _ = manager.update(entity);
411                }
412            }
413        }
414        for (collection, id, xid, _previous_xmax) in tombstones {
415            if let Some(manager) = store.get_collection(&collection) {
416                if let Some(mut entity) = manager.get(id) {
417                    entity.set_xmax(xid);
418                    let _ = manager.update(entity);
419                }
420            }
421        }
422    }
423
424    pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
425        self.inner
426            .pending_versioned_updates
427            .write()
428            .remove(&conn_id);
429    }
430
431    pub(crate) fn finalize_pending_queue_dedup(&self, conn_id: u64) {
432        self.inner.pending_queue_dedup.write().remove(&conn_id);
433    }
434
435    pub(crate) fn discard_pending_queue_dedup(&self, conn_id: u64) {
436        let Some(pending) = self.inner.pending_queue_dedup.write().remove(&conn_id) else {
437            return;
438        };
439        let store = self.inner.db.store();
440        for (_queue, _dedup_key, metadata_id) in pending {
441            let _ = store.delete(
442                crate::runtime::impl_queue::QUEUE_META_COLLECTION,
443                metadata_id,
444            );
445        }
446    }
447
448    pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) {
449        let Some(pending) = self
450            .inner
451            .pending_versioned_updates
452            .write()
453            .remove(&conn_id)
454        else {
455            return;
456        };
457
458        let store = self.inner.db.store();
459        for (collection, old_id, new_id, xid, previous_xmax) in pending {
460            if let Some(manager) = store.get_collection(&collection) {
461                if let Some(mut old) = manager.get(old_id) {
462                    if old.xmax == xid {
463                        old.set_xmax(previous_xmax);
464                        let _ = manager.update(old);
465                    }
466                }
467            }
468            let _ = store.delete_batch(&collection, &[new_id]);
469        }
470    }
471
472    /// Flush tombstones on COMMIT. The xmax stamp is already the durable
473    /// delete marker; commit only drops the rollback journal and emits
474    /// side effects. Physical reclamation is left for VACUUM so old
475    /// snapshots can still resolve the pre-delete row version.
476    pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
477        let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
478            return;
479        };
480        if pending.is_empty() {
481            return;
482        }
483
484        let store = self.inner.db.store();
485        for (collection, id, _xid, _previous_xmax) in pending {
486            store.context_index().remove_entity(id);
487            self.cdc_emit(
488                crate::replication::cdc::ChangeOperation::Delete,
489                &collection,
490                id.raw(),
491                "entity",
492            );
493        }
494    }
495
496    /// Revive tombstones on ROLLBACK — reset `xmax` to 0 so the tuples
497    /// become visible again to future snapshots. Best-effort: a row
498    /// already reclaimed by a concurrent VACUUM stays gone, but VACUUM
499    /// never reclaims tuples whose xmax is still referenced by any
500    /// active snapshot, so this case is only reachable via external
501    /// storage corruption.
502    pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
503        let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
504            return;
505        };
506
507        let store = self.inner.db.store();
508        for (collection, id, xid, previous_xmax) in pending {
509            let Some(manager) = store.get_collection(&collection) else {
510                continue;
511            };
512            if let Some(mut entity) = manager.get(id) {
513                if entity.xmax == xid {
514                    entity.set_xmax(previous_xmax);
515                    let _ = manager.update(entity);
516                }
517            }
518        }
519    }
520
521    /// Slice C of PRD #718 — accessor for the local wait registry.
522    pub fn queue_wait_registry(
523        &self,
524    ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
525        self.inner.queue_wait_registry.clone()
526    }
527
528    /// Buffer a `(scope, queue)` wake on the current connection so it
529    /// fires post-COMMIT, or notify immediately if no transaction is
530    /// open (autocommit path). The wait registry only ever observes
531    /// notifies for committed work — rollback drops the buffer.
532    pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
533        if self.current_xid().is_some() {
534            let conn_id = current_connection_id();
535            self.inner
536                .pending_queue_wakes
537                .write()
538                .entry(conn_id)
539                .or_default()
540                .push((scope.to_string(), queue.to_string()));
541            return;
542        }
543        self.inner.queue_wait_registry.notify(scope, queue);
544    }
545
546    pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
547        let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
548            return;
549        };
550        for (scope, queue) in pending {
551            self.inner.queue_wait_registry.notify(&scope, &queue);
552        }
553    }
554
555    pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
556        self.inner.pending_queue_wakes.write().remove(&conn_id);
557    }
558
559    pub(crate) fn release_pending_claim_locks(&self, conn_id: u64) {
560        self.inner
561            .pending_claim_locks
562            .write()
563            .retain(|_, owner| *owner != conn_id);
564    }
565
566    pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
567        let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
568            return;
569        };
570        for event in pending {
571            self.cdc_emit_kv(
572                event.op,
573                &event.collection,
574                &event.key,
575                0,
576                event.before,
577                event.after,
578            );
579        }
580    }
581
582    pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
583        self.inner.pending_kv_watch_events.write().remove(&conn_id);
584    }
585
586    /// Phase 1.1 MVCC universal: post-save hook that stamps `xmin` on a
587    /// freshly-inserted entity when the current connection holds an
588    /// open transaction. Used by graph / vector / queue / timeseries
589    /// write paths that go through the DevX builder API (`db.node(...)
590    /// .save()` and friends) — those live in the storage crate and
591    /// can't reach `current_xid()` without crossing layers, so the
592    /// application layer calls this helper right after `save()` to
593    /// finalise the MVCC stamp.
594    ///
595    /// Autocommit (outside BEGIN) is a no-op — no extra lookup or
596    /// write, so the non-transactional hot path stays untouched.
597    ///
598    /// Best-effort: if the collection or entity disappears between
599    /// the save and the stamp (concurrent DROP), we silently skip.
600    pub(crate) fn stamp_xmin_if_in_txn(
601        &self,
602        collection: &str,
603        id: crate::storage::unified::entity::EntityId,
604    ) {
605        let Some(xid) = self.current_xid() else {
606            return;
607        };
608        let store = self.inner.db.store();
609        let Some(manager) = store.get_collection(collection) else {
610            return;
611        };
612        if let Some(mut entity) = manager.get(id) {
613            entity.set_xmin(xid);
614            let _ = manager.update(entity);
615        }
616    }
617
618    /// Revive tombstones stamped by `stamper_xid` or any sub-xid
619    /// allocated after it (Phase 2.3.2e savepoint rollback). Any
620    /// pending entries with `xid < stamper_xid` stay queued because
621    /// they belong to the enclosing scope — they'll either flush on
622    /// COMMIT or revive on an outer ROLLBACK TO SAVEPOINT.
623    ///
624    /// Returns the number of tuples whose `xmax` was wiped back to 0.
625    pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
626        let mut guard = self.inner.pending_tombstones.write();
627        let Some(pending) = guard.get_mut(&conn_id) else {
628            return 0;
629        };
630
631        let store = self.inner.db.store();
632        let mut revived = 0usize;
633        pending.retain(|(collection, id, xid, previous_xmax)| {
634            if *xid < stamper_xid {
635                // Stamped before the savepoint — keep in queue.
636                return true;
637            }
638            if let Some(manager) = store.get_collection(collection) {
639                if let Some(mut entity) = manager.get(*id) {
640                    if entity.xmax == *xid {
641                        entity.set_xmax(*previous_xmax);
642                        let _ = manager.update(entity);
643                        revived += 1;
644                    }
645                }
646            }
647            false
648        });
649        if pending.is_empty() {
650            guard.remove(&conn_id);
651        }
652        revived
653    }
654
655    /// Return the snapshot the current connection should use for visibility
656    /// checks (Phase 2.3 PG parity).
657    ///
658    /// * If the connection is inside a READ COMMITTED transaction, capture
659    ///   a fresh statement snapshot while preserving the transaction's xid
660    ///   and write-set lifecycle.
661    /// * If the connection is inside a SNAPSHOT transaction, reuse the
662    ///   snapshot stored in its `TxnContext`.
663    /// * Otherwise (autocommit), capture a fresh snapshot tied to an
664    ///   implicit xid=0 — the read path treats pre-MVCC rows as always
665    ///   visible so this degrades to "see everything committed".
666    pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
667        let conn_id = current_connection_id();
668        if let Some(ctx) = self.inner.tx_contexts.read().get(&conn_id).cloned() {
669            if ctx.isolation == crate::storage::transaction::IsolationLevel::ReadCommitted {
670                let high_water = self.inner.snapshot_manager.peek_next_xid();
671                return self.inner.snapshot_manager.snapshot(high_water);
672            }
673            return ctx.snapshot;
674        }
675        // Autocommit: take a fresh snapshot bounded by `peek_next_xid` so
676        // every already-committed xid (which is strictly less) passes the
677        // `xmin <= snap.xid` gate, while concurrently-active xids land in
678        // the `in_progress` set and stay hidden until they commit. Using
679        // xid=0 would incorrectly hide every MVCC-stamped tuple.
680        let high_water = self.inner.snapshot_manager.peek_next_xid();
681        self.inner.snapshot_manager.snapshot(high_water)
682    }
683
684    /// Xid of the current connection's active transaction, or `None` when
685    /// running outside a BEGIN/COMMIT block. Write paths call this to
686    /// decide whether to stamp `xmin`/`xmax` on tuples.
687    /// Phase 2.3.2e: when a savepoint is open, `writer_xid` returns the
688    /// sub-xid so new writes can be selectively rolled back. Otherwise
689    /// the parent txn's xid is returned, matching pre-savepoint
690    /// behaviour. Callers that need the enclosing *transaction* xid
691    /// (e.g. VACUUM min-active calculations) should read `ctx.xid`
692    /// directly.
693    pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
694        let conn_id = current_connection_id();
695        self.inner
696            .tx_contexts
697            .read()
698            .get(&conn_id)
699            .map(|ctx| ctx.writer_xid())
700    }
701
702    /// `true` when the given connection id has an open `BEGIN`. Issue
703    /// #760 — `OpenStream` consults this to refuse output streams that
704    /// would otherwise collide with an interactive transaction (see
705    /// ADR 0029 "Transaction interaction"). HTTP requests pre-dating the
706    /// connection-id plumbing run with id `0`, which never carries a
707    /// transaction context, so this returns `false` on those paths.
708    pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
709        self.inner.tx_contexts.read().contains_key(&conn_id)
710    }
711
712    /// Access the shared `SnapshotManager` — useful for VACUUM to compute
713    /// the oldest-active xid when reclaiming dead tuples.
714    pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
715        Arc::clone(&self.inner.snapshot_manager)
716    }
717
718    pub(crate) fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
719        let manager = &self.inner.snapshot_manager;
720        let next_xid = manager.peek_next_xid();
721        let mut cutoff = next_xid;
722        if let Some(oldest_active) = manager.oldest_active_xid() {
723            cutoff = cutoff.min(oldest_active);
724        }
725        if let Some(oldest_pinned) = manager.oldest_pinned_xid() {
726            cutoff = cutoff.min(oldest_pinned);
727        }
728        let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
729        if retention_xids > 0 {
730            cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
731        }
732        cutoff
733    }
734
735    /// Own-tx xids (parent + open/released savepoints) for the current
736    /// connection. Transports + tests that build a `SnapshotContext`
737    /// manually (outside the `execute_query` scope) need this set so
738    /// the writer's own uncommitted tuples stay visible to self.
739    pub fn current_txn_own_xids(
740        &self,
741    ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
742        let mut set = std::collections::HashSet::new();
743        if let Some(ctx) = self.inner.tx_contexts.read().get(&current_connection_id()) {
744            set.insert(ctx.xid);
745            for (_, sub) in &ctx.savepoints {
746                set.insert(*sub);
747            }
748            for sub in &ctx.released_sub_xids {
749                set.insert(*sub);
750            }
751        }
752        set
753    }
754}