Skip to main content

reifydb_core/key/
cdc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::borrow::Cow;
5
6use reifydb_codec::key::{deserializer::KeyDeserializer, encoded::EncodedKey, serializer::KeySerializer};
7use smallvec::{SmallVec, smallvec};
8
9use super::KeyTag;
10use crate::{
11	interface::{catalog::flow::FlowId, cdc::CdcConsumerId},
12	key::{
13		any::{ByteEncoding, Field, KeyFields},
14		bound::TaggedKeyBoundRange,
15	},
16};
17
18pub trait ToConsumerKey {
19	fn to_consumer_key(&self) -> CdcConsumerKey;
20}
21
22impl ToConsumerKey for CdcConsumerKey {
23	fn to_consumer_key(&self) -> CdcConsumerKey {
24		self.clone()
25	}
26}
27
28impl ToConsumerKey for CdcConsumerId {
29	fn to_consumer_key(&self) -> CdcConsumerKey {
30		CdcConsumerKey {
31			consumer: self.clone(),
32		}
33	}
34}
35
36impl ToConsumerKey for FlowId {
37	fn to_consumer_key(&self) -> CdcConsumerKey {
38		CdcConsumerKey::new(CdcConsumerId::new(format!("flow:{}", self.0)))
39	}
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub struct CdcConsumerKey {
44	pub consumer: CdcConsumerId,
45}
46
47impl CdcConsumerKey {
48	pub fn new(consumer: impl Into<CdcConsumerId>) -> Self {
49		Self {
50			consumer: consumer.into(),
51		}
52	}
53
54	pub fn encoded(consumer: impl Into<CdcConsumerId>) -> EncodedKey {
55		Self {
56			consumer: consumer.into(),
57		}
58		.encode()
59	}
60
61	pub fn full_scan() -> TaggedKeyBoundRange {
62		TaggedKeyBoundRange::kind(Self::TAG)
63	}
64}
65
66impl CdcConsumerKey {
67	pub const TAG: KeyTag = KeyTag::CdcConsumer;
68
69	pub fn encode(&self) -> EncodedKey {
70		let mut serializer = KeySerializer::new();
71		serializer.extend_u8(Self::TAG as u8).extend_str(&self.consumer);
72		serializer.to_encoded_key()
73	}
74
75	pub fn decode(key: &EncodedKey) -> Option<Self>
76	where
77		Self: Sized,
78	{
79		let mut de = KeyDeserializer::from_bytes(key.as_slice());
80
81		let kind: KeyTag = de.read_u8().ok()?.try_into().ok()?;
82		if kind != Self::TAG {
83			return None;
84		}
85
86		let consumer_id = de.read_str().ok()?;
87
88		Some(Self {
89			consumer: CdcConsumerId(consumer_id),
90		})
91	}
92}
93
94#[cfg(test)]
95pub mod cdc_consumer_key_tests {
96	use std::ops::RangeBounds;
97
98	use super::{CdcConsumerKey, ToConsumerKey};
99	use crate::interface::{catalog::flow::FlowId, cdc::CdcConsumerId};
100
101	#[test]
102	fn test_encode_decode_cdc_consumer() {
103		let key = CdcConsumerKey {
104			consumer: CdcConsumerId::new("test-consumer"),
105		};
106
107		let encoded = key.encode();
108		let decoded = CdcConsumerKey::decode(&encoded).expect("Failed to decode key");
109
110		assert_eq!(decoded.consumer, CdcConsumerId::new("test-consumer"));
111	}
112
113	#[test]
114	fn test_cdc_consumer_keys_within_range() {
115		let key1 = CdcConsumerKey {
116			consumer: CdcConsumerId::new("consumer-a"),
117		}
118		.encode();
119
120		let key2 = CdcConsumerKey {
121			consumer: CdcConsumerId::new("consumer-b"),
122		}
123		.encode();
124
125		let key3 = CdcConsumerKey {
126			consumer: CdcConsumerId::new("consumer-z"),
127		}
128		.encode();
129
130		let range = CdcConsumerKey::full_scan().encode();
131
132		assert!(range.contains(&key1), "consumer-a key should be in range");
133		assert!(range.contains(&key2), "consumer-b key should be in range");
134		assert!(range.contains(&key3), "consumer-z key should be in range");
135	}
136
137	#[test]
138	fn test_flow_id_to_consumer_key() {
139		let flow_id = FlowId(42);
140		let encoded = flow_id.to_consumer_key().encode();
141
142		let decoded = CdcConsumerKey::decode(&encoded).expect("Failed to decode key");
143		assert_eq!(decoded.consumer, CdcConsumerId::new("flow:42"));
144	}
145
146	#[test]
147	fn test_flow_id_keys_within_range() {
148		let flow1 = FlowId(1).to_consumer_key().encode();
149		let flow2 = FlowId(100).to_consumer_key().encode();
150		let flow3 = FlowId(999).to_consumer_key().encode();
151
152		let range = CdcConsumerKey::full_scan().encode();
153
154		assert!(range.contains(&flow1), "flow:1 key should be in range");
155		assert!(range.contains(&flow2), "flow:100 key should be in range");
156		assert!(range.contains(&flow3), "flow:999 key should be in range");
157	}
158}
159
160pub fn should_exclude_from_cdc(kind: KeyTag) -> bool {
161	matches!(
162		kind,
163		KeyTag::OperatorState
164			| KeyTag::CdcConsumer | KeyTag::Metric
165			| KeyTag::SystemSequence
166			| KeyTag::RowSequence | KeyTag::ColumnSequence
167			| KeyTag::SystemVersion
168			| KeyTag::TransactionVersion
169			| KeyTag::FlowVersion | KeyTag::RingBufferMetadata
170			| KeyTag::Index | KeyTag::ConfigStorage
171			| KeyTag::Token | KeyTag::VersionEpoch
172			| KeyTag::QueuePartition
173			| KeyTag::QueueItemState
174			| KeyTag::QueueDue | KeyTag::QueueKeyActive
175	)
176}
177
178#[cfg(test)]
179pub mod primary_key_tests {
180	use super::*;
181
182	#[test]
183	fn test_all_key_kinds_have_explicit_cdc_decision() {
184		// The exhaustive match forces every new KeyTag to make a CDC-exclusion decision instead of
185		// silently defaulting into the log.
186
187		let test_variant = KeyTag::Row;
188
189		match test_variant {
190			KeyTag::Namespace => {}
191			KeyTag::Table => {}
192			KeyTag::Row => {}
193			KeyTag::NamespaceTable => {}
194			KeyTag::SystemSequence => {}
195			KeyTag::Columns => {}
196			KeyTag::Column => {}
197			KeyTag::RowSequence => {}
198			KeyTag::ColumnProperty => {}
199			KeyTag::SystemVersion => {}
200			KeyTag::TransactionVersion => {}
201			KeyTag::Index => {}
202			KeyTag::IndexEntry => {}
203			KeyTag::ColumnSequence => {}
204			KeyTag::CdcConsumer => {}
205			KeyTag::View => {}
206			KeyTag::NamespaceView => {}
207			KeyTag::PrimaryKey => {}
208			KeyTag::OperatorState => {}
209			KeyTag::RingBuffer => {}
210			KeyTag::NamespaceRingBuffer => {}
211			KeyTag::RingBufferMetadata => {}
212			KeyTag::Flow => {}
213			KeyTag::NamespaceFlow => {}
214			KeyTag::Operator => {}
215			KeyTag::OperatorByFlow => {}
216			KeyTag::FlowEdge => {}
217			KeyTag::FlowEdgeByFlow => {}
218			KeyTag::OutputFrontier => {}
219			KeyTag::Dictionary => {}
220			KeyTag::DictionaryEntry => {}
221			KeyTag::DictionaryEntryIndex => {}
222			KeyTag::NamespaceDictionary => {}
223			KeyTag::Metric => {}
224			KeyTag::FlowVersion => {}
225			KeyTag::RowShape => {}
226			KeyTag::SumType => {}
227			KeyTag::NamespaceSumType => {}
228			KeyTag::RowShapeField => {}
229			KeyTag::Handler => {}
230			KeyTag::NamespaceHandler => {}
231			KeyTag::VariantHandler => {}
232			KeyTag::Series => {}
233			KeyTag::NamespaceSeries => {}
234			KeyTag::SeriesMetadata => {}
235			KeyTag::Identity => {}
236			KeyTag::Role => {}
237			KeyTag::GrantedRole => {}
238			KeyTag::Policy => {}
239			KeyTag::PolicyOp => {}
240			KeyTag::Migration => {}
241			KeyTag::Authentication => {}
242			KeyTag::MigrationEvent => {}
243			KeyTag::ConfigStorage => {}
244			KeyTag::Token => {}
245			KeyTag::Source => {}
246			KeyTag::NamespaceSource => {}
247			KeyTag::Sink => {}
248			KeyTag::NamespaceSink => {}
249			KeyTag::RowSettings => {}
250			KeyTag::Procedure => {}
251			KeyTag::NamespaceProcedure => {}
252			KeyTag::ProcedureParam => {}
253			KeyTag::Binding => {}
254			KeyTag::OperatorSettings => {}
255			KeyTag::NamespaceBinding => {}
256			KeyTag::ColumnSnapshot => {}
257			KeyTag::SeriesColumnSnapshot => {}
258			KeyTag::TableColumnSnapshot => {}
259			KeyTag::IdentityAttribute => {}
260			KeyTag::IdentityAttributeValue => {}
261			KeyTag::PartitionedRow => {}
262			KeyTag::PartitionedSeriesRow => {}
263			KeyTag::Partition => {}
264			KeyTag::Queue => {}
265			KeyTag::NamespaceQueue => {}
266			KeyTag::QueueDeduplication => {}
267			KeyTag::QueuePartition => {}
268			KeyTag::QueueItemState => {}
269			KeyTag::QueueDue => {}
270			KeyTag::QueueAttempt => {}
271			KeyTag::QueueKeyActive => {}
272			KeyTag::VersionEpoch => {}
273			KeyTag::SeriesRow => {}
274			KeyTag::SortedViewRow => {}
275			KeyTag::PartitionedSortedViewRow => {}
276			KeyTag::Relationship => {} /* When adding a new variant, add it here.
277			                            * The compiler will error if you forget.
278			                            * Then add a test and update should_exclude_from_cdc() if
279			                            * needed. */
280		}
281	}
282
283	#[test]
284	fn test_sorted_view_rows_reach_the_cdc_log() {
285		// A sorted view's rows carry their own kind, and a subscriber that never sees them reads a
286		// view that silently stops changing.
287		assert!(!should_exclude_from_cdc(KeyTag::SortedViewRow));
288		assert!(!should_exclude_from_cdc(KeyTag::PartitionedSortedViewRow));
289	}
290
291	#[test]
292	fn test_exclude_operator_state() {
293		assert!(should_exclude_from_cdc(KeyTag::OperatorState));
294	}
295
296	#[test]
297	fn test_exclude_cdc_consumer() {
298		assert!(should_exclude_from_cdc(KeyTag::CdcConsumer));
299	}
300
301	#[test]
302	fn test_exclude_storage_tracker() {
303		assert!(should_exclude_from_cdc(KeyTag::Metric));
304	}
305
306	#[test]
307	fn test_exclude_system_sequence() {
308		assert!(should_exclude_from_cdc(KeyTag::SystemSequence));
309	}
310
311	#[test]
312	fn test_exclude_row_sequence() {
313		assert!(should_exclude_from_cdc(KeyTag::RowSequence));
314	}
315
316	#[test]
317	fn test_exclude_column_sequence() {
318		assert!(should_exclude_from_cdc(KeyTag::ColumnSequence));
319	}
320
321	#[test]
322	fn test_exclude_system_version() {
323		assert!(should_exclude_from_cdc(KeyTag::SystemVersion));
324	}
325
326	#[test]
327	fn test_exclude_transaction_version() {
328		assert!(should_exclude_from_cdc(KeyTag::TransactionVersion));
329	}
330
331	#[test]
332	fn test_exclude_ring_buffer_metadata() {
333		assert!(should_exclude_from_cdc(KeyTag::RingBufferMetadata));
334	}
335
336	#[test]
337	fn test_exclude_index() {
338		assert!(should_exclude_from_cdc(KeyTag::Index));
339	}
340
341	#[test]
342	fn test_include_namespace() {
343		assert!(!should_exclude_from_cdc(KeyTag::Namespace));
344	}
345
346	#[test]
347	fn test_include_table() {
348		assert!(!should_exclude_from_cdc(KeyTag::Table));
349	}
350
351	#[test]
352	fn test_include_row() {
353		assert!(!should_exclude_from_cdc(KeyTag::Row));
354	}
355
356	#[test]
357	fn test_include_series_row() {
358		// Series rows rode into the log under KeyTag::Row, so their own kind must keep them there.
359		assert!(!should_exclude_from_cdc(KeyTag::SeriesRow));
360	}
361
362	#[test]
363	fn test_include_partitioned_row() {
364		assert!(!should_exclude_from_cdc(KeyTag::PartitionedRow));
365	}
366
367	#[test]
368	fn test_include_partition() {
369		assert!(!should_exclude_from_cdc(KeyTag::Partition));
370	}
371
372	#[test]
373	fn test_include_namespace_table() {
374		assert!(!should_exclude_from_cdc(KeyTag::NamespaceTable));
375	}
376
377	#[test]
378	fn test_include_columns() {
379		assert!(!should_exclude_from_cdc(KeyTag::Columns));
380	}
381
382	#[test]
383	fn test_include_column() {
384		assert!(!should_exclude_from_cdc(KeyTag::Column));
385	}
386
387	#[test]
388	fn test_include_column_property() {
389		assert!(!should_exclude_from_cdc(KeyTag::ColumnProperty));
390	}
391
392	#[test]
393	fn test_include_index_entry() {
394		assert!(!should_exclude_from_cdc(KeyTag::IndexEntry));
395	}
396
397	#[test]
398	fn test_include_view() {
399		assert!(!should_exclude_from_cdc(KeyTag::View));
400	}
401
402	#[test]
403	fn test_include_namespace_view() {
404		assert!(!should_exclude_from_cdc(KeyTag::NamespaceView));
405	}
406
407	#[test]
408	fn test_include_primary_key() {
409		assert!(!should_exclude_from_cdc(KeyTag::PrimaryKey));
410	}
411
412	#[test]
413	fn test_include_ring_buffer() {
414		assert!(!should_exclude_from_cdc(KeyTag::RingBuffer));
415	}
416
417	#[test]
418	fn test_include_namespace_ring_buffer() {
419		assert!(!should_exclude_from_cdc(KeyTag::NamespaceRingBuffer));
420	}
421
422	#[test]
423	fn test_include_queue() {
424		assert!(!should_exclude_from_cdc(KeyTag::Queue));
425	}
426
427	#[test]
428	fn test_include_namespace_queue() {
429		assert!(!should_exclude_from_cdc(KeyTag::NamespaceQueue));
430	}
431
432	#[test]
433	fn test_include_queue_deduplication() {
434		assert!(!should_exclude_from_cdc(KeyTag::QueueDeduplication));
435	}
436
437	#[test]
438	fn test_exclude_queue_partition() {
439		assert!(should_exclude_from_cdc(KeyTag::QueuePartition));
440	}
441
442	#[test]
443	fn test_exclude_queue_item_state() {
444		assert!(should_exclude_from_cdc(KeyTag::QueueItemState));
445	}
446
447	#[test]
448	fn test_exclude_queue_due() {
449		assert!(should_exclude_from_cdc(KeyTag::QueueDue));
450	}
451
452	#[test]
453	fn test_exclude_queue_key_active() {
454		assert!(should_exclude_from_cdc(KeyTag::QueueKeyActive));
455	}
456
457	#[test]
458	fn test_include_queue_attempt() {
459		// Attempt records are the durable audit trail of what a worker reported, not internal
460		// scheduling churn. Excluding them would make every ack invisible to subscribers and
461		// to any downstream view built on effect outcomes.
462		assert!(!should_exclude_from_cdc(KeyTag::QueueAttempt));
463	}
464
465	#[test]
466	fn test_include_operator_settings() {
467		assert!(!should_exclude_from_cdc(KeyTag::OperatorSettings));
468	}
469
470	#[test]
471	fn test_include_flow() {
472		assert!(!should_exclude_from_cdc(KeyTag::Flow));
473	}
474
475	#[test]
476	fn test_include_namespace_flow() {
477		assert!(!should_exclude_from_cdc(KeyTag::NamespaceFlow));
478	}
479
480	#[test]
481	fn test_include_operator() {
482		assert!(!should_exclude_from_cdc(KeyTag::Operator));
483	}
484
485	#[test]
486	fn test_include_operator_by_flow() {
487		assert!(!should_exclude_from_cdc(KeyTag::OperatorByFlow));
488	}
489
490	#[test]
491	fn test_include_flow_edge() {
492		assert!(!should_exclude_from_cdc(KeyTag::FlowEdge));
493	}
494
495	#[test]
496	fn test_include_flow_edge_by_flow() {
497		assert!(!should_exclude_from_cdc(KeyTag::FlowEdgeByFlow));
498	}
499
500	#[test]
501	fn test_include_dictionary() {
502		assert!(!should_exclude_from_cdc(KeyTag::Dictionary));
503	}
504
505	#[test]
506	fn test_include_dictionary_entry() {
507		assert!(!should_exclude_from_cdc(KeyTag::DictionaryEntry));
508	}
509
510	#[test]
511	fn test_include_dictionary_entry_index() {
512		assert!(!should_exclude_from_cdc(KeyTag::DictionaryEntryIndex));
513	}
514
515	#[test]
516	fn test_include_namespace_dictionary() {
517		assert!(!should_exclude_from_cdc(KeyTag::NamespaceDictionary));
518	}
519
520	#[test]
521	fn test_include_handler() {
522		assert!(!should_exclude_from_cdc(KeyTag::Handler));
523	}
524
525	#[test]
526	fn test_include_namespace_handler() {
527		assert!(!should_exclude_from_cdc(KeyTag::NamespaceHandler));
528	}
529
530	#[test]
531	fn test_include_variant_handler() {
532		assert!(!should_exclude_from_cdc(KeyTag::VariantHandler));
533	}
534
535	#[test]
536	fn test_include_shape() {
537		assert!(!should_exclude_from_cdc(KeyTag::RowShape));
538	}
539
540	#[test]
541	fn test_include_sum_type() {
542		assert!(!should_exclude_from_cdc(KeyTag::SumType));
543	}
544
545	#[test]
546	fn test_include_namespace_sum_type() {
547		assert!(!should_exclude_from_cdc(KeyTag::NamespaceSumType));
548	}
549
550	#[test]
551	fn test_include_shape_field() {
552		assert!(!should_exclude_from_cdc(KeyTag::RowShapeField));
553	}
554
555	#[test]
556	fn test_include_series() {
557		assert!(!should_exclude_from_cdc(KeyTag::Series));
558	}
559
560	#[test]
561	fn test_include_namespace_series() {
562		assert!(!should_exclude_from_cdc(KeyTag::NamespaceSeries));
563	}
564
565	#[test]
566	fn test_include_series_metadata() {
567		assert!(!should_exclude_from_cdc(KeyTag::SeriesMetadata));
568	}
569
570	#[test]
571	fn test_include_identity() {
572		assert!(!should_exclude_from_cdc(KeyTag::Identity));
573	}
574
575	#[test]
576	fn test_include_role() {
577		assert!(!should_exclude_from_cdc(KeyTag::Role));
578	}
579
580	#[test]
581	fn test_include_granted_role() {
582		assert!(!should_exclude_from_cdc(KeyTag::GrantedRole));
583	}
584
585	#[test]
586	fn test_include_identity_attribute() {
587		assert!(!should_exclude_from_cdc(KeyTag::IdentityAttribute));
588	}
589
590	#[test]
591	fn test_include_identity_attribute_value() {
592		assert!(!should_exclude_from_cdc(KeyTag::IdentityAttributeValue));
593	}
594
595	#[test]
596	fn test_include_authentication() {
597		assert!(!should_exclude_from_cdc(KeyTag::Authentication));
598	}
599
600	#[test]
601	fn test_include_policy() {
602		assert!(!should_exclude_from_cdc(KeyTag::Policy));
603	}
604
605	#[test]
606	fn test_include_policy_op() {
607		assert!(!should_exclude_from_cdc(KeyTag::PolicyOp));
608	}
609
610	#[test]
611	fn test_include_migration() {
612		assert!(!should_exclude_from_cdc(KeyTag::Migration));
613	}
614
615	#[test]
616	fn test_include_migration_event() {
617		assert!(!should_exclude_from_cdc(KeyTag::MigrationEvent));
618	}
619
620	#[test]
621	fn test_exclude_flow_version() {
622		assert!(should_exclude_from_cdc(KeyTag::FlowVersion));
623	}
624
625	#[test]
626	fn test_exclude_config() {
627		assert!(should_exclude_from_cdc(KeyTag::ConfigStorage));
628	}
629
630	#[test]
631	fn test_exclude_version_epoch() {
632		assert!(should_exclude_from_cdc(KeyTag::VersionEpoch));
633	}
634}
635
636impl KeyFields for CdcConsumerKey {
637	fn fields(&self) -> SmallVec<[Field<'_>; 6]> {
638		smallvec![Field::BytesDesc(ByteEncoding::Escaped, Cow::Borrowed(self.consumer.as_ref().as_bytes()))]
639	}
640}