Skip to main content

tuwunel_database/
stream.rs

1mod items;
2mod items_rev;
3mod keys;
4mod keys_rev;
5
6use std::{mem::replace, sync::Arc};
7
8use rocksdb::{DBRawIteratorWithThreadMode, ReadOptions};
9use tuwunel_core::Result;
10
11pub(crate) use self::{items::Items, items_rev::ItemsRev, keys::Keys, keys_rev::KeysRev};
12use crate::{
13	Map, Slice,
14	engine::Db,
15	keyval::{Key, KeyVal, Val},
16	util::{is_incomplete, map_err},
17};
18
19/// Owns a RocksDB raw iterator and its initial positioning state.
20///
21/// The iterator borrows the map's engine for `'a`. The flags distinguish the
22/// first poll from later cursor movement and record whether an explicit seek
23/// position has already been chosen.
24pub(crate) struct State<'a> {
25	inner: Inner<'a>,
26	seek: bool,
27	init: bool,
28}
29
30/// Defines the polling operations shared by database cursor streams.
31///
32/// Implementations position a [`State`], fetch one borrowed item, and surface
33/// RocksDB cursor errors. Borrowed items remain valid only until the next
34/// cursor movement and must be owned before they are retained.
35pub(crate) trait Cursor<'a, T>: Send {
36	fn state(&self) -> &State<'a>;
37
38	fn state_mut(&mut self) -> &mut State<'a>;
39
40	fn count(&self) -> (usize, Option<usize>);
41
42	fn fetch(&self) -> Option<T>;
43
44	fn seek(&mut self);
45
46	#[inline]
47	fn get(&self) -> Option<Result<T>> {
48		self.fetch()
49			.map(Ok)
50			.or_else(|| self.state().status().map(map_err).map(Err))
51	}
52
53	#[inline]
54	fn seek_and_get(&mut self) -> Option<Result<T>> {
55		self.seek();
56		self.get()
57	}
58}
59
60type Inner<'a> = DBRawIteratorWithThreadMode<'a, Db>;
61type From<'a> = Option<Key<'a>>;
62
63impl<'a> State<'a> {
64	#[inline]
65	pub(super) fn new(map: &'a Arc<Map>, opts: ReadOptions) -> Self {
66		Self {
67			init: true,
68			seek: false,
69			inner: map
70				.engine()
71				.db
72				.raw_iterator_cf_opt(&map.cf(), opts),
73		}
74	}
75
76	#[inline]
77	#[tracing::instrument(level = "trace", skip_all)]
78	pub(super) fn init_fwd(mut self, from: From<'_>) -> Self {
79		debug_assert!(self.init, "init must be set to make this call");
80		debug_assert!(!self.seek, "seek must not be set to make this call");
81
82		if let Some(key) = from {
83			self.inner.seek(key);
84		} else {
85			self.inner.seek_to_first();
86		}
87
88		self.seek = true;
89		self
90	}
91
92	#[inline]
93	#[tracing::instrument(level = "trace", skip_all)]
94	pub(super) fn init_rev(mut self, from: From<'_>) -> Self {
95		debug_assert!(self.init, "init must be set to make this call");
96		debug_assert!(!self.seek, "seek must not be set to make this call");
97
98		if let Some(key) = from {
99			self.inner.seek_for_prev(key);
100		} else {
101			self.inner.seek_to_last();
102		}
103
104		self.seek = true;
105		self
106	}
107
108	#[inline]
109	#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
110	pub(super) fn seek_fwd(&mut self) {
111		if !replace(&mut self.init, false) {
112			self.inner.next();
113		} else if !self.seek {
114			self.inner.seek_to_first();
115		}
116	}
117
118	#[inline]
119	#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
120	pub(super) fn seek_rev(&mut self) {
121		if !replace(&mut self.init, false) {
122			self.inner.prev();
123		} else if !self.seek {
124			self.inner.seek_to_last();
125		}
126	}
127
128	#[inline]
129	#[expect(clippy::unused_self)]
130	#[tracing::instrument(level = "trace", skip_all, ret)]
131	pub(super) fn count_fwd(&self) -> (usize, Option<usize>) { (0, None) }
132
133	#[inline]
134	#[expect(clippy::unused_self)]
135	#[tracing::instrument(level = "trace", skip_all, ret)]
136	pub(super) fn count_rev(&self) -> (usize, Option<usize>) { (0, None) }
137
138	#[inline]
139	fn fetch_key(&self) -> Option<Key<'_>> { self.inner.key() }
140
141	#[inline]
142	fn _fetch_val(&self) -> Option<Val<'_>> { self.inner.value() }
143
144	#[inline]
145	fn fetch(&self) -> Option<KeyVal<'_>> { self.inner.item() }
146
147	pub(super) fn is_incomplete(&self) -> bool {
148		matches!(self.status(), Some(e) if is_incomplete(&e))
149	}
150
151	#[inline]
152	pub(super) fn status(&self) -> Option<rocksdb::Error> { self.inner.status().err() }
153
154	#[inline]
155	pub(super) fn valid(&self) -> bool { self.inner.valid() }
156}
157
158/// Extends both borrows in a cursor key-value pair to the stream lifetime.
159///
160/// The returned pair remains valid only until the next cursor movement. This
161/// helper delegates each component to [`slice_longevity`] and carries the same
162/// lifetime contract.
163fn keyval_longevity<'a, 'b: 'a>(item: KeyVal<'a>) -> KeyVal<'b> {
164	(slice_longevity::<'a, 'b>(item.0), slice_longevity::<'a, 'b>(item.1))
165}
166
167/// Extends a RocksDB cursor slice borrow to the stream lifetime.
168///
169/// The extension bridges `Stream`'s fixed item type and does not extend the
170/// underlying storage validity. The returned reference becomes invalid when the
171/// cursor next moves, so callers must not retain it across a poll.
172fn slice_longevity<'a, 'b: 'a>(item: &'a Slice) -> &'b Slice {
173	// SAFETY: The lifetime of the data returned by the rocksdb cursor is only valid
174	// between each movement of the cursor. It is hereby unsafely extended to match
175	// the lifetime of the cursor itself. This is due to the limitation of the
176	// Stream trait where the Item is incapable of conveying a lifetime; this is due
177	// to GAT's being unstable during its development. This unsafety can be removed
178	// as soon as this limitation is addressed by an upcoming version.
179	//
180	// We have done our best to mitigate the implications of this in conjunction
181	// with the deserialization API such that borrows being held across movements of
182	// the cursor do not happen accidentally. The compiler will still error when
183	// values herein produced try to leave a closure passed to a StreamExt API. But
184	// escapes can happen if you explicitly and intentionally attempt it, and there
185	// will be no compiler error or warning. This is primarily the case with
186	// calling collect() without a preceding map(ToOwned::to_owned). A collection
187	// of references here is illegal, but this will not be enforced by the compiler.
188	unsafe { std::mem::transmute(item) }
189}