Skip to main content

reifydb_core/key/
cdc.rs

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