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_codec::key::encoded::EncodedKey;
20use reifydb_core::{
21	common::CommitVersion,
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 find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
117		let Some(min) = self.min_version()? else {
118			return Ok(None);
119		};
120		let Some(max) = self.max_version()? else {
121			return Ok(None);
122		};
123
124		let mut next_start = Bound::Included(min);
125		loop {
126			let batch = self.read_range(next_start, Bound::Unbounded, 256)?;
127			if batch.items.is_empty() {
128				return Ok(Some(CommitVersion(max.0.saturating_add(1))));
129			}
130			if let Some(version) = scan_batch_for_cutoff(&batch.items, cutoff) {
131				return Ok(Some(version));
132			}
133			match next_start_after_batch(&batch, max) {
134				ScanContinuation::Done(v) => return Ok(Some(v)),
135				ScanContinuation::Continue(start) => next_start = start,
136			}
137		}
138	}
139
140	fn range(&self, start: Bound<CommitVersion>, end: Bound<CommitVersion>) -> CdcStorageResult<CdcBatch> {
141		self.read_range(start, end, 1024)
142	}
143
144	fn scan(&self, batch_size: u64) -> CdcStorageResult<CdcBatch> {
145		self.read_range(Bound::Unbounded, Bound::Unbounded, batch_size)
146	}
147}
148
149impl<T: CdcStorage> CdcStorage for sync::Arc<T> {
150	fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
151		(**self).write(cdc)
152	}
153
154	fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
155		(**self).read(version)
156	}
157
158	fn read_range(
159		&self,
160		start: Bound<CommitVersion>,
161		end: Bound<CommitVersion>,
162		batch_size: u64,
163	) -> CdcStorageResult<CdcBatch> {
164		(**self).read_range(start, end, batch_size)
165	}
166
167	fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
168		(**self).count(version)
169	}
170
171	fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
172		(**self).min_version()
173	}
174
175	fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
176		(**self).max_version()
177	}
178
179	fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
180		(**self).drop_before(version, limit)
181	}
182
183	fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
184		(**self).find_ttl_cutoff(cutoff)
185	}
186}
187
188#[derive(Clone)]
189pub enum CdcStore {
190	Memory(MemoryCdcStorage),
191
192	#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
193	Sqlite(CachedCdcStorage<sqlite::storage::SqliteCdcStorage>),
194}
195
196impl Shutdown for CdcStore {
197	fn shutdown(&self) {
198		match self {
199			Self::Memory(_) => {}
200			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
201			Self::Sqlite(s) => s.inner().shutdown(),
202		}
203	}
204}
205
206impl CdcStore {
207	pub fn memory() -> Self {
208		Self::Memory(MemoryCdcStorage::new())
209	}
210
211	#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
212	pub fn sqlite(config: SqliteConfig, recent_cache_capacity: usize) -> Self {
213		Self::Sqlite(CachedCdcStorage::new(
214			sqlite::storage::SqliteCdcStorage::new(config),
215			recent_cache_capacity,
216		))
217	}
218
219	#[cfg_attr(any(not(feature = "sqlite"), target_arch = "wasm32"), allow(unused_variables))]
220	pub fn configure_wal_autocheckpoint(&self, frames: u32) {
221		match self {
222			Self::Memory(_) => {}
223			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
224			Self::Sqlite(s) => s.inner().set_wal_autocheckpoint(frames),
225		}
226	}
227
228	pub fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
229		match self {
230			Self::Memory(s) => s.write(cdc),
231			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
232			Self::Sqlite(s) => s.write(cdc),
233		}
234	}
235
236	pub fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
237		match self {
238			Self::Memory(s) => s.read(version),
239			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
240			Self::Sqlite(s) => s.read(version),
241		}
242	}
243
244	pub fn read_range(
245		&self,
246		start: Bound<CommitVersion>,
247		end: Bound<CommitVersion>,
248		batch_size: u64,
249	) -> CdcStorageResult<CdcBatch> {
250		match self {
251			Self::Memory(s) => s.read_range(start, end, batch_size),
252			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
253			Self::Sqlite(s) => s.read_range(start, end, batch_size),
254		}
255	}
256
257	pub fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
258		match self {
259			Self::Memory(s) => s.count(version),
260			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
261			Self::Sqlite(s) => s.count(version),
262		}
263	}
264
265	pub fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
266		match self {
267			Self::Memory(s) => s.min_version(),
268			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
269			Self::Sqlite(s) => s.min_version(),
270		}
271	}
272
273	pub fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
274		match self {
275			Self::Memory(s) => s.max_version(),
276			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
277			Self::Sqlite(s) => s.max_version(),
278		}
279	}
280
281	pub fn delete_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
282		match self {
283			Self::Memory(s) => s.drop_before(version, limit),
284			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
285			Self::Sqlite(s) => s.drop_before(version, limit),
286		}
287	}
288
289	pub fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
290		match self {
291			Self::Memory(s) => s.find_ttl_cutoff(cutoff),
292			#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
293			Self::Sqlite(s) => s.find_ttl_cutoff(cutoff),
294		}
295	}
296}
297
298impl CdcStorage for CdcStore {
299	fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
300		CdcStore::write(self, cdc)
301	}
302
303	fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
304		CdcStore::read(self, version)
305	}
306
307	fn read_range(
308		&self,
309		start: Bound<CommitVersion>,
310		end: Bound<CommitVersion>,
311		batch_size: u64,
312	) -> CdcStorageResult<CdcBatch> {
313		CdcStore::read_range(self, start, end, batch_size)
314	}
315
316	fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
317		CdcStore::count(self, version)
318	}
319
320	fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
321		CdcStore::min_version(self)
322	}
323
324	fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
325		CdcStore::max_version(self)
326	}
327
328	fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
329		CdcStore::delete_before(self, version, limit)
330	}
331
332	fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
333		CdcStore::find_ttl_cutoff(self, cutoff)
334	}
335}