1pub mod memory;
9#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
10pub mod sqlite;
11
12use std::{
13 collections::{Bound, HashMap},
14 sync,
15};
16
17use memory::MemoryCdcStorage;
18use reifydb_catalog::metrics::storage::parser::parse_id;
19use reifydb_core::{
20 common::CommitVersion,
21 event::metric::CdcEviction,
22 interface::{
23 catalog::metrics::MetricsId,
24 cdc::{Cdc, CdcBatch, SystemChange},
25 },
26};
27use reifydb_runtime::shutdown::Shutdown;
28#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
29use reifydb_sqlite::SqliteConfig;
30use reifydb_value::{byte_size::ByteSize, count::Count, value::datetime::DateTime};
31
32use crate::error::CdcError;
33
34pub type CdcStorageResult<T> = Result<T, CdcError>;
35
36enum ScanContinuation {
37 Done(CommitVersion),
38 Continue(Bound<CommitVersion>),
39}
40
41#[inline]
42fn scan_batch_for_cutoff(items: &[Cdc], cutoff: DateTime) -> Option<CommitVersion> {
43 for cdc in items {
44 if cdc.timestamp >= cutoff {
45 return Some(cdc.version);
46 }
47 }
48 None
49}
50
51#[inline]
52fn next_start_after_batch(batch: &CdcBatch, max: CommitVersion) -> ScanContinuation {
53 if !batch.has_more {
54 return ScanContinuation::Done(CommitVersion(max.0.saturating_add(1)));
55 }
56 let last = batch.items.last().unwrap().version;
57 ScanContinuation::Continue(Bound::Excluded(last))
58}
59
60#[inline]
61pub(crate) fn normalize_range_inclusive(
62 start: Bound<CommitVersion>,
63 end: Bound<CommitVersion>,
64) -> Option<(CommitVersion, CommitVersion)> {
65 let lo_inc = match start {
66 Bound::Included(v) => v,
67 Bound::Excluded(v) => CommitVersion(v.0.saturating_add(1)),
68 Bound::Unbounded => CommitVersion(0),
69 };
70 let hi_inc = match end {
71 Bound::Included(v) => v,
72 Bound::Excluded(v) => CommitVersion(v.0.saturating_sub(1)),
73 Bound::Unbounded => CommitVersion(u64::MAX),
74 };
75 if lo_inc > hi_inc {
76 None
77 } else {
78 Some((lo_inc, hi_inc))
79 }
80}
81
82#[derive(Debug, Clone, Default)]
83pub struct DropBeforeResult {
84 pub count: Count,
85 pub entries: Vec<CdcEviction>,
86 pub more_remaining: bool,
87}
88
89pub(crate) fn aggregate_evictions<'a, I>(system_changes: I) -> Vec<CdcEviction>
92where
93 I: IntoIterator<Item = &'a SystemChange>,
94{
95 let mut by_source: HashMap<MetricsId, CdcEviction> = HashMap::new();
96 for change in system_changes {
97 let key = change.key();
98 let id = parse_id(key.as_ref());
99 let entry = by_source.entry(id).or_insert_with(|| CdcEviction {
100 id,
101 key_bytes: ByteSize::ZERO,
102 value_bytes: ByteSize::ZERO,
103 count: Count::ZERO,
104 });
105 entry.key_bytes = entry.key_bytes.saturating_add(ByteSize::from_bytes(key.as_ref().len() as u64));
106 entry.value_bytes = entry.value_bytes.saturating_add(ByteSize::from_bytes(change.value_bytes() as u64));
107 entry.count = entry.count.saturating_add(Count::new(1));
108 }
109 by_source.into_values().collect()
110}
111
112pub(crate) fn total_evicted_count(evictions: &[CdcEviction]) -> Count {
113 evictions.iter().fold(Count::ZERO, |acc, e| acc.saturating_add(e.count))
114}
115
116#[cfg(not(target_arch = "wasm32"))]
119pub(crate) fn merge_evictions(evictions: Vec<CdcEviction>) -> Vec<CdcEviction> {
120 let mut by_source: HashMap<MetricsId, CdcEviction> = HashMap::new();
121 for e in evictions {
122 let acc = by_source.entry(e.id).or_insert_with(|| CdcEviction {
123 id: e.id,
124 key_bytes: ByteSize::ZERO,
125 value_bytes: ByteSize::ZERO,
126 count: Count::ZERO,
127 });
128 acc.key_bytes = acc.key_bytes.saturating_add(e.key_bytes);
129 acc.value_bytes = acc.value_bytes.saturating_add(e.value_bytes);
130 acc.count = acc.count.saturating_add(e.count);
131 }
132 by_source.into_values().collect()
133}
134
135pub trait CdcStorage: Send + Sync + Clone + 'static {
136 fn write(&self, cdc: &Cdc) -> CdcStorageResult<()>;
137
138 fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>>;
139
140 fn read_range(
141 &self,
142 start: Bound<CommitVersion>,
143 end: Bound<CommitVersion>,
144 batch_size: u64,
145 ) -> CdcStorageResult<CdcBatch>;
146
147 fn count(&self, version: CommitVersion) -> CdcStorageResult<usize>;
148
149 fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>>;
150
151 fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>>;
152
153 fn exists(&self, version: CommitVersion) -> CdcStorageResult<bool> {
154 Ok(self.read(version)?.is_some())
155 }
156
157 fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult>;
158
159 fn truncated_before(&self) -> CdcStorageResult<CommitVersion>;
160
161 fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
162 let Some(min) = self.min_version()? else {
163 return Ok(None);
164 };
165 let Some(max) = self.max_version()? else {
166 return Ok(None);
167 };
168
169 let mut next_start = Bound::Included(min);
170 loop {
171 let batch = self.read_range(next_start, Bound::Unbounded, 256)?;
172 if batch.items.is_empty() {
173 return Ok(Some(CommitVersion(max.0.saturating_add(1))));
174 }
175 if let Some(version) = scan_batch_for_cutoff(&batch.items, cutoff) {
176 return Ok(Some(version));
177 }
178 match next_start_after_batch(&batch, max) {
179 ScanContinuation::Done(v) => return Ok(Some(v)),
180 ScanContinuation::Continue(start) => next_start = start,
181 }
182 }
183 }
184
185 fn range(&self, start: Bound<CommitVersion>, end: Bound<CommitVersion>) -> CdcStorageResult<CdcBatch> {
186 self.read_range(start, end, 1024)
187 }
188
189 fn scan(&self, batch_size: u64) -> CdcStorageResult<CdcBatch> {
190 self.read_range(Bound::Unbounded, Bound::Unbounded, batch_size)
191 }
192}
193
194impl<T: CdcStorage> CdcStorage for sync::Arc<T> {
195 fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
196 (**self).write(cdc)
197 }
198
199 fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
200 (**self).read(version)
201 }
202
203 fn read_range(
204 &self,
205 start: Bound<CommitVersion>,
206 end: Bound<CommitVersion>,
207 batch_size: u64,
208 ) -> CdcStorageResult<CdcBatch> {
209 (**self).read_range(start, end, batch_size)
210 }
211
212 fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
213 (**self).count(version)
214 }
215
216 fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
217 (**self).min_version()
218 }
219
220 fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
221 (**self).max_version()
222 }
223
224 fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
225 (**self).drop_before(version, limit)
226 }
227
228 fn truncated_before(&self) -> CdcStorageResult<CommitVersion> {
229 (**self).truncated_before()
230 }
231
232 fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
233 (**self).find_ttl_cutoff(cutoff)
234 }
235}
236
237#[derive(Clone)]
238pub enum CdcStore {
239 Memory(MemoryCdcStorage),
240
241 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
242 Sqlite(sqlite::storage::SqliteCdcStorage),
243}
244
245#[derive(Clone)]
246pub struct CdcHotReader {
247 store: CdcStore,
248}
249
250impl CdcHotReader {
251 pub fn read_range(
252 &self,
253 start: Bound<CommitVersion>,
254 end: Bound<CommitVersion>,
255 batch_size: u64,
256 ) -> CdcStorageResult<CdcBatch> {
257 match &self.store {
258 CdcStore::Memory(s) => s.read_range(start, end, batch_size),
259 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
260 CdcStore::Sqlite(s) => s.read_range_hot(start, end, batch_size),
261 }
262 }
263}
264
265impl Shutdown for CdcStore {
266 fn shutdown(&self) {
267 match self {
268 Self::Memory(_) => {}
269 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
270 Self::Sqlite(s) => s.shutdown(),
271 }
272 }
273}
274
275impl CdcStore {
276 pub fn memory() -> Self {
277 Self::Memory(MemoryCdcStorage::new())
278 }
279
280 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
281 pub fn sqlite(config: SqliteConfig) -> Self {
282 Self::Sqlite(sqlite::storage::SqliteCdcStorage::new(config))
283 }
284
285 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
286 pub fn sqlite_with_block_cache_capacity(config: SqliteConfig, block_cache_capacity: usize) -> Self {
287 Self::Sqlite(sqlite::storage::SqliteCdcStorage::new_with_cache_capacity(config, block_cache_capacity))
288 }
289
290 pub fn hot_reader(&self) -> CdcHotReader {
291 CdcHotReader {
292 store: self.clone(),
293 }
294 }
295
296 #[cfg_attr(any(not(feature = "sqlite"), target_arch = "wasm32"), allow(unused_variables))]
297 pub fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
298 match self {
299 Self::Memory(s) => s.write(cdc),
300 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
301 Self::Sqlite(s) => s.write(cdc),
302 }
303 }
304
305 pub fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
306 match self {
307 Self::Memory(s) => s.read(version),
308 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
309 Self::Sqlite(s) => s.read(version),
310 }
311 }
312
313 pub fn read_range(
314 &self,
315 start: Bound<CommitVersion>,
316 end: Bound<CommitVersion>,
317 batch_size: u64,
318 ) -> CdcStorageResult<CdcBatch> {
319 match self {
320 Self::Memory(s) => s.read_range(start, end, batch_size),
321 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
322 Self::Sqlite(s) => s.read_range(start, end, batch_size),
323 }
324 }
325
326 pub fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
327 match self {
328 Self::Memory(s) => s.count(version),
329 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
330 Self::Sqlite(s) => s.count(version),
331 }
332 }
333
334 pub fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
335 match self {
336 Self::Memory(s) => s.min_version(),
337 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
338 Self::Sqlite(s) => s.min_version(),
339 }
340 }
341
342 pub fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
343 match self {
344 Self::Memory(s) => s.max_version(),
345 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
346 Self::Sqlite(s) => s.max_version(),
347 }
348 }
349
350 pub fn delete_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
351 match self {
352 Self::Memory(s) => s.drop_before(version, limit),
353 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
354 Self::Sqlite(s) => s.drop_before(version, limit),
355 }
356 }
357
358 pub fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
359 match self {
360 Self::Memory(s) => s.find_ttl_cutoff(cutoff),
361 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
362 Self::Sqlite(s) => s.find_ttl_cutoff(cutoff),
363 }
364 }
365
366 pub fn truncated_before(&self) -> CdcStorageResult<CommitVersion> {
367 match self {
368 Self::Memory(s) => s.truncated_before(),
369 #[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
370 Self::Sqlite(s) => s.truncated_before(),
371 }
372 }
373}
374
375impl CdcStorage for CdcStore {
376 fn write(&self, cdc: &Cdc) -> CdcStorageResult<()> {
377 CdcStore::write(self, cdc)
378 }
379
380 fn read(&self, version: CommitVersion) -> CdcStorageResult<Option<Cdc>> {
381 CdcStore::read(self, version)
382 }
383
384 fn read_range(
385 &self,
386 start: Bound<CommitVersion>,
387 end: Bound<CommitVersion>,
388 batch_size: u64,
389 ) -> CdcStorageResult<CdcBatch> {
390 CdcStore::read_range(self, start, end, batch_size)
391 }
392
393 fn count(&self, version: CommitVersion) -> CdcStorageResult<usize> {
394 CdcStore::count(self, version)
395 }
396
397 fn min_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
398 CdcStore::min_version(self)
399 }
400
401 fn max_version(&self) -> CdcStorageResult<Option<CommitVersion>> {
402 CdcStore::max_version(self)
403 }
404
405 fn drop_before(&self, version: CommitVersion, limit: usize) -> CdcStorageResult<DropBeforeResult> {
406 CdcStore::delete_before(self, version, limit)
407 }
408
409 fn truncated_before(&self) -> CdcStorageResult<CommitVersion> {
410 CdcStore::truncated_before(self)
411 }
412
413 fn find_ttl_cutoff(&self, cutoff: DateTime) -> CdcStorageResult<Option<CommitVersion>> {
414 CdcStore::find_ttl_cutoff(self, cutoff)
415 }
416}