1use super::execution_context::current_connection_id;
22use super::*;
23
24thread_local! {
25 static DEFERRED_UPDATES: std::cell::RefCell<Option<Vec<DeferredUpdate>>> = const { std::cell::RefCell::new(None) };
26 static GRAPH_INSERT_WRITER: std::cell::Cell<Option<(usize, u64, u64)>> = const { std::cell::Cell::new(None) };
27 static DML_CAPTURE_OWNER: std::cell::Cell<Option<(usize, u64, bool)>> = const { std::cell::Cell::new(None) };
28 static DML_CAPTURE_GRAPH_WRITER: std::cell::RefCell<Option<GraphInsertWriter>> = const { std::cell::RefCell::new(None) };
29}
30
31struct GraphInsertWriter {
32 manager: std::sync::Arc<crate::storage::transaction::snapshot::SnapshotManager>,
33 xid: u64,
34 finished: bool,
35}
36
37impl GraphInsertWriter {
38 fn begin(runtime: &RedDBRuntime) -> Self {
39 let manager = runtime.snapshot_manager();
40 let xid = manager.begin();
41 let identity = std::sync::Arc::as_ptr(&runtime.inner) as usize;
42 GRAPH_INSERT_WRITER.with(|slot| {
43 assert!(slot
44 .replace(Some((identity, current_connection_id(), xid)))
45 .is_none());
46 });
47 Self {
48 manager,
49 xid,
50 finished: false,
51 }
52 }
53
54 fn commit(&mut self) {
55 self.manager.commit(self.xid);
56 self.finished = true;
57 }
58}
59
60impl Drop for GraphInsertWriter {
61 fn drop(&mut self) {
62 GRAPH_INSERT_WRITER.with(|slot| slot.set(None));
63 if !self.finished {
64 self.manager.rollback(self.xid);
65 }
66 }
67}
68
69struct DeferredUpdate {
70 runtime: RedDBRuntime,
71 collection: String,
72 old_id: EntityId,
73 new_id: EntityId,
74 previous_xmax: u64,
75 update: Arc<crate::application::entity::AutocommitUpdate>,
76 committed: bool,
77}
78
79impl Drop for DeferredUpdate {
80 fn drop(&mut self) {
81 if !self.committed {
82 let topology = self
83 .runtime
84 .index_store_ref()
85 .collection_topology_lock(&self.collection);
86 if let Err(error) = self.runtime.revive_versioned_update(
87 &self.collection,
88 self.old_id,
89 self.new_id,
90 self.update.xid,
91 self.previous_xmax,
92 topology.read_recursive(),
93 ) {
94 tracing::error!(%error, "autocommit update rollback failed");
95 }
96 }
97 }
98}
99
100struct DeferredUpdates;
101
102impl DeferredUpdates {
103 fn begin() -> Self {
104 DEFERRED_UPDATES.with(|slot| {
105 assert!(slot.borrow().is_none(), "update WAL scopes cannot nest");
106 *slot.borrow_mut() = Some(Vec::new());
107 });
108 Self
109 }
110
111 fn commit(self) {
112 DEFERRED_UPDATES.with(|slot| {
113 for mut pending in slot.borrow_mut().take().expect("update WAL scope") {
114 let update = &pending.update;
115 update.commit();
116 pending.committed = true;
117 }
118 });
119 }
120
121 fn prepare(&self) -> RedDBResult<()> {
122 DEFERRED_UPDATES.with(|slot| {
123 for pending in slot.borrow().as_ref().expect("update WAL scope") {
124 let store = pending.runtime.db().store();
125 let Some(manager) = store.get_collection(&pending.collection) else {
126 return Err(RedDBError::Internal(
127 "updated collection disappeared".into(),
128 ));
129 };
130 let epoch = manager
131 .try_get(pending.new_id, usize::MAX)?
132 .and_then(|entity| entity.branch_version().map(|version| version.origin));
133 let old_owned = manager
134 .try_get(pending.old_id, usize::MAX)
135 .map_err(|error| RedDBError::Internal(error.to_string()))?
136 .is_some_and(|old| {
137 epoch.map_or(old.xmax, |epoch| old.branch_xmax(epoch)) == pending.update.xid
138 });
139 let new_owned = manager
140 .try_get(pending.new_id, usize::MAX)
141 .map_err(|error| RedDBError::Internal(error.to_string()))?
142 .is_some_and(|new| new.xmin == pending.update.xid);
143 if !old_owned || !new_owned {
144 return Err(RedDBRuntime::conflict_error(
145 &pending.collection,
146 pending.old_id,
147 pending.update.xid,
148 ));
149 }
150 }
151 Ok(())
152 })
153 }
154}
155
156impl Drop for DeferredUpdates {
157 fn drop(&mut self) {
158 let pending = DEFERRED_UPDATES.with(|slot| slot.borrow_mut().take());
159 if let Some(pending) = pending {
160 for update in pending.into_iter().rev() {
161 drop(update);
162 }
163 }
164 }
165}
166
167pub(crate) fn autocommit_update_capture_available() -> bool {
168 DEFERRED_UPDATES.with(|slot| slot.borrow().is_some())
169}
170
171pub(crate) fn publish_autocommit_update(
172 runtime: &RedDBRuntime,
173 item: &crate::application::entity::AppliedEntityMutation,
174 update: &Arc<crate::application::entity::AutocommitUpdate>,
175) {
176 if UnifiedStore::deferred_store_wal_capture_active() {
177 DEFERRED_UPDATES.with(|slot| {
178 slot.borrow_mut()
179 .as_mut()
180 .expect("update WAL scope")
181 .push(DeferredUpdate {
182 runtime: runtime.clone(),
183 collection: item.collection.clone(),
184 old_id: item.replaced_entity.as_ref().expect("versioned update").id,
185 new_id: item.entity.id,
186 previous_xmax: item.replaced_entity_previous_xmax,
187 update: Arc::clone(update),
188 committed: false,
189 });
190 });
191 } else {
192 update.commit();
193 }
194}
195
196struct DmlWalCapture(bool);
198
199struct CapturedDmlWal {
200 actions: crate::storage::unified::DeferredStoreWalActions,
201 graph_writer: Option<GraphInsertWriter>,
202}
203
204impl DmlWalCapture {
205 fn begin(runtime: &RedDBRuntime, transactional: bool) -> Self {
206 assert!(
207 !UnifiedStore::deferred_store_wal_capture_active(),
208 "DML WAL captures cannot nest"
209 );
210 let identity = std::sync::Arc::as_ptr(&runtime.inner) as usize;
211 DML_CAPTURE_OWNER.with(|slot| {
212 assert!(slot
213 .replace(Some((identity, current_connection_id(), transactional)))
214 .is_none());
215 });
216 UnifiedStore::begin_deferred_store_wal_capture();
217 Self(true)
218 }
219
220 fn finish(mut self) -> CapturedDmlWal {
221 self.0 = false;
222 DML_CAPTURE_OWNER.with(|slot| slot.set(None));
223 CapturedDmlWal {
224 actions: UnifiedStore::take_deferred_store_wal_capture(),
225 graph_writer: DML_CAPTURE_GRAPH_WRITER.with(|slot| slot.borrow_mut().take()),
226 }
227 }
228}
229
230impl Drop for DmlWalCapture {
231 fn drop(&mut self) {
232 if self.0 {
233 DML_CAPTURE_OWNER.with(|slot| slot.set(None));
234 let _ = UnifiedStore::take_deferred_store_wal_capture();
235 DML_CAPTURE_GRAPH_WRITER.with(|slot| drop(slot.borrow_mut().take()));
236 }
237 }
238}
239
240impl RedDBRuntime {
241 pub(super) fn vcs_state_write_snapshot(
244 &self,
245 collection: &str,
246 ) -> RedDBResult<(
247 super::execution_context::SnapshotContext,
248 crate::storage::transaction::snapshot::SnapshotRetentionGuard,
249 )> {
250 self.vcs_writer_epoch(collection)?;
251 let (mut context, retention) = self.native_read_snapshot()?;
252 if self.current_xid().is_none() {
253 context.snapshot = self.snapshot_manager().fresh_read_snapshot();
254 context.branch = self.vcs_branch_read_view(&context.snapshot, None)?;
255 }
256 Ok((context, retention))
257 }
258
259 pub(crate) fn insert_vcs_state_version(
262 &self,
263 collection: &str,
264 mut entity: UnifiedEntity,
265 previous: Option<EntityId>,
266 ) -> RedDBResult<EntityId> {
267 let store = self.db().store();
268 let epoch = self.vcs_writer_epoch(collection)?.ok_or_else(|| {
269 RedDBError::InvalidOperation("state version requires a versioned collection".into())
270 })?;
271 let xid = self
272 .current_xid()
273 .or_else(|| self.graph_insert_xid())
274 .ok_or_else(|| {
275 RedDBError::InvalidOperation("state version requires a WAL writer".into())
276 })?;
277 entity.id = store.next_entity_id();
278 entity.set_xmin(xid);
279 entity.set_xmax(0);
280 entity.clear_branch_version();
281 entity.set_branch_origin(epoch);
282 let id = entity.id;
283 if let Some(previous) = previous {
284 let mut old = store
285 .try_get(collection, previous, usize::MAX)?
286 .ok_or_else(|| RedDBError::NotFound(previous.to_string()))?;
287 entity.set_logical_id(old.logical_id());
288 if let (
289 crate::storage::EntityKind::TableRow {
290 row_id: destination,
291 ..
292 },
293 crate::storage::EntityKind::TableRow { row_id: source, .. },
294 ) = (&mut entity.kind, &old.kind)
295 {
296 *destination = *source;
297 }
298 let previous_xmax = old.branch_xmax(epoch);
299 old.set_branch_xmax(epoch, xid);
300 store.install_versioned_table_row_update(collection, old, entity, None)?;
301 if self.current_xid().is_some() {
302 self.record_pending_versioned_update(
303 current_connection_id(),
304 collection,
305 previous,
306 id,
307 xid,
308 previous_xmax,
309 );
310 }
311 } else {
312 store.insert_auto(collection, entity)?;
313 }
314 Ok(id)
315 }
316
317 pub(crate) fn graph_insert_xid(&self) -> Option<u64> {
318 let identity = std::sync::Arc::as_ptr(&self.inner) as usize;
319 GRAPH_INSERT_WRITER.with(|slot| {
320 slot.get().and_then(|(owner, conn, xid)| {
321 (owner == identity && conn == current_connection_id()).then_some(xid)
322 })
323 })
324 }
325
326 pub(crate) fn with_versioned_graph_insert<T>(
328 &self,
329 f: impl FnOnce() -> RedDBResult<T>,
330 ) -> RedDBResult<T> {
331 let identity = std::sync::Arc::as_ptr(&self.inner) as usize;
332 let connection_id = current_connection_id();
333 if self.current_xid().is_some() {
334 if UnifiedStore::deferred_store_wal_capture_active() {
335 if DML_CAPTURE_OWNER.with(|slot| slot.get())
336 == Some((identity, connection_id, true))
337 {
338 return f();
339 }
340 return Err(RedDBError::InvalidOperation(
341 "versioned entity insertion cannot join an unrelated WAL capture".to_string(),
342 ));
343 }
344 return self.with_deferred_store_wal_if_transaction(f);
345 }
346 if self.graph_insert_xid().is_some() {
347 return f();
348 }
349 if UnifiedStore::deferred_store_wal_capture_active() {
350 if DML_CAPTURE_OWNER.with(|slot| slot.get()) != Some((identity, connection_id, false))
351 || GRAPH_INSERT_WRITER.with(|slot| slot.get().is_some())
352 {
353 return Err(RedDBError::InvalidOperation(
354 "versioned entity insertion cannot join an unrelated WAL capture".to_string(),
355 ));
356 }
357 let writer = GraphInsertWriter::begin(self);
358 let result = f();
359 DML_CAPTURE_GRAPH_WRITER.with(|slot| {
360 assert!(slot.borrow_mut().replace(writer).is_none());
361 });
362 return result;
363 }
364 self.drain_committed_writes_under_pressure(0)?;
365 let deletions = super::delete_publication::DeletePublicationScope::begin();
366 let updates = DeferredUpdates::begin();
367 let mut writer = GraphInsertWriter::begin(self);
368 let capture = DmlWalCapture::begin(self, false);
369 let result = f();
370 let CapturedDmlWal {
371 actions,
372 mut graph_writer,
373 } = capture.finish();
374 if result.is_err() {
375 self.flush_deferred_cache_invalidation();
376 return result;
377 }
378 let published = deletions.publish(self, || {
379 updates.prepare()?;
380 self.inner
381 .db
382 .store()
383 .append_deferred_store_wal_actions(actions)
384 .map_err(|err| RedDBError::Internal(err.to_string()))?;
385 if let Some(writer) = &mut graph_writer {
386 writer.commit();
387 }
388 writer.commit();
389 updates.commit();
390 Ok(())
391 });
392 self.flush_deferred_cache_invalidation();
393 published?;
394 result
395 }
396
397 pub(crate) fn with_graph_insert_for_dml<T>(
398 &self,
399 query: &InsertQuery,
400 f: impl FnOnce() -> RedDBResult<T>,
401 ) -> RedDBResult<T> {
402 if matches!(
403 query.entity_type,
404 InsertEntityType::Node
405 | InsertEntityType::Edge
406 | InsertEntityType::Vector
407 | InsertEntityType::Document
408 ) && (self.vcs_is_versioned(&query.table)? || self.has_graph_contract(&query.table))
409 {
410 self.with_versioned_graph_insert(f)
411 } else {
412 self.with_deferred_store_wal_for_dml(self.insert_may_emit_events(query), f)
413 }
414 }
415
416 pub(crate) fn rollback_connection(
417 &self,
418 connection_id: u64,
419 ) -> RedDBResult<Option<crate::storage::transaction::snapshot::TxnContext>> {
420 let undo = self.revive_pending_versioned_updates(connection_id);
424 self.revive_pending_tombstones(connection_id);
425 let context = self.inner.transaction_state.rollback(connection_id);
426 self.discard_pending_queue_dedup(connection_id);
427 self.discard_pending_kv_watch_events(connection_id);
428 self.discard_pending_queue_wakes(connection_id);
429 self.discard_pending_store_wal_actions(connection_id);
430 self.release_pending_claim_locks(connection_id);
431 self.discard_pending_vault_writes(connection_id)?;
432 undo?;
433 Ok(context)
434 }
435
436 pub(crate) fn record_pending_tombstone(
442 &self,
443 conn_id: u64,
444 collection: &str,
445 id: crate::storage::unified::entity::EntityId,
446 stamper_xid: crate::storage::transaction::snapshot::Xid,
447 previous_xmax: crate::storage::transaction::snapshot::Xid,
448 branch_epoch: Option<u64>,
449 ) {
450 self.inner
451 .pending_tombstones
452 .write()
453 .entry(conn_id)
454 .or_default()
455 .push((
456 collection.to_string(),
457 id,
458 stamper_xid,
459 previous_xmax,
460 branch_epoch,
461 ));
462 }
463
464 pub(crate) fn record_pending_versioned_update(
465 &self,
466 conn_id: u64,
467 collection: &str,
468 old_id: crate::storage::unified::entity::EntityId,
469 new_id: crate::storage::unified::entity::EntityId,
470 stamper_xid: crate::storage::transaction::snapshot::Xid,
471 previous_xmax: crate::storage::transaction::snapshot::Xid,
472 ) {
473 self.inner
474 .pending_versioned_updates
475 .write()
476 .entry(conn_id)
477 .or_default()
478 .push((
479 collection.to_string(),
480 old_id,
481 new_id,
482 stamper_xid,
483 previous_xmax,
484 ));
485 }
486
487 pub(crate) fn record_pending_queue_dedup(
488 &self,
489 conn_id: u64,
490 queue: &str,
491 dedup_key: &str,
492 metadata_id: crate::storage::unified::entity::EntityId,
493 ) {
494 self.inner
495 .pending_queue_dedup
496 .write()
497 .entry(conn_id)
498 .or_default()
499 .push((queue.to_string(), dedup_key.to_string(), metadata_id));
500 }
501
502 pub(crate) fn with_deferred_store_wal_if_transaction<T>(
503 &self,
504 f: impl FnOnce() -> RedDBResult<T>,
505 ) -> RedDBResult<T> {
506 let conn_id = current_connection_id();
507 if !self.inner.transaction_state.in_transaction(conn_id) {
508 return f();
509 }
510
511 let logical_start = self
512 .inner
513 .pending_logical_records
514 .lock()
515 .get(&conn_id)
516 .map_or(0, |pending| pending.len());
517 let capture = DmlWalCapture::begin(self, true);
518 let result = f();
519 let CapturedDmlWal {
520 actions,
521 graph_writer,
522 } = capture.finish();
523 if graph_writer.is_some() {
524 return Err(RedDBError::InvalidOperation(
525 "autocommit graph writer cannot join a transaction WAL capture".to_string(),
526 ));
527 }
528 self.flush_deferred_cache_invalidation();
529 match result {
530 Ok(value) => {
531 self.record_pending_store_wal_actions(conn_id, actions);
532 Ok(value)
533 }
534 Err(err) => {
535 if let Some(records) = self.inner.pending_logical_records.lock().get_mut(&conn_id) {
536 records.truncate(logical_start);
537 }
538 Err(err)
539 }
540 }
541 }
542
543 pub(crate) fn with_deferred_store_wal_for_dml<T>(
544 &self,
545 capture_autocommit_events: bool,
546 f: impl FnOnce() -> RedDBResult<T>,
547 ) -> RedDBResult<T> {
548 let conn_id = current_connection_id();
549 if self.inner.transaction_state.in_transaction(conn_id) {
550 return self.with_deferred_store_wal_if_transaction(f);
551 }
552 self.drain_committed_writes_under_pressure(0)?;
553 if !capture_autocommit_events {
554 return f();
555 }
556
557 let deletions = super::delete_publication::DeletePublicationScope::begin();
558 let updates = DeferredUpdates::begin();
559 let capture = DmlWalCapture::begin(self, false);
560 let result = f();
561 let CapturedDmlWal {
562 actions,
563 mut graph_writer,
564 } = capture.finish();
565 if result.is_err() && (deletions.has_pending() || graph_writer.is_some()) {
566 self.flush_deferred_cache_invalidation();
567 return result;
568 }
569 let published = deletions.publish(self, || {
570 updates.prepare()?;
571 self.inner
572 .db
573 .store()
574 .append_deferred_store_wal_actions(actions)
575 .map_err(|err| RedDBError::Internal(err.to_string()))?;
576 if let Some(writer) = &mut graph_writer {
577 writer.commit();
578 }
579 updates.commit();
580 Ok(())
581 });
582 self.flush_deferred_cache_invalidation();
583 published?;
584 result
585 }
586
587 pub(crate) fn insert_may_emit_events(&self, query: &InsertQuery) -> bool {
588 !query.suppress_events
589 && self.collection_has_event_subscriptions_for_operation(
590 &query.table,
591 crate::catalog::SubscriptionOperation::Insert,
592 )
593 }
594
595 pub(crate) fn update_may_emit_events(&self, query: &UpdateQuery) -> bool {
596 !query.suppress_events
597 && self.collection_has_event_subscriptions_for_operation(
598 &query.table,
599 crate::catalog::SubscriptionOperation::Update,
600 )
601 }
602
603 pub(crate) fn delete_may_emit_events(&self, query: &DeleteQuery) -> bool {
604 !query.suppress_events
605 && self.collection_has_event_subscriptions_for_operation(
606 &query.table,
607 crate::catalog::SubscriptionOperation::Delete,
608 )
609 }
610
611 fn collection_has_event_subscriptions_for_operation(
612 &self,
613 collection: &str,
614 operation: crate::catalog::SubscriptionOperation,
615 ) -> bool {
616 let Some(contract) = self.db().collection_contract_arc(collection) else {
617 return false;
618 };
619 contract.subscriptions.iter().any(|subscription| {
620 subscription.enabled
621 && (subscription.ops_filter.is_empty()
622 || subscription.ops_filter.contains(&operation))
623 })
624 }
625
626 pub(crate) fn record_pending_store_wal_actions(
627 &self,
628 conn_id: u64,
629 actions: crate::storage::unified::DeferredStoreWalActions,
630 ) {
631 if actions.is_empty() {
632 return;
633 }
634 let mut guard = self.inner.pending_store_wal_actions.write();
635 guard
636 .entry(conn_id)
637 .or_default()
638 .push((self.current_xid().unwrap_or(0), actions));
639 }
640
641 pub(crate) fn flush_pending_store_wal_actions(&self, conn_id: u64) -> RedDBResult<()> {
642 let groups = self
643 .inner
644 .pending_store_wal_actions
645 .write()
646 .remove(&conn_id)
647 .unwrap_or_default();
648 let mut actions = crate::storage::unified::DeferredStoreWalActions::default();
649 for (xid, group) in groups {
650 if !self.snapshot_manager().is_aborted(xid) {
651 actions.extend(group);
652 }
653 }
654 self.commit_transactional_replication(conn_id, actions)
655 }
656
657 pub(crate) fn discard_pending_store_wal_actions(&self, conn_id: u64) {
658 self.inner.pending_commit_policies.lock().remove(&conn_id);
659 self.inner.pending_logical_records.lock().remove(&conn_id);
660 self.inner
661 .pending_store_wal_actions
662 .write()
663 .remove(&conn_id);
664 }
665
666 fn xid_conflicts_with_snapshot(
667 &self,
668 xid: crate::storage::transaction::snapshot::Xid,
669 snapshot: &crate::storage::transaction::snapshot::Snapshot,
670 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
671 ) -> bool {
672 xid != 0
673 && !own_xids.contains(&xid)
674 && !self
675 .inner
676 .transaction_state
677 .snapshot_manager()
678 .is_aborted(xid)
679 && !self
680 .inner
681 .transaction_state
682 .snapshot_manager()
683 .is_active(xid)
684 && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
685 }
686
687 pub(super) fn conflict_error(
688 collection: &str,
689 logical_id: crate::storage::unified::entity::EntityId,
690 xid: crate::storage::transaction::snapshot::Xid,
691 ) -> RedDBError {
692 RedDBError::Query(format!(
693 "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
694 logical_id.raw()
695 ))
696 }
697
698 fn check_logical_row_conflict(
699 &self,
700 collection: &str,
701 logical_id: crate::storage::unified::entity::EntityId,
702 excluded_ids: &[crate::storage::unified::entity::EntityId],
703 snapshot: &crate::storage::transaction::snapshot::Snapshot,
704 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
705 branch_epoch: Option<u64>,
706 ) -> RedDBResult<()> {
707 let store = self.inner.db.store();
708 let Some(manager) = store.get_collection(collection) else {
709 return Ok(());
710 };
711
712 for candidate in manager.query_all(|_| true) {
713 if let Some(epoch) = branch_epoch {
714 if candidate
715 .branch_version()
716 .is_some_and(|version| version.origin != epoch)
717 {
718 continue;
719 }
720 }
721 if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
722 continue;
723 }
724 if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
725 return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
726 }
727 if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
728 return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
729 }
730 }
731 Ok(())
732 }
733
734 pub(crate) fn check_table_row_write_conflicts(
735 &self,
736 conn_id: u64,
737 snapshot: &crate::storage::transaction::snapshot::Snapshot,
738 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
739 isolation: crate::storage::transaction::IsolationLevel,
740 ) -> RedDBResult<()> {
741 let versioned_updates = self
742 .inner
743 .pending_versioned_updates
744 .read()
745 .get(&conn_id)
746 .cloned()
747 .unwrap_or_default();
748 let tombstones = self
749 .inner
750 .pending_tombstones
751 .read()
752 .get(&conn_id)
753 .cloned()
754 .unwrap_or_default();
755
756 let mut serializable_write_set = std::collections::HashSet::new();
757 let store = self.inner.db.store();
758 for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
759 let Some(manager) = store.get_collection(&collection) else {
760 continue;
761 };
762 let Some(old) = manager.get(old_id) else {
763 continue;
764 };
765 let logical_id = old.logical_id();
766 let epoch = manager
767 .get(new_id)
768 .and_then(|entity| entity.branch_version().map(|version| version.origin));
769 let old_xmax = epoch.map_or(old.xmax, |epoch| old.branch_xmax(epoch));
770 serializable_write_set.insert((collection.clone(), logical_id));
771 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
772 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
773 }
774 if old_xmax != xid && self.xid_conflicts_with_snapshot(old_xmax, snapshot, own_xids) {
775 return Err(Self::conflict_error(&collection, logical_id, old_xmax));
776 }
777 self.check_logical_row_conflict(
778 &collection,
779 logical_id,
780 &[old_id, new_id],
781 snapshot,
782 own_xids,
783 epoch,
784 )?;
785 }
786
787 for (collection, id, xid, previous_xmax, epoch) in tombstones {
788 let Some(manager) = store.get_collection(&collection) else {
789 continue;
790 };
791 let Some(entity) = manager.get(id) else {
792 continue;
793 };
794 let logical_id = entity.logical_id();
795 let xmax = epoch.map_or(entity.xmax, |epoch| entity.branch_xmax(epoch));
796 serializable_write_set.insert((collection.clone(), logical_id));
797 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
798 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
799 }
800 if xmax != xid && self.xid_conflicts_with_snapshot(xmax, snapshot, own_xids) {
801 return Err(Self::conflict_error(&collection, logical_id, xmax));
802 }
803 self.check_logical_row_conflict(
804 &collection,
805 logical_id,
806 &[id],
807 snapshot,
808 own_xids,
809 epoch,
810 )?;
811 }
812
813 if isolation == crate::storage::transaction::IsolationLevel::Serializable
814 && self
815 .inner
816 .transaction_state
817 .snapshot_manager()
818 .serializable_commit_would_be_dangerous(
819 *own_xids.iter().min().unwrap_or(&0),
820 &serializable_write_set,
821 )
822 {
823 return Err(RedDBError::Query(
824 "serialization conflict: serializable transaction would complete rw-antidependency dangerous structure".to_string(),
825 ));
826 }
827
828 Ok(())
829 }
830
831 pub(crate) fn check_queue_dedup_write_conflicts(
832 &self,
833 conn_id: u64,
834 snapshot: &crate::storage::transaction::snapshot::Snapshot,
835 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
836 ) -> RedDBResult<()> {
837 let pending = self
838 .inner
839 .pending_queue_dedup
840 .read()
841 .get(&conn_id)
842 .cloned()
843 .unwrap_or_default();
844 if pending.is_empty() {
845 return Ok(());
846 }
847
848 let store = self.inner.db.store();
849 let Some(manager) = store.get_collection(crate::runtime::impl_queue::QUEUE_META_COLLECTION)
850 else {
851 return Ok(());
852 };
853
854 for (queue, dedup_key, metadata_id) in pending {
855 for candidate in manager.query_all(|entity| {
856 entity.data.as_row().is_some_and(|row| {
857 crate::runtime::impl_queue::row_text(row, "kind").as_deref()
858 == Some(crate::runtime::impl_queue::KIND_QUEUE_PUSH_DEDUP)
859 && crate::runtime::impl_queue::row_text(row, "queue").as_deref()
860 == Some(&queue)
861 && crate::runtime::impl_queue::row_text(row, "dedup_key").as_deref()
862 == Some(&dedup_key)
863 })
864 }) {
865 if candidate.id == metadata_id
866 || self
867 .inner
868 .transaction_state
869 .snapshot_manager()
870 .is_aborted(candidate.xmin)
871 {
872 continue;
873 }
874 if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
875 return Err(RedDBError::Query(format!(
876 "serialization conflict: queue dedup key {queue}/{dedup_key} was modified by concurrent transaction {}",
877 candidate.xmin
878 )));
879 }
880 }
881 }
882 Ok(())
883 }
884
885 pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
886 let versioned_updates = self
887 .inner
888 .pending_versioned_updates
889 .read()
890 .get(&conn_id)
891 .cloned()
892 .unwrap_or_default();
893 let tombstones = self
894 .inner
895 .pending_tombstones
896 .read()
897 .get(&conn_id)
898 .cloned()
899 .unwrap_or_default();
900
901 let store = self.inner.db.store();
902 let _publication = store.vault_publication_lock.lock();
903 for (collection, old_id, new_id, xid, _previous_xmax) in versioned_updates {
904 if let Some(manager) = store.get_collection(&collection) {
905 let epoch = manager
906 .get(new_id)
907 .and_then(|entity| entity.branch_version().map(|version| version.origin));
908 if let Some(mut entity) = manager.get(old_id) {
909 if let Some(epoch) = epoch {
910 entity.set_branch_xmax(epoch, xid);
911 } else {
912 entity.set_xmax(xid);
913 }
914 let _ = manager.update(entity);
915 }
916 }
917 }
918 for (collection, id, xid, _previous_xmax, epoch) in tombstones {
919 if let Some(manager) = store.get_collection(&collection) {
920 if let Some(mut entity) = manager.get(id) {
921 if let Some(epoch) = epoch {
922 entity.set_branch_xmax(epoch, xid);
923 } else {
924 entity.set_xmax(xid);
925 }
926 let _ = manager.update(entity);
927 }
928 }
929 }
930 }
931
932 pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
933 self.inner
934 .pending_versioned_updates
935 .write()
936 .remove(&conn_id);
937 }
938
939 pub(crate) fn finalize_pending_queue_dedup(&self, conn_id: u64) {
940 self.inner.pending_queue_dedup.write().remove(&conn_id);
941 }
942
943 pub(crate) fn discard_pending_queue_dedup(&self, conn_id: u64) {
944 let Some(pending) = self.inner.pending_queue_dedup.write().remove(&conn_id) else {
945 return;
946 };
947 let store = self.inner.db.store();
948 for (_queue, _dedup_key, metadata_id) in pending {
949 let _ = store.delete(
950 crate::runtime::impl_queue::QUEUE_META_COLLECTION,
951 metadata_id,
952 );
953 }
954 }
955
956 pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) -> RedDBResult<()> {
957 let Some(pending) = self
958 .inner
959 .pending_versioned_updates
960 .write()
961 .remove(&conn_id)
962 else {
963 return Ok(());
964 };
965 for (collection, old_id, new_id, xid, previous_xmax) in pending.into_iter().rev() {
967 let topology_lock = self.index_store_ref().collection_topology_lock(&collection);
968 #[cfg(test)]
969 self.index_store_ref().mutation_test_hook(
970 &collection,
971 super::index_store::MutationTestPhase::BeforeUndoTopology,
972 );
973 self.revive_versioned_update(
974 &collection,
975 old_id,
976 new_id,
977 xid,
978 previous_xmax,
979 topology_lock.read(),
980 )?;
981 }
982 Ok(())
983 }
984
985 pub(crate) fn revive_versioned_update(
986 &self,
987 collection: &str,
988 old_id: crate::storage::EntityId,
989 new_id: crate::storage::EntityId,
990 xid: u64,
991 previous_xmax: u64,
992 _topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
993 ) -> RedDBResult<()> {
994 let _indexed_mutation = self.index_store_ref().begin_indexed_mutation();
995 let store = self.inner.db.store();
996 let _publication = store.vault_publication_lock.lock();
997 let epoch = store
998 .get(collection, new_id)
999 .and_then(|entity| entity.branch_version().map(|version| version.origin));
1000 if let Some(new) = store.get(collection, new_id) {
1001 let fields = crate::application::ports::entity_row_fields_snapshot(&new);
1002 self.index_store_ref()
1003 .index_entity_delete(collection, new_id, &fields)
1004 .map_err(RedDBError::Internal)?;
1005 #[cfg(test)]
1006 self.index_store_ref().mutation_test_hook(
1007 collection,
1008 super::index_store::MutationTestPhase::UndoIndexRemoved,
1009 );
1010 }
1011 store
1012 .delete_batch(collection, &[new_id])
1013 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1014 if let Some(manager) = store.get_collection(collection) {
1015 if let Some(mut old) = manager.get(old_id) {
1016 if epoch.map_or(old.xmax, |epoch| old.branch_xmax(epoch)) == xid {
1017 if let Some(epoch) = epoch {
1018 old.set_branch_xmax(epoch, previous_xmax);
1019 } else {
1020 old.set_xmax(previous_xmax);
1021 }
1022 manager
1023 .update(old.clone())
1024 .map_err(|error| RedDBError::Internal(error.to_string()))?;
1025 store.context_index().index_entity(collection, &old);
1026 let fields = crate::application::ports::entity_row_fields_snapshot(&old);
1027 self.index_store_ref()
1028 .index_entity_insert(collection, old_id, &fields)
1029 .map_err(RedDBError::Internal)?;
1030 }
1031 }
1032 }
1033 Ok(())
1034 }
1035
1036 pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
1041 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
1042 return;
1043 };
1044 if pending.is_empty() {
1045 return;
1046 }
1047
1048 let store = self.inner.db.store();
1049 for (collection, id, _xid, _previous_xmax, _epoch) in pending {
1050 if let Some(manager) = store.get_collection(&collection) {
1051 manager.remove_metadata_expiry(id);
1052 }
1053 store.context_index().remove_entity(id);
1054 self.invalidate_result_cache_for_table(&collection);
1055 if self.inner.db.replication.is_none() {
1058 self.inner.cdc.emit(
1059 crate::replication::cdc::ChangeOperation::Delete,
1060 &collection,
1061 id.raw(),
1062 "entity",
1063 );
1064 }
1065 }
1066 }
1067
1068 pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
1075 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
1076 return;
1077 };
1078
1079 let store = self.inner.db.store();
1080 let _publication = store.vault_publication_lock.lock();
1081 for (collection, id, xid, previous_xmax, epoch) in pending {
1082 let Some(manager) = store.get_collection(&collection) else {
1083 continue;
1084 };
1085 if let Some(mut entity) = manager.get(id) {
1086 if epoch.map_or(entity.xmax, |epoch| entity.branch_xmax(epoch)) == xid {
1087 if let Some(epoch) = epoch {
1088 entity.set_branch_xmax(epoch, previous_xmax);
1089 } else {
1090 entity.set_xmax(previous_xmax);
1091 }
1092 let _ = manager.update(entity);
1093 }
1094 }
1095 }
1096 }
1097
1098 pub fn queue_wait_registry(
1100 &self,
1101 ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
1102 self.inner.queue_wait_registry.clone()
1103 }
1104
1105 pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
1110 if self.current_xid().is_some() {
1111 let conn_id = current_connection_id();
1112 self.inner
1113 .pending_queue_wakes
1114 .write()
1115 .entry(conn_id)
1116 .or_default()
1117 .push((scope.to_string(), queue.to_string()));
1118 return;
1119 }
1120 self.inner.queue_wait_registry.notify(scope, queue);
1121 }
1122
1123 pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
1124 let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
1125 return;
1126 };
1127 for (scope, queue) in pending {
1128 self.inner.queue_wait_registry.notify(&scope, &queue);
1129 }
1130 }
1131
1132 pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
1133 self.inner.pending_queue_wakes.write().remove(&conn_id);
1134 }
1135
1136 pub(crate) fn release_pending_claim_locks(&self, conn_id: u64) {
1137 self.inner
1138 .pending_claim_locks
1139 .write()
1140 .retain(|_, owner| *owner != conn_id);
1141 }
1142
1143 pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
1144 let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
1145 return;
1146 };
1147 for (_, event) in pending {
1148 self.cdc_emit_kv(
1149 event.op,
1150 &event.collection,
1151 &event.key,
1152 0,
1153 event.before,
1154 event.after,
1155 );
1156 }
1157 }
1158
1159 pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
1160 self.inner.pending_kv_watch_events.write().remove(&conn_id);
1161 }
1162
1163 pub(crate) fn stamp_xmin_if_in_txn(
1178 &self,
1179 collection: &str,
1180 id: crate::storage::unified::entity::EntityId,
1181 ) {
1182 let Some(xid) = self.current_xid() else {
1183 return;
1184 };
1185 let store = self.inner.db.store();
1186 let Some(manager) = store.get_collection(collection) else {
1187 return;
1188 };
1189 if let Some(mut entity) = manager.get(id) {
1190 entity.set_xmin(xid);
1191 let _ = manager.update(entity);
1192 }
1193 }
1194
1195 pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
1203 let store = self.inner.db.store();
1204 let _publication = store.vault_publication_lock.lock();
1205 let mut guard = self.inner.pending_tombstones.write();
1206 let Some(pending) = guard.get_mut(&conn_id) else {
1207 return 0;
1208 };
1209
1210 let mut revived = 0usize;
1211 pending.retain(|(collection, id, xid, previous_xmax, epoch)| {
1212 if *xid < stamper_xid {
1213 return true;
1215 }
1216 if let Some(manager) = store.get_collection(collection) {
1217 if let Some(mut entity) = manager.get(*id) {
1218 if epoch.map_or(entity.xmax, |epoch| entity.branch_xmax(epoch)) == *xid {
1219 if let Some(epoch) = epoch {
1220 entity.set_branch_xmax(*epoch, *previous_xmax);
1221 } else {
1222 entity.set_xmax(*previous_xmax);
1223 }
1224 let _ = manager.update(entity);
1225 revived += 1;
1226 }
1227 }
1228 }
1229 false
1230 });
1231 if pending.is_empty() {
1232 guard.remove(&conn_id);
1233 }
1234 revived
1235 }
1236
1237 pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
1249 self.inner
1250 .transaction_state
1251 .current_snapshot(current_connection_id())
1252 }
1253
1254 pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
1264 self.inner
1265 .transaction_state
1266 .writer_xid(current_connection_id())
1267 }
1268
1269 pub(crate) fn own_transaction_xids(&self) -> std::collections::HashSet<u64> {
1270 let mut xids = self
1271 .inner
1272 .transaction_state
1273 .own_xids(current_connection_id());
1274 if let Some(xid) = self.graph_insert_xid() {
1275 xids.insert(xid);
1276 }
1277 xids
1278 }
1279
1280 pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
1287 self.inner.transaction_state.in_transaction(conn_id)
1288 }
1289
1290 pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
1293 self.inner.transaction_state.snapshot_manager()
1294 }
1295
1296 pub(crate) fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
1297 let manager = self.inner.transaction_state.snapshot_manager();
1298 let next_xid = manager.peek_next_xid();
1299 let mut cutoff = next_xid;
1300 if let Some(oldest_required) = manager.oldest_required_xid() {
1301 cutoff = cutoff.min(oldest_required);
1302 }
1303 let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
1304 if retention_xids > 0 {
1305 cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
1306 }
1307 cutoff
1308 }
1309
1310 pub fn current_txn_own_xids(
1315 &self,
1316 ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
1317 self.inner
1318 .transaction_state
1319 .own_xids(current_connection_id())
1320 }
1321}