1pub 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}