Skip to main content

reifydb_core/interface/
cdc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	collections::{BTreeMap, BTreeSet},
6	ops::Deref,
7	sync::{Arc, OnceLock},
8};
9
10use reifydb_codec::{key::encoded::EncodedKey, row::bytes::EncodedBytes};
11use reifydb_value::value::{datetime::DateTime, row_number::RowNumber};
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13
14use crate::{
15	common::{ChangeVersion, CommitVersion},
16	interface::{catalog::object::ObjectId, change::Change},
17};
18
19#[repr(transparent)]
20#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)]
21pub struct CdcConsumerId(pub(crate) String);
22
23impl CdcConsumerId {
24	const FLOW: &'static str = "__FLOW_COORDINATOR";
25	const SUBSCRIPTION: &'static str = "__SUBSCRIPTION_CONSUMER";
26
27	pub fn new(id: impl Into<String>) -> Self {
28		let id = id.into();
29		assert_ne!(id, Self::FLOW);
30		assert_ne!(id, Self::SUBSCRIPTION);
31		Self(id)
32	}
33
34	pub fn flow_consumer() -> Self {
35		Self(Self::FLOW.to_string())
36	}
37
38	pub fn subscription_consumer() -> Self {
39		Self(Self::SUBSCRIPTION.to_string())
40	}
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ConsumerClass {
45	Pinning,
46	Ephemeral,
47}
48
49impl ConsumerClass {
50	pub fn encode(self) -> u8 {
51		match self {
52			Self::Pinning => 0,
53			Self::Ephemeral => 1,
54		}
55	}
56
57	pub fn decode(byte: u8) -> Option<Self> {
58		match byte {
59			0 => Some(Self::Pinning),
60			1 => Some(Self::Ephemeral),
61			_ => None,
62		}
63	}
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum CheckpointState {
68	Valid,
69	Invalidated,
70}
71
72impl CheckpointState {
73	pub fn encode(self) -> u8 {
74		match self {
75			Self::Valid => 0,
76			Self::Invalidated => 1,
77		}
78	}
79
80	pub fn decode(byte: u8) -> Option<Self> {
81		match byte {
82			0 => Some(Self::Valid),
83			1 => Some(Self::Invalidated),
84			_ => None,
85		}
86	}
87}
88
89impl AsRef<str> for CdcConsumerId {
90	fn as_ref(&self) -> &str {
91		&self.0
92	}
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub enum CdcChange {
97	Insert {
98		key: EncodedKey,
99		post: EncodedBytes,
100	},
101	Update {
102		key: EncodedKey,
103		pre: EncodedBytes,
104		post: EncodedBytes,
105	},
106	Delete {
107		key: EncodedKey,
108		pre: Option<EncodedBytes>,
109		visible: bool,
110	},
111}
112
113impl CdcChange {
114	pub fn key(&self) -> &EncodedKey {
115		match self {
116			CdcChange::Insert {
117				key,
118				..
119			} => key,
120			CdcChange::Update {
121				key,
122				..
123			} => key,
124			CdcChange::Delete {
125				key,
126				..
127			} => key,
128		}
129	}
130
131	pub fn value_bytes(&self) -> usize {
132		match self {
133			CdcChange::Insert {
134				post,
135				..
136			} => post.len(),
137			CdcChange::Update {
138				pre,
139				post,
140				..
141			} => pre.len() + post.len(),
142			CdcChange::Delete {
143				pre,
144				..
145			} => pre.as_ref().map(|p| p.len()).unwrap_or(0),
146		}
147	}
148}
149
150#[derive(Debug, Clone)]
151pub struct Cdc(Arc<CdcRecord>);
152
153#[derive(Debug, Serialize, Deserialize)]
154pub struct CdcRecord {
155	pub version: ChangeVersion,
156	pub timestamp: DateTime,
157
158	pub changes: Vec<CdcChange>,
159
160	#[serde(skip)]
161	changed_objects: OnceLock<BTreeSet<ObjectId>>,
162
163	#[serde(skip)]
164	lift: OnceLock<CdcLift>,
165}
166
167#[derive(Debug, Clone, Default)]
168pub struct CdcLift {
169	pub objects: BTreeMap<ObjectId, ObjectLift>,
170}
171
172#[derive(Debug, Clone, Default)]
173pub struct ObjectLift {
174	pub rows: Vec<(usize, RowNumber)>,
175	pub first_missing_pre: Option<usize>,
176	pub change: OnceLock<Option<Change>>,
177}
178
179impl Cdc {
180	pub fn new(version: ChangeVersion, timestamp: DateTime, changes: Vec<CdcChange>) -> Self {
181		Self(Arc::new(CdcRecord {
182			version,
183			timestamp,
184			changes,
185			changed_objects: OnceLock::new(),
186			lift: OnceLock::new(),
187		}))
188	}
189
190	pub fn changed_objects_with(&self, compute: impl FnOnce(&Cdc) -> BTreeSet<ObjectId>) -> &BTreeSet<ObjectId> {
191		self.0.changed_objects.get_or_init(|| compute(self))
192	}
193
194	pub fn lift_with(&self, build: impl FnOnce(&Cdc) -> CdcLift) -> &CdcLift {
195		self.0.lift.get_or_init(|| build(self))
196	}
197}
198
199impl Deref for Cdc {
200	type Target = CdcRecord;
201
202	fn deref(&self) -> &CdcRecord {
203		&self.0
204	}
205}
206
207impl Serialize for Cdc {
208	fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
209		self.0.serialize(serializer)
210	}
211}
212
213impl<'de> Deserialize<'de> for Cdc {
214	fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
215		CdcRecord::deserialize(deserializer).map(|record| Self(Arc::new(record)))
216	}
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct ConsumerState {
221	pub consumer_id: CdcConsumerId,
222	pub checkpoint: CommitVersion,
223}
224
225#[derive(Debug, Clone)]
226pub struct CdcBatch {
227	pub items: Vec<Cdc>,
228
229	pub has_more: bool,
230}
231
232impl CdcBatch {
233	pub fn empty() -> Self {
234		Self {
235			items: Vec::new(),
236			has_more: false,
237		}
238	}
239
240	pub fn is_empty(&self) -> bool {
241		self.items.is_empty()
242	}
243}
244
245#[cfg(test)]
246mod tests {
247	use std::cell::Cell;
248
249	use super::*;
250	use crate::interface::catalog::id::TableId;
251
252	#[test]
253	fn changed_objects_are_computed_once_per_commit() {
254		// Every flow actor asks the same shared commit for its objects; a second compute would redo the key
255		// decode per flow.
256		let cdc = Cdc::new(ChangeVersion::from(CommitVersion(1)), DateTime::from_nanos(1), Vec::new());
257		let calls = Cell::new(0);
258		let first = BTreeSet::from([ObjectId::Table(TableId(1))]);
259
260		let a = cdc.changed_objects_with(|_| {
261			calls.set(calls.get() + 1);
262			first.clone()
263		});
264		assert_eq!(a, &first);
265		let b = cdc.changed_objects_with(|_| {
266			calls.set(calls.get() + 1);
267			BTreeSet::from([ObjectId::Table(TableId(2))])
268		});
269
270		assert_eq!(calls.get(), 1, "the second call recomputed instead of reading the memo");
271		assert_eq!(b, &first, "the memoised set was replaced by the second compute");
272	}
273}