Skip to main content

reifydb_transaction/multi/transaction/
write.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use core::mem;
5use std::{
6	cmp::Ordering,
7	collections::BTreeSet,
8	iter,
9	ops::{Bound, RangeBounds},
10	sync::Arc,
11	vec,
12};
13
14use reifydb_codec::{
15	key::encoded::{EncodedKey, EncodedKeyRange},
16	row::bytes::EncodedBytes,
17};
18use reifydb_core::{
19	common::{ChangeVersion, CommitVersion, SourceVersion},
20	delta::{Delta, RemoveAnnounce},
21	event::transaction::PostCommitEvent,
22	interface::{
23		catalog::{object::ObjectId, storage::StorageId},
24		change::Change,
25		store::{MultiVersionBatch, MultiVersionContains, MultiVersionGet, MultiVersionRow},
26	},
27	key::{
28		any::TaggedKey,
29		bound::{TaggedKeyBound, TaggedKeyBoundRange, object_fields},
30		row::{PartitionedRowKey, RowKey, StoragePartitionedRowKey, StorageRowKey},
31		tag::KeyTag,
32	},
33};
34use reifydb_value::{
35	Result,
36	byte_size::ByteSize,
37	reifydb_assertions,
38	util::{cowvec::CowVec, hex::display as hex_display},
39};
40use tracing::{instrument, warn};
41
42use super::{MultiTransaction, version::StandardVersionProvider};
43use crate::{
44	TransactionId,
45	delta::optimize_deltas,
46	error::TransactionError,
47	multi::{
48		RangeScope,
49		conflict::ConflictManager,
50		lease::VersionLeaseGuard,
51		marker::Marker,
52		oracle::{CreateCommitResult, Oracle},
53		pending::PendingWrites,
54		types::{DeltaEntry, TransactionValue},
55	},
56};
57
58pub struct WriteSavepoint {
59	pub(crate) pending_writes: PendingWrites,
60	pub(crate) count: u64,
61	pub(crate) size: ByteSize,
62	pub(crate) duplicates: Vec<DeltaEntry>,
63	pub(crate) delta_log_len: usize,
64	pub(crate) conflicts: ConflictManager,
65	pub(crate) preexisting_keys: BTreeSet<TaggedKey>,
66}
67
68#[derive(Clone, Copy, PartialEq)]
69pub(crate) enum Lifecycle {
70	Active,
71	QueryDone,
72	Discarded,
73}
74
75pub struct MultiWriteTransaction {
76	engine: MultiTransaction,
77
78	pub(crate) id: TransactionId,
79	pub(crate) version: CommitVersion,
80	pub(crate) read_version: Option<CommitVersion>,
81	source: Option<SourceVersion>,
82	pub(crate) size: ByteSize,
83	pub(crate) count: u64,
84	pub(crate) oracle: Arc<Oracle<StandardVersionProvider>>,
85	pub(crate) conflicts: ConflictManager,
86	pub(crate) pending_writes: PendingWrites,
87	pub(crate) duplicates: Vec<DeltaEntry>,
88
89	pub(crate) delta_log: Vec<DeltaEntry>,
90
91	pub(crate) preexisting_keys: BTreeSet<TaggedKey>,
92
93	pub(crate) lifecycle: Lifecycle,
94
95	pub(crate) self_lease: Option<VersionLeaseGuard>,
96
97	pending_query_pin: Option<CommitVersion>,
98}
99
100impl MultiWriteTransaction {
101	#[instrument(name = "transaction::command::new", level = "debug", skip(engine))]
102	pub fn new(engine: MultiTransaction) -> Result<Self> {
103		let oracle = engine.tm.oracle().clone();
104		let version = oracle.query.register_in_flight_with(|| oracle.version())?;
105
106		let id = TransactionId::generate(oracle.metrics_clock(), oracle.rng());
107		Ok(Self {
108			engine,
109			id,
110			version,
111			read_version: None,
112			source: None,
113			size: ByteSize::ZERO,
114			count: 0,
115			oracle,
116			conflicts: ConflictManager::new(),
117			pending_writes: PendingWrites::new(),
118			duplicates: Vec::new(),
119			delta_log: Vec::new(),
120			preexisting_keys: BTreeSet::new(),
121			lifecycle: Lifecycle::Active,
122			self_lease: None,
123			pending_query_pin: None,
124		})
125	}
126
127	fn transition_to(&mut self, next: Lifecycle) {
128		reifydb_assertions! {
129			assert!(matches!(
130				(self.lifecycle, next),
131				(Lifecycle::Active, Lifecycle::QueryDone)
132					| (Lifecycle::Active, Lifecycle::Discarded)
133					| (Lifecycle::QueryDone, Lifecycle::Discarded)
134			));
135		}
136		self.lifecycle = next;
137	}
138}
139
140impl Drop for MultiWriteTransaction {
141	fn drop(&mut self) {
142		if self.lifecycle != Lifecycle::Discarded {
143			self.discard();
144		}
145	}
146}
147
148impl MultiWriteTransaction {
149	pub fn id(&self) -> TransactionId {
150		self.id
151	}
152
153	pub fn version(&self) -> CommitVersion {
154		self.read_version.unwrap_or(self.version)
155	}
156
157	pub fn base_version(&self) -> CommitVersion {
158		self.version
159	}
160
161	pub fn stamp_source(&mut self, source: SourceVersion) {
162		self.source = Some(source);
163	}
164
165	pub fn read_as_of_version_exclusive(&mut self, version: CommitVersion) {
166		self.read_version = Some(CommitVersion(version.0.saturating_sub(1)));
167	}
168
169	pub fn read_as_of_version_inclusive(&mut self, version: CommitVersion) -> Result<()> {
170		self.read_as_of_version_exclusive(CommitVersion(version.0 + 1));
171		Ok(())
172	}
173
174	pub fn pending_writes(&self) -> &PendingWrites {
175		&self.pending_writes
176	}
177
178	pub fn conflicts(&self) -> &ConflictManager {
179		&self.conflicts
180	}
181
182	pub fn mark_preexisting<K: Into<TaggedKey> + Clone>(&mut self, key: &K) {
183		self.preexisting_keys.insert(key.clone().into());
184	}
185
186	pub fn preexisting_keys(&self) -> &BTreeSet<TaggedKey> {
187		&self.preexisting_keys
188	}
189}
190
191impl MultiWriteTransaction {
192	pub fn savepoint(&self) -> WriteSavepoint {
193		WriteSavepoint {
194			pending_writes: self.pending_writes.clone(),
195			count: self.count,
196			size: self.size,
197			duplicates: self.duplicates.clone(),
198			delta_log_len: self.delta_log.len(),
199			conflicts: self.conflicts.clone(),
200			preexisting_keys: self.preexisting_keys.clone(),
201		}
202	}
203
204	pub fn restore_savepoint(&mut self, sp: WriteSavepoint) {
205		self.pending_writes = sp.pending_writes;
206		self.count = sp.count;
207		self.size = sp.size;
208		self.duplicates = sp.duplicates;
209		self.delta_log.truncate(sp.delta_log_len);
210		self.conflicts = sp.conflicts;
211		self.preexisting_keys = sp.preexisting_keys;
212	}
213}
214
215impl MultiWriteTransaction {
216	pub fn marker(&mut self) -> Marker<'_> {
217		Marker::new(&mut self.conflicts)
218	}
219
220	pub fn marker_with_pending_writes(&mut self) -> (Marker<'_>, &PendingWrites) {
221		(Marker::new(&mut self.conflicts), &self.pending_writes)
222	}
223
224	pub fn mark_read(&mut self, k: &TaggedKey) {
225		self.conflicts.mark_read(k);
226	}
227
228	pub fn mark_write(&mut self, k: &TaggedKey) {
229		self.conflicts.mark_write(k);
230	}
231
232	pub fn reserve_writes(&mut self, additional: usize) {
233		self.conflicts.reserve_writes(additional);
234	}
235
236	pub(crate) fn disable_conflict_tracking(&mut self) {
237		self.conflicts.set_disabled();
238	}
239}
240
241impl MultiWriteTransaction {
242	#[instrument(name = "transaction::command::set", level = "trace", skip(self, bytes), fields(
243		txn_id = %self.id,
244		key = ?key
245	))]
246	fn set_any(&mut self, key: TaggedKey, bytes: EncodedBytes) -> Result<()> {
247		if self.lifecycle == Lifecycle::Discarded {
248			return Err(TransactionError::RolledBack.into());
249		}
250		let encoded = key.encode();
251		self.modify(
252			encoded,
253			DeltaEntry {
254				delta: Delta::Set {
255					key,
256					bytes,
257				},
258				version: self.base_version(),
259			},
260		)
261	}
262
263	pub fn set<K: Into<TaggedKey> + Clone>(&mut self, key: &K, bytes: impl Into<EncodedBytes>) -> Result<()> {
264		self.set_any(key.clone().into(), bytes.into())
265	}
266
267	#[instrument(name = "transaction::command::remove_with_pre", level = "trace", skip(self, pre), fields(
268		txn_id = %self.id,
269		key = ?key,
270		value_len = pre.len()
271	))]
272	fn remove_with_pre_any(&mut self, key: TaggedKey, pre: EncodedBytes) -> Result<()> {
273		if self.lifecycle == Lifecycle::Discarded {
274			return Err(TransactionError::RolledBack.into());
275		}
276		let encoded = key.encode();
277		self.modify(
278			encoded,
279			DeltaEntry {
280				delta: Delta::remove_announced(key, pre),
281				version: self.base_version(),
282			},
283		)
284	}
285
286	pub fn remove_with_pre<K: Into<TaggedKey> + Clone>(&mut self, key: &K, pre: EncodedBytes) -> Result<()> {
287		self.remove_with_pre_any(key.clone().into(), pre)
288	}
289
290	#[instrument(name = "transaction::command::remove", level = "trace", skip(self), fields(
291		txn_id = %self.id,
292		key = ?key
293	))]
294	fn remove_any(&mut self, key: TaggedKey) -> Result<()> {
295		if self.lifecycle == Lifecycle::Discarded {
296			return Err(TransactionError::RolledBack.into());
297		}
298		let encoded = key.encode();
299		let announce = match self.get(&key)? {
300			Some(found) => RemoveAnnounce::Announced {
301				pre: found.bytes().clone(),
302			},
303			None => RemoveAnnounce::Silent,
304		};
305		self.modify(
306			encoded,
307			DeltaEntry {
308				delta: Delta::Remove {
309					key,
310					announce,
311				},
312				version: self.base_version(),
313			},
314		)
315	}
316
317	pub fn remove<K: Into<TaggedKey> + Clone>(&mut self, key: &K) -> Result<()> {
318		self.remove_any(key.clone().into())
319	}
320
321	#[instrument(name = "transaction::command::remove_unobserved", level = "trace", skip(self), fields(
322		txn_id = %self.id,
323		key = ?key
324	))]
325	fn remove_unobserved_any(&mut self, key: TaggedKey) -> Result<()> {
326		if self.lifecycle == Lifecycle::Discarded {
327			return Err(TransactionError::RolledBack.into());
328		}
329		let encoded = key.encode();
330		let announce = match self.get(&key)? {
331			Some(found) => RemoveAnnounce::Unobserved {
332				pre: found.bytes().clone(),
333			},
334			None => RemoveAnnounce::Silent,
335		};
336		self.modify(
337			encoded,
338			DeltaEntry {
339				delta: Delta::Remove {
340					key,
341					announce,
342				},
343				version: self.base_version(),
344			},
345		)
346	}
347
348	pub fn remove_unobserved<K: Into<TaggedKey> + Clone>(&mut self, key: &K) -> Result<()> {
349		self.remove_unobserved_any(key.clone().into())
350	}
351
352	#[instrument(name = "transaction::command::remove_unobserved_with_pre", level = "trace", skip(self, pre), fields(
353		txn_id = %self.id,
354		key = ?key,
355		value_len = pre.len()
356	))]
357	fn remove_unobserved_with_pre_any(&mut self, key: TaggedKey, pre: EncodedBytes) -> Result<()> {
358		if self.lifecycle == Lifecycle::Discarded {
359			return Err(TransactionError::RolledBack.into());
360		}
361		let encoded = key.encode();
362		self.modify(
363			encoded,
364			DeltaEntry {
365				delta: Delta::remove_unobserved(key, pre),
366				version: self.base_version(),
367			},
368		)
369	}
370
371	pub fn remove_unobserved_with_pre<K: Into<TaggedKey> + Clone>(
372		&mut self,
373		key: &K,
374		pre: EncodedBytes,
375	) -> Result<()> {
376		self.remove_unobserved_with_pre_any(key.clone().into(), pre)
377	}
378
379	#[instrument(name = "transaction::command::remove_silent", level = "trace", skip(self), fields(
380		txn_id = %self.id,
381		key = ?key
382	))]
383	fn remove_silent_any(&mut self, key: TaggedKey) -> Result<()> {
384		if self.lifecycle == Lifecycle::Discarded {
385			return Err(TransactionError::RolledBack.into());
386		}
387		let encoded = key.encode();
388		self.modify(
389			encoded,
390			DeltaEntry {
391				delta: Delta::remove_silent(key),
392				version: self.base_version(),
393			},
394		)
395	}
396
397	pub fn remove_silent<K: Into<TaggedKey> + Clone>(&mut self, key: &K) -> Result<()> {
398		self.remove_silent_any(key.clone().into())
399	}
400
401	#[instrument(name = "transaction::command::rollback", level = "debug", skip(self), fields(txn_id = %self.id))]
402	pub fn rollback(&mut self) -> Result<()> {
403		if self.lifecycle == Lifecycle::Discarded {
404			return Err(TransactionError::RolledBack.into());
405		}
406
407		self.pending_writes.rollback();
408		self.conflicts.rollback();
409		self.delta_log.clear();
410		self.duplicates.clear();
411		Ok(())
412	}
413
414	#[instrument(name = "transaction::command::contains_key", level = "trace", skip(self, key), fields(
415		txn_id = %self.id
416	))]
417	pub fn contains<K: Into<TaggedKey> + Clone>(&mut self, key: &K) -> Result<bool> {
418		if self.lifecycle == Lifecycle::Discarded {
419			return Err(TransactionError::RolledBack.into());
420		}
421
422		let key = key.clone().into();
423		let version = self.version();
424		match self.pending_writes.get(&key) {
425			Some(pending) => {
426				if pending.was_removed() {
427					return Ok(false);
428				}
429				Ok(true)
430			}
431			None => {
432				self.conflicts.mark_read(&key);
433				MultiVersionContains::contains(&self.engine.store, &key, version)
434			}
435		}
436	}
437
438	#[instrument(name = "transaction::command::get", level = "trace", skip(self, key), fields(
439		txn_id = %self.id
440	))]
441	pub fn get<K: Into<TaggedKey> + Clone>(&mut self, key: &K) -> Result<Option<TransactionValue>> {
442		if self.lifecycle == Lifecycle::Discarded {
443			return Err(TransactionError::RolledBack.into());
444		}
445
446		let key = key.clone().into();
447		let version = self.version();
448		if let Some(v) = self.pending_writes.get(&key) {
449			if let Some(bytes) = v.bytes() {
450				return Ok(Some(DeltaEntry {
451					delta: Delta::Set {
452						key: v.key().clone(),
453						bytes: bytes.clone(),
454					},
455					version: v.version,
456				}
457				.into()));
458			}
459			return Ok(None);
460		}
461		self.conflicts.mark_read(&key);
462		Ok(MultiVersionGet::get(&self.engine.store, &key, version)?.map(Into::into))
463	}
464
465	#[instrument(name = "transaction::command::get_committed", level = "trace", skip(self, key), fields(
466		txn_id = %self.id
467	))]
468	pub fn get_committed<K: Into<TaggedKey> + Clone>(&mut self, key: &K) -> Result<Option<TransactionValue>> {
469		if self.lifecycle == Lifecycle::Discarded {
470			return Err(TransactionError::RolledBack.into());
471		}
472		let key = key.clone().into();
473		let version = self.version();
474		self.conflicts.mark_read(&key);
475		Ok(MultiVersionGet::get(&self.engine.store, &key, version)?.map(Into::into))
476	}
477}
478
479impl MultiWriteTransaction {
480	#[instrument(name = "transaction::command::modify", level = "trace", skip(self, pending), fields(
481		txn_id = %self.id,
482		key_hex = %hex_display(encoded.as_ref()),
483		is_remove = pending.was_removed()
484	))]
485	fn modify(&mut self, encoded: EncodedKey, pending: DeltaEntry) -> Result<()> {
486		reifydb_assertions! {
487			assert!(
488				!matches!(pending.key(), TaggedKey::OperatorState(_)),
489				"operator state must reach the operator store through the committer split, never the \
490				 multi store: {}",
491				hex_display(encoded.as_ref())
492			);
493		}
494
495		let cnt = self.count + 1;
496		let size = self.size + self.pending_writes.estimate_size(&pending);
497		if cnt >= self.pending_writes.max_batch_entries() || size >= self.pending_writes.max_batch_size() {
498			return Err(TransactionError::TooLarge.into());
499		}
500
501		self.count = cnt;
502		self.size = size;
503
504		self.conflicts.mark_write(pending.key());
505
506		let version = pending.version;
507
508		let superseded = self
509			.pending_writes
510			.get_entry(pending.key())
511			.filter(|(_, old_value)| old_value.version != version)
512			.map(|(_, old_value)| old_value.key().clone());
513
514		if let Some(old_key) = superseded {
515			self.duplicates.push(DeltaEntry {
516				delta: match &pending.delta {
517					Delta::Set {
518						bytes,
519						..
520					} => Delta::Set {
521						key: old_key,
522						bytes: bytes.clone(),
523					},
524					Delta::Remove {
525						announce,
526						..
527					} => Delta::Remove {
528						key: old_key,
529						announce: announce.clone(),
530					},
531				},
532				version,
533			})
534		}
535
536		self.delta_log.push(pending.clone());
537		self.pending_writes.insert(pending);
538
539		Ok(())
540	}
541}
542
543impl MultiWriteTransaction {
544	#[instrument(name = "transaction::command::commit_pending", level = "debug", skip(self), fields(
545		txn_id = %self.id,
546		pending_count = self.pending_writes.len()
547	))]
548	fn commit_pending(&mut self, deltas: CowVec<Delta>) -> Result<CommitVersion> {
549		if self.lifecycle == Lifecycle::Discarded {
550			return Err(TransactionError::RolledBack.into());
551		}
552		let conflict_manager = mem::take(&mut self.conflicts);
553		let base_version = self.base_version();
554
555		let result = self.oracle.new_commit(base_version, conflict_manager, deltas);
556		self.release_read_snapshot(base_version);
557
558		match result? {
559			CreateCommitResult::Conflict(conflicts) => {
560				self.conflicts = conflicts;
561				Err(TransactionError::Conflict.into())
562			}
563			CreateCommitResult::TooOld => Err(TransactionError::TooOld.into()),
564			CreateCommitResult::Success(version) => {
565				self.pending_query_pin = Some(version);
566				self.clear_pending_state();
567				Ok(version)
568			}
569		}
570	}
571
572	#[instrument(name = "transaction::command::commit_pending_unchecked", level = "debug", skip(self), fields(
573		txn_id = %self.id,
574		pending_count = self.pending_writes.len()
575	))]
576	fn commit_pending_unchecked(&mut self, deltas: CowVec<Delta>) -> Result<CommitVersion> {
577		if self.lifecycle == Lifecycle::Discarded {
578			return Err(TransactionError::RolledBack.into());
579		}
580		let _ = mem::take(&mut self.conflicts);
581		let base_version = self.base_version();
582
583		let result = self.oracle.advance_unchecked(base_version, deltas);
584		self.release_read_snapshot(base_version);
585
586		match result? {
587			CreateCommitResult::Conflict(_) => unreachable!("advance_unchecked never reports a conflict"),
588			CreateCommitResult::TooOld => Err(TransactionError::TooOld.into()),
589			CreateCommitResult::Success(version) => {
590				self.pending_query_pin = Some(version);
591				self.clear_pending_state();
592				Ok(version)
593			}
594		}
595	}
596
597	#[inline]
598	fn release_read_snapshot(&mut self, base_version: CommitVersion) {
599		if self.lifecycle == Lifecycle::Active {
600			self.oracle.query.mark_finished(base_version);
601			self.transition_to(Lifecycle::QueryDone);
602		}
603	}
604
605	#[inline]
606	fn build_deltas(&self) -> CowVec<Delta> {
607		CowVec::new(optimize_deltas(
608			self.delta_log.iter().chain(self.duplicates.iter()).map(|pending| pending.delta.clone()),
609			self.preexisting_keys(),
610		))
611	}
612
613	#[inline]
614	fn clear_pending_state(&mut self) {
615		let _ = mem::take(&mut self.pending_writes);
616		let _ = mem::take(&mut self.duplicates);
617		let _ = mem::take(&mut self.delta_log);
618	}
619}
620
621impl MultiWriteTransaction {
622	#[instrument(name = "transaction::command::commit", level = "debug", skip(self), fields(pending_count = self.pending_writes().len()))]
623	pub fn commit(&mut self, flow_changes: Vec<Change>) -> Result<CommitVersion> {
624		if self.pending_writes.is_empty() {
625			self.discard();
626			return Ok(CommitVersion(0));
627		}
628		let deltas = self.build_deltas();
629		let commit_version = self.commit_pending(deltas.clone())?;
630		self.finalize_commit(commit_version, deltas, flow_changes)
631	}
632
633	#[instrument(name = "transaction::command::commit_unchecked", level = "debug", skip(self), fields(pending_count = self.pending_writes().len()))]
634	pub(crate) fn commit_unchecked(&mut self, flow_changes: Vec<Change>) -> Result<CommitVersion> {
635		if self.pending_writes.is_empty() {
636			self.discard();
637			return Ok(CommitVersion(0));
638		}
639		let deltas = self.build_deltas();
640		let commit_version = self.commit_pending_unchecked(deltas.clone())?;
641		self.finalize_commit(commit_version, deltas, flow_changes)
642	}
643
644	#[inline]
645	fn finalize_commit(
646		&mut self,
647		commit_version: CommitVersion,
648		deltas: CowVec<Delta>,
649		flow_changes: Vec<Change>,
650	) -> Result<CommitVersion> {
651		reifydb_assertions! {
652			assert_ne!(
653				commit_version, 0,
654				"finalize_commit reached with commit_version=0 but {} non-empty deltas; \
655				 CommitVersion(0) is the empty/discarded sentinel callers read as 'nothing \
656				 committed', so committing real deltas at it would silently drop them",
657				deltas.len()
658			);
659		}
660		let self_lease = self.oracle.leases.try_acquire(commit_version, self.oracle.query.done_until()).ok();
661		reifydb_assertions! {
662			assert!(
663				self_lease.is_some(),
664				"self-version lease on freshly-committed version {} must succeed: it is the newest \
665				 version so query.done_until() < it; failing means the historical-GC cutoff passed our \
666				 own commit version before its post-commit hooks ran",
667				commit_version.0
668			);
669		}
670		self.self_lease = self_lease;
671		if let Some(v) = self.pending_query_pin.take() {
672			self.oracle.query.mark_finished(v);
673		}
674		self.discard();
675		self.publish(commit_version, deltas, flow_changes);
676		Ok(commit_version)
677	}
678
679	#[inline]
680	fn publish(&self, commit_version: CommitVersion, deltas: CowVec<Delta>, flow_changes: Vec<Change>) {
681		self.oracle.done_commit(commit_version);
682		let version = ChangeVersion {
683			commit: commit_version,
684			source: self.source.unwrap_or(SourceVersion::from(commit_version)),
685		};
686		self.engine.event_bus.emit(PostCommitEvent::new(deltas, version, flow_changes));
687	}
688}
689
690impl MultiWriteTransaction {
691	#[instrument(name = "transaction::command::discard", level = "trace", skip(self), fields(txn_id = %self.id))]
692	pub fn discard(&mut self) {
693		if let Some(v) = self.pending_query_pin.take() {
694			self.oracle.query.mark_finished(v);
695		}
696		match self.lifecycle {
697			Lifecycle::Discarded => return,
698			Lifecycle::Active => self.oracle.query.mark_finished(self.version),
699			Lifecycle::QueryDone => {}
700		}
701		self.transition_to(Lifecycle::Discarded);
702	}
703
704	pub fn is_discard(&self) -> bool {
705		self.lifecycle == Lifecycle::Discarded
706	}
707
708	pub(crate) fn take_self_lease(&mut self) -> Option<VersionLeaseGuard> {
709		self.self_lease.take()
710	}
711}
712
713impl MultiWriteTransaction {
714	pub fn prefix(&mut self, prefix: &EncodedKey) -> Result<MultiVersionBatch<TaggedKey>> {
715		let items: Vec<_> = self
716			.range_encoded(EncodedKeyRange::prefix(prefix), RangeScope::All, 1024)
717			.collect::<Result<Vec<_>>>()?;
718		Ok(MultiVersionBatch {
719			items,
720			has_more: false,
721		})
722	}
723
724	pub fn prefix_rev(&mut self, prefix: &EncodedKey) -> Result<MultiVersionBatch<TaggedKey>> {
725		let items: Vec<_> = self
726			.range_encoded_rev(EncodedKeyRange::prefix(prefix), RangeScope::All, 1024)
727			.collect::<Result<Vec<_>>>()?;
728		Ok(MultiVersionBatch {
729			items,
730			has_more: false,
731		})
732	}
733
734	fn range_encoded(
735		&mut self,
736		range: EncodedKeyRange,
737		scope: RangeScope,
738		batch_size: usize,
739	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + '_> {
740		let multi_scope = scope.into_multi(self.version());
741		let (mut marker, pw) = self.marker_with_pending_writes();
742
743		marker.mark_range_encoded(range.clone());
744
745		let pending: Vec<(TaggedKey, DeltaEntry)> = pw
746			.iter()
747			.filter(|(k, _)| range.contains(&k.encode()))
748			.map(|(_, v)| (v.delta.key().clone(), v.clone()))
749			.collect();
750
751		let storage_iter = self.engine.store.range(range, multi_scope, batch_size);
752
753		Box::new(MergePendingIterator::new(pending, storage_iter, false))
754	}
755
756	fn range_encoded_rev(
757		&mut self,
758		range: EncodedKeyRange,
759		scope: RangeScope,
760		batch_size: usize,
761	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + '_> {
762		let multi_scope = scope.into_multi(self.version());
763		let (mut marker, pw) = self.marker_with_pending_writes();
764
765		marker.mark_range_encoded(range.clone());
766
767		let mut pending: Vec<(TaggedKey, DeltaEntry)> = pw
768			.iter()
769			.filter(|(k, _)| range.contains(&k.encode()))
770			.map(|(_, v)| (v.delta.key().clone(), v.clone()))
771			.collect();
772		pending.reverse();
773
774		let storage_iter = self.engine.store.range_rev(range, multi_scope, batch_size);
775
776		Box::new(MergePendingIterator::new(pending, storage_iter, true))
777	}
778
779	pub fn range(
780		&mut self,
781		range: TaggedKeyBoundRange,
782		scope: RangeScope,
783		batch_size: usize,
784	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + '_> {
785		let multi_scope = scope.into_multi(self.version());
786		let encoded = range.encode();
787		let (mut marker, pw) = self.marker_with_pending_writes();
788
789		marker.mark_range(range.clone());
790
791		let pending: Vec<(TaggedKey, DeltaEntry)> = pw
792			.range((range.start.as_ref(), range.end.as_ref()))
793			.map(|(_, v)| (v.delta.key().clone(), v.clone()))
794			.collect();
795
796		let storage_iter = self.engine.store.range(encoded, multi_scope, batch_size);
797
798		Box::new(MergePendingIterator::new(pending, storage_iter, false))
799	}
800
801	pub fn range_row(
802		&mut self,
803		storage: StorageId,
804		start: Bound<StorageRowKey>,
805		end: Bound<StorageRowKey>,
806		scope: RangeScope,
807		batch_size: usize,
808	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<StorageRowKey>>> + Send + '_> {
809		let multi_scope = scope.into_multi(self.version());
810		let range = row_bounds_to_typed(storage, &start, &end);
811		let (mut marker, pw) = self.marker_with_pending_writes();
812
813		marker.mark_range(range.clone());
814
815		let pending: Vec<(StorageRowKey, DeltaEntry)> = pw
816			.iter()
817			.filter_map(|(k, v)| {
818				let TaggedKeyBound::Key(TaggedKey::Row(decoded)) = k else {
819					return None;
820				};
821				(decoded.storage == storage && range.contains(k))
822					.then(|| (StorageRowKey::new(decoded.row), v.clone()))
823			})
824			.collect();
825
826		let storage_iter = self.engine.store.range_row(storage, start, end, multi_scope, batch_size);
827
828		Box::new(MergePendingIterator::new(pending, storage_iter, false))
829	}
830
831	pub fn range_partitioned_row(
832		&mut self,
833		storage: StorageId,
834		start: Bound<StoragePartitionedRowKey>,
835		end: Bound<StoragePartitionedRowKey>,
836		scope: RangeScope,
837		batch_size: usize,
838	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<StoragePartitionedRowKey>>> + Send + '_> {
839		let multi_scope = scope.into_multi(self.version());
840		let range = partitioned_row_bounds_to_typed(storage, &start, &end);
841		let (mut marker, pw) = self.marker_with_pending_writes();
842
843		marker.mark_range(range.clone());
844
845		let pending: Vec<(StoragePartitionedRowKey, DeltaEntry)> = pw
846			.iter()
847			.filter_map(|(k, v)| {
848				let TaggedKeyBound::Key(TaggedKey::PartitionedRow(decoded)) = k else {
849					return None;
850				};
851				(decoded.storage == storage && range.contains(k)).then(|| {
852					(StoragePartitionedRowKey::new(decoded.partition, decoded.row), v.clone())
853				})
854			})
855			.collect();
856
857		let storage_iter =
858			self.engine.store.range_partitioned_row(storage, start, end, multi_scope, batch_size);
859
860		Box::new(MergePendingIterator::new(pending, storage_iter, false))
861	}
862
863	pub fn range_persistence(
864		&mut self,
865		range: TaggedKeyBoundRange,
866		scope: RangeScope,
867		batch_size: usize,
868	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + '_> {
869		let multi_scope = scope.into_multi(self.version());
870		let encoded = range.encode();
871		let (mut marker, pw) = self.marker_with_pending_writes();
872
873		marker.mark_range(range.clone());
874
875		let pending: Vec<(TaggedKey, DeltaEntry)> = pw
876			.range((range.start.as_ref(), range.end.as_ref()))
877			.map(|(_, v)| (v.delta.key().clone(), v.clone()))
878			.collect();
879
880		let storage_iter = self.engine.store.range_persistence(encoded, multi_scope, batch_size);
881
882		Box::new(MergePendingIterator::new(pending, storage_iter, false))
883	}
884
885	pub fn range_rev(
886		&mut self,
887		range: TaggedKeyBoundRange,
888		scope: RangeScope,
889		batch_size: usize,
890	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + '_> {
891		let multi_scope = scope.into_multi(self.version());
892		let encoded = range.encode();
893		let (mut marker, pw) = self.marker_with_pending_writes();
894
895		marker.mark_range(range.clone());
896
897		let pending: Vec<(TaggedKey, DeltaEntry)> = pw
898			.range((range.start.as_ref(), range.end.as_ref()))
899			.rev()
900			.map(|(_, v)| (v.delta.key().clone(), v.clone()))
901			.collect();
902
903		let storage_iter = self.engine.store.range_rev(encoded, multi_scope, batch_size);
904
905		Box::new(MergePendingIterator::new(pending, storage_iter, true))
906	}
907
908	pub fn range_rev_persistence(
909		&mut self,
910		range: TaggedKeyBoundRange,
911		scope: RangeScope,
912		batch_size: usize,
913	) -> Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + '_> {
914		let multi_scope = scope.into_multi(self.version());
915		let encoded = range.encode();
916		let (mut marker, pw) = self.marker_with_pending_writes();
917
918		marker.mark_range(range.clone());
919
920		let pending: Vec<(TaggedKey, DeltaEntry)> = pw
921			.range((range.start.as_ref(), range.end.as_ref()))
922			.rev()
923			.map(|(_, v)| (v.delta.key().clone(), v.clone()))
924			.collect();
925
926		let storage_iter = self.engine.store.range_rev_persistence(encoded, multi_scope, batch_size);
927
928		Box::new(MergePendingIterator::new(pending, storage_iter, true))
929	}
930}
931
932#[cfg(test)]
933mod tests {
934	use reifydb_codec::key::serializer::KeySerializer;
935	use reifydb_core::{common::CommitVersion, interface::catalog::id::QueueId, key::queue::QueueDeduplicationKey};
936	use reifydb_value::{
937		util::cowvec::CowVec,
938		value::{duration::Duration, partition::Partition, row_number::RowNumber},
939	};
940
941	use super::*;
942	use crate::multi::transaction::MultiTransaction;
943
944	fn test_key(s: &str) -> QueueDeduplicationKey {
945		QueueDeduplicationKey::new(QueueId(1), s.as_bytes().iter().map(|b| !b).collect::<Vec<u8>>())
946	}
947
948	fn test_bytes(s: &str) -> EncodedBytes {
949		let mut ser = KeySerializer::new();
950		ser.extend_str(s);
951		EncodedBytes(CowVec::new(ser.finish().as_slice().to_vec()))
952	}
953
954	#[test]
955	fn commit_version_stays_protected_from_query_watermark_race_until_finalized() {
956		// An allocated but unfinalized commit version must hold the query watermark down. If a
957		// racing higher version can advance done_until past it, the historical-GC cutoff crosses
958		// a commit version whose own post-commit hooks have not run.
959		let engine = MultiTransaction::testing();
960		let mut txn = engine.begin_command().unwrap();
961		txn.set(&test_key("race-key"), test_bytes("race-value")).unwrap();
962
963		// Allocate commit_version exactly as commit() would, without finalizing it yet.
964		let deltas = txn.build_deltas();
965		let commit_version = txn.commit_pending(deltas.clone()).unwrap();
966		assert_ne!(commit_version, CommitVersion(0));
967
968		// An unrelated transaction finishing at a higher version is the real-world trigger.
969		let racer = CommitVersion(commit_version.0 + 1);
970		txn.oracle.query.register_in_flight(racer);
971		txn.oracle.query.mark_finished(racer);
972
973		// Bounded wait, not a sleep: while commit_version is open done_until can never reach the
974		// racer, so both outcomes resolve deterministically inside the bound.
975		let racer_observed =
976			txn.oracle.query.wait_for_mark_timeout(racer, Duration::from_milliseconds(300).unwrap());
977		assert!(
978			!racer_observed,
979			"query watermark advanced to {} before commit_version {} was finalized - the \
980			 historical-GC cutoff raced past our own not-yet-leased commit version",
981			racer.0, commit_version.0
982		);
983
984		let result = txn.finalize_commit(commit_version, deltas, vec![]);
985		assert_eq!(
986			result.unwrap(),
987			commit_version,
988			"commit of our own freshly-allocated version must succeed even under a racing query watermark"
989		);
990	}
991
992	fn storage() -> StorageId {
993		StorageId::table(7)
994	}
995
996	fn row_range_bytes(start: Bound<StorageRowKey>, end: Bound<StorageRowKey>) -> EncodedKeyRange {
997		row_bounds_to_typed(storage(), &start, &end).encode()
998	}
999
1000	fn partitioned_range_bytes(
1001		start: Bound<StoragePartitionedRowKey>,
1002		end: Bound<StoragePartitionedRowKey>,
1003	) -> EncodedKeyRange {
1004		partitioned_row_bounds_to_typed(storage(), &start, &end).encode()
1005	}
1006
1007	#[test]
1008	fn an_unbounded_row_range_still_spans_exactly_the_storages_own_bytes() {
1009		// The conflict manager now tracks these ranges typed, so the typed spelling of an
1010		// open end has to land on the same byte as the storage_end producer it replaced.
1011		// Anything wider silently reports conflicts that are not there; anything narrower
1012		// silently misses real ones, and neither shows up as a failure anywhere else.
1013		let range = row_range_bytes(Bound::Unbounded, Bound::Unbounded);
1014		assert_eq!(range.start, Bound::Included(RowKey::storage_start(storage())));
1015		assert_eq!(range.end, Bound::Included(RowKey::storage_end(storage())));
1016
1017		let range = partitioned_range_bytes(Bound::Unbounded, Bound::Unbounded);
1018		assert_eq!(range.start, Bound::Included(PartitionedRowKey::storage_start(storage())));
1019		assert_eq!(range.end, Bound::Included(PartitionedRowKey::storage_end(storage())));
1020	}
1021
1022	#[test]
1023	fn a_bounded_row_range_encodes_the_keys_it_names() {
1024		let low = StorageRowKey::new(RowNumber(1));
1025		let high = StorageRowKey::new(RowNumber(9));
1026		let range = row_range_bytes(Bound::Included(low), Bound::Excluded(high));
1027		assert_eq!(range.start, Bound::Included(RowKey::encoded(storage(), RowNumber(1))));
1028		assert_eq!(range.end, Bound::Excluded(RowKey::encoded(storage(), RowNumber(9))));
1029
1030		let low = StoragePartitionedRowKey::new(Partition(3), RowNumber(1));
1031		let high = StoragePartitionedRowKey::new(Partition(3), RowNumber(9));
1032		let range = partitioned_range_bytes(Bound::Excluded(low), Bound::Included(high));
1033		assert_eq!(
1034			range.start,
1035			Bound::Excluded(PartitionedRowKey::encoded(storage(), Partition(3), RowNumber(1)))
1036		);
1037		assert_eq!(
1038			range.end,
1039			Bound::Included(PartitionedRowKey::encoded(storage(), Partition(3), RowNumber(9)))
1040		);
1041	}
1042
1043	#[test]
1044	fn a_row_range_contains_the_same_keys_typed_as_its_bytes_do() {
1045		// The pending-writes filter reads containment off the typed bounds while the storage
1046		// iterator reads it off the bytes; a disagreement hides a transaction's own writes.
1047		// Row numbers encode descending, so a non-empty span runs from the higher number to
1048		// the lower one. Spelling it 3..9 would make every case empty and let the end bound
1049		// answer alone, which hides whatever the start bound does.
1050		let rows: Vec<RowNumber> = (0u64..12).map(RowNumber).collect();
1051		let low = StorageRowKey::new(RowNumber(9));
1052		let high = StorageRowKey::new(RowNumber(3));
1053		let cases = [
1054			(Bound::Unbounded, Bound::Unbounded),
1055			(Bound::Included(low), Bound::Unbounded),
1056			(Bound::Excluded(low), Bound::Unbounded),
1057			(Bound::Excluded(low), Bound::Excluded(high)),
1058			(Bound::Included(low), Bound::Included(high)),
1059			(Bound::Unbounded, Bound::Excluded(high)),
1060			(Bound::Unbounded, Bound::Included(high)),
1061		];
1062
1063		for (start, end) in cases {
1064			let typed = row_bounds_to_typed(storage(), &start, &end);
1065			let bytes = typed.encode();
1066			for row in &rows {
1067				let key = RowKey::new(storage(), *row);
1068				let encoded = RowKey::encoded(storage(), *row);
1069				assert_eq!(
1070					typed.contains(&TaggedKeyBound::Key(TaggedKey::Row(key))),
1071					bytes.contains(&encoded),
1072					"row {row:?} in {start:?}..{end:?}"
1073				);
1074			}
1075		}
1076	}
1077}
1078
1079pub(crate) struct MergePendingIterator<I, K = EncodedKey> {
1080	pending_iter: iter::Peekable<vec::IntoIter<(K, DeltaEntry)>>,
1081	storage_iter: I,
1082	next_storage: Option<MultiVersionRow<K>>,
1083	reverse: bool,
1084}
1085
1086impl<I, K> MergePendingIterator<I, K>
1087where
1088	K: Ord,
1089	I: Iterator<Item = Result<MultiVersionRow<K>>>,
1090{
1091	pub(crate) fn new(pending: Vec<(K, DeltaEntry)>, storage_iter: I, reverse: bool) -> Self {
1092		Self {
1093			pending_iter: pending.into_iter().peekable(),
1094			storage_iter,
1095			next_storage: None,
1096			reverse,
1097		}
1098	}
1099}
1100
1101impl<I, K> Iterator for MergePendingIterator<I, K>
1102where
1103	K: Ord,
1104	I: Iterator<Item = Result<MultiVersionRow<K>>>,
1105{
1106	type Item = Result<MultiVersionRow<K>>;
1107
1108	fn next(&mut self) -> Option<Self::Item> {
1109		loop {
1110			if self.next_storage.is_none() {
1111				self.next_storage = match self.storage_iter.next() {
1112					Some(Ok(v)) => Some(v),
1113					Some(Err(e)) => return Some(Err(e)),
1114					None => None,
1115				};
1116			}
1117
1118			match (self.pending_iter.peek(), &self.next_storage) {
1119				(Some((pending_key, _)), Some(storage_val)) => {
1120					let cmp = pending_key.cmp(&storage_val.key);
1121					let should_yield_pending = if self.reverse {
1122						matches!(cmp, Ordering::Greater)
1123					} else {
1124						matches!(cmp, Ordering::Less)
1125					};
1126
1127					if should_yield_pending {
1128						let (key, value) = self.pending_iter.next().unwrap();
1129						if let Some(bytes) = value.bytes() {
1130							return Some(Ok(MultiVersionRow {
1131								key,
1132								bytes: bytes.clone(),
1133								version: value.version,
1134							}));
1135						}
1136					} else if matches!(cmp, Ordering::Equal) {
1137						let (key, value) = self.pending_iter.next().unwrap();
1138						self.next_storage = None;
1139						if let Some(bytes) = value.bytes() {
1140							return Some(Ok(MultiVersionRow {
1141								key,
1142								bytes: bytes.clone(),
1143								version: value.version,
1144							}));
1145						}
1146					} else {
1147						return Some(Ok(self.next_storage.take().unwrap()));
1148					}
1149				}
1150				(Some(_), None) => {
1151					let (key, value) = self.pending_iter.next().unwrap();
1152					if let Some(bytes) = value.bytes() {
1153						return Some(Ok(MultiVersionRow {
1154							key,
1155							bytes: bytes.clone(),
1156							version: value.version,
1157						}));
1158					}
1159				}
1160				(None, Some(_)) => {
1161					return Some(Ok(self.next_storage.take().unwrap()));
1162				}
1163				(None, None) => return None,
1164			}
1165		}
1166	}
1167}
1168
1169fn row_bounds_to_typed(
1170	storage: StorageId,
1171	start: &Bound<StorageRowKey>,
1172	end: &Bound<StorageRowKey>,
1173) -> TaggedKeyBoundRange {
1174	let bound = |k: &StorageRowKey| TaggedKeyBound::Key(TaggedKey::Row(RowKey::new(storage, k.row())));
1175	let lower = match start {
1176		Bound::Included(k) => Bound::Included(bound(k)),
1177		Bound::Excluded(k) => Bound::Excluded(bound(k)),
1178		Bound::Unbounded => Bound::Included(storage_span_start(KeyTag::Row, storage)),
1179	};
1180	let upper = match end {
1181		Bound::Included(k) => Bound::Included(bound(k)),
1182		Bound::Excluded(k) => Bound::Excluded(bound(k)),
1183		Bound::Unbounded => Bound::Included(storage_span_end(KeyTag::Row, storage)),
1184	};
1185	TaggedKeyBoundRange {
1186		start: lower,
1187		end: upper,
1188	}
1189}
1190
1191fn partitioned_row_bounds_to_typed(
1192	storage: StorageId,
1193	start: &Bound<StoragePartitionedRowKey>,
1194	end: &Bound<StoragePartitionedRowKey>,
1195) -> TaggedKeyBoundRange {
1196	let bound = |k: &StoragePartitionedRowKey| {
1197		TaggedKeyBound::Key(TaggedKey::PartitionedRow(PartitionedRowKey::new(storage, k.partition(), k.row())))
1198	};
1199	let lower = match start {
1200		Bound::Included(k) => Bound::Included(bound(k)),
1201		Bound::Excluded(k) => Bound::Excluded(bound(k)),
1202		Bound::Unbounded => Bound::Included(storage_span_start(KeyTag::PartitionedRow, storage)),
1203	};
1204	let upper = match end {
1205		Bound::Included(k) => Bound::Included(bound(k)),
1206		Bound::Excluded(k) => Bound::Excluded(bound(k)),
1207		Bound::Unbounded => Bound::Included(storage_span_end(KeyTag::PartitionedRow, storage)),
1208	};
1209	TaggedKeyBoundRange {
1210		start: lower,
1211		end: upper,
1212	}
1213}
1214
1215fn storage_span_start(kind: KeyTag, storage: StorageId) -> TaggedKeyBound {
1216	TaggedKeyBound::prefix(kind, object_fields(ObjectId::from(storage)))
1217}
1218
1219fn storage_span_end(kind: KeyTag, storage: StorageId) -> TaggedKeyBound {
1220	TaggedKeyBound::prefix(kind, object_fields(ObjectId::from(storage).prev()))
1221}