Skip to main content

reifydb_cdc/storage/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4//! Pluggable backing store for the CDC log. The in-memory implementation is the testing default; SQLite is the
5//! durable default for production deployments. Both implement the same trait surface so the producer and consumer
6//! sides are agnostic to which is configured.
7
8pub mod cached;
9pub mod memory;
10pub mod recent_cache;
11#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
12pub mod sqlite;
13
14use std::{collections::Bound, sync};
15
16#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
17use cached::CachedCdcStorage;
18use memory::MemoryCdcStorage;
19use reifydb_core::{
20	common::CommitVersion,
21	encoded::key::EncodedKey,
22	interface::cdc::{Cdc, CdcBatch},
23};
24use reifydb_runtime::shutdown::Shutdown;
25#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
26use reifydb_sqlite::SqliteConfig;
27use reifydb_value::value::datetime::DateTime;
28
29use crate::error::CdcError;
30
31pub type CdcStorageResult<T> = Result<T, CdcError>;
32
33enum ScanContinuation {
34	Done(CommitVersion),
35	Continue(Bound<CommitVersion>),
36}
37
38#[inline]
39fn scan_batch_for_cutoff(items: &[Cdc], cutoff: DateTime) -> Option<CommitVersion> {
40	for cdc in items {
41		if cdc.timestamp >= cutoff {
42			return Some(cdc.version);
43		}
44	}
45	None
46}
47
48#[inline]
49fn next_start_after_batch(batch: &CdcBatch, max: CommitVersion) -> ScanContinuation {
50	if !batch.has_more {
51		return ScanContinuation::Done(CommitVersion(max.0.saturating_add(1)));
52	}
53	let last = batch.items.last().unwrap().version;
54	ScanContinuation::Continue(Bound::Excluded(last))
55}
56
57#[inline]
58pub(crate) fn normalize_range_inclusive(
59	start: Bound<CommitVersion>,
60	end: Bound<CommitVersion>,
61) -> Option<(CommitVersion, CommitVersion)> {
62	let lo_inc = match start {
63		Bound::Included(v) => v,
64		Bound::Excluded(v) => CommitVersion(v.0.saturating_add(1)),
65		Bound::Unbounded => CommitVersion(0),
66	};
67	let hi_inc = match end {
68		Bound::Included(v) => v,
69		Bound::Excluded(v) => CommitVersion(v.0.saturating_sub(1)),
70		Bound::Unbounded => CommitVersion(u64::MAX),
71	};
72	if lo_inc > hi_inc {
73		None
74	} else {
75		Some((lo_inc, hi_inc))
76	}
77}
78
79#[derive(Debug, Clone)]
80pub struct DroppedCdcEntry {
81	pub key: EncodedKey,
82	pub value_bytes: u64,
83}
84
85#[derive(Debug, Clone, Default)]
86pub struct DropBeforeResult {
87	pub count: usize,
88	pub entries: Vec<DroppedCdcEntry>,
89	pub more_remaining: bool,
90}
91
92pub trait CdcStorage: Send + Sync + Clone + 'static {
93	fn write(&self, cdc: &Cdc) -> CdcStorageResult<()>;
94
95	fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>>;
96
97	fn read_range(
98		&self,
99		start: Bound<CommitVersion>,
100		end: Bound<CommitVersion>,
101		batch_size: u64,
102	) -> CdcStorageResult<CdcBatch>;
103
104	fn count(&self, version: CommitVersion) -> CdcStorageResult<usize>;
105
106	fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>>;
107
108	fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>>;
109
110	fn exists(&self, version: CommitVersion) -> CdcStorageResult<bool> {
111		Ok(self.read(version)?.is_some())
112	}
113
114	fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult>;
115
116	fn vacuum(&self) -> CdcStorageResult<()> {
117		Ok(())
118	}
119
120	fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
121		let Some(min) = self.min_version()? else {
122			return Ok(None);
123		};
124		let Some(max) = self.max_version()? else {
125			return Ok(None);
126		};
127
128		let mut next_start = Bound::Included(min);
129		loop {
130			let batch = self.read_range(next_start, Bound::Unbounded, 256)?;
131			if batch.items.is_empty() {
132				return Ok(Some(CommitVersion(max.0.saturating_add(1))));
133			}
134			if let Some(version) = scan_batch_for_cutoff(&batch.items, cutoff) {
135				return Ok(Some(version));
136			}
137			match next_start_after_batch(&batch, max) {
138				ScanContinuation::Done(v) => return Ok(Some(v)),
139				ScanContinuation::Continue(start) => next_start = start,
140			}
141		}
142	}
143
144	fn range(&self, start: Bound<CommitVersion>, end: Bound<CommitVersion>) -> CdcStorageResult<CdcBatch> {
145		self.read_range(start, end, 1024)
146	}
147
148	fn scan(&self, batch_size: u64) -> CdcStorageResult<CdcBatch> {
149		self.read_range(Bound::Unbounded, Bound::Unbounded, batch_size)
150	}
151}
152
153impl<T: CdcStorage> CdcStorage for sync::Arc<T> {
154	fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
155		(**self).write(cdc)
156	}
157
158	fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
159		(**self).read(version)
160	}
161
162	fn read_range(
163		&self,
164		start: Bound<CommitVersion>,
165		end: Bound<CommitVersion>,
166		batch_size: u64,
167	) -> CdcStorageResult<CdcBatch> {
168		(**self).read_range(start, end, batch_size)
169	}
170
171	fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
172		(**self).count(version)
173	}
174
175	fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
176		(**self).min_version()
177	}
178
179	fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
180		(**self).max_version()
181	}
182
183	fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
184		(**self).drop_before(version, limit)
185	}
186
187	fn vacuum(&self) -> CdcStorageResult<()> {
188		(**self).vacuum()
189	}
190
191	fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
192		(**self).find_ttl_cutoff(cutoff)
193	}
194}
195
196#[derive(Clone)]
197pub enum CdcStore {
198	Memory(MemoryCdcStorage),
199
200	#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
201	Sqlite(CachedCdcStorage<sqlite::storage::SqliteCdcStorage>),
202}
203
204impl Shutdown for CdcStore {
205	fn shutdown(&self) {
206		match self {
207			Self::Memory(_) => {}
208			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
209			Self::Sqlite(s) => s.inner().shutdown(),
210		}
211	}
212}
213
214impl CdcStore {
215	pub fn memory() -> Self {
216		Self::Memory(MemoryCdcStorage::new())
217	}
218
219	#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
220	pub fn sqlite(config: SqliteConfig, recent_cache_capacity: usize) -> Self {
221		Self::Sqlite(CachedCdcStorage::new(
222			sqlite::storage::SqliteCdcStorage::new(config),
223			recent_cache_capacity,
224		))
225	}
226
227	pub fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
228		match self {
229			Self::Memory(s) => s.write(cdc),
230			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
231			Self::Sqlite(s) => s.write(cdc),
232		}
233	}
234
235	pub fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
236		match self {
237			Self::Memory(s) => s.read(version),
238			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
239			Self::Sqlite(s) => s.read(version),
240		}
241	}
242
243	pub fn read_range(
244		&self,
245		start: Bound<CommitVersion>,
246		end: Bound<CommitVersion>,
247		batch_size: u64,
248	) -> CdcStorageResult<CdcBatch> {
249		match self {
250			Self::Memory(s) => s.read_range(start, end, batch_size),
251			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
252			Self::Sqlite(s) => s.read_range(start, end, batch_size),
253		}
254	}
255
256	pub fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
257		match self {
258			Self::Memory(s) => s.count(version),
259			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
260			Self::Sqlite(s) => s.count(version),
261		}
262	}
263
264	pub fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
265		match self {
266			Self::Memory(s) => s.min_version(),
267			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
268			Self::Sqlite(s) => s.min_version(),
269		}
270	}
271
272	pub fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
273		match self {
274			Self::Memory(s) => s.max_version(),
275			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
276			Self::Sqlite(s) => s.max_version(),
277		}
278	}
279
280	pub fn delete_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
281		match self {
282			Self::Memory(s) => s.drop_before(version, limit),
283			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
284			Self::Sqlite(s) => s.drop_before(version, limit),
285		}
286	}
287
288	pub fn vacuum(&self) -> CdcStorageResult<()> {
289		match self {
290			Self::Memory(s) => s.vacuum(),
291			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
292			Self::Sqlite(s) => s.vacuum(),
293		}
294	}
295
296	pub fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
297		match self {
298			Self::Memory(s) => s.find_ttl_cutoff(cutoff),
299			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
300			Self::Sqlite(s) => s.find_ttl_cutoff(cutoff),
301		}
302	}
303}
304
305impl CdcStorage for CdcStore {
306	fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
307		CdcStore::write(self, cdc)
308	}
309
310	fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
311		CdcStore::read(self, version)
312	}
313
314	fn read_range(
315		&self,
316		start: Bound<CommitVersion>,
317		end: Bound<CommitVersion>,
318		batch_size: u64,
319	) -> CdcStorageResult<CdcBatch> {
320		CdcStore::read_range(self, start, end, batch_size)
321	}
322
323	fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
324		CdcStore::count(self, version)
325	}
326
327	fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
328		CdcStore::min_version(self)
329	}
330
331	fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
332		CdcStore::max_version(self)
333	}
334
335	fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
336		CdcStore::delete_before(self, version, limit)
337	}
338
339	fn vacuum(&self) -> CdcStorageResult<()> {
340		CdcStore::vacuum(self)
341	}
342
343	fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
344		CdcStore::find_ttl_cutoff(self, cutoff)
345	}
346}