1pub mod distinct;
5pub mod expiry;
6pub mod join;
7pub mod ringbuffer;
8pub mod root;
9pub mod timer;
10pub mod window;
11
12use reifydb_codec::row::{operator::state::OperatorState, pod::EncodedPodRow};
13#[cfg(test)]
14use reifydb_value::util::hash::Hash128;
15
16#[cfg(test)]
17use crate::key::typed::{BoundedKey, DenseKey};
18use crate::{
19 key::{
20 operator::{
21 keyspace::{
22 distinct::{DistinctEntry, DistinctLayout},
23 expiry::{Expiry, ReapQueue, TumblingExpiry},
24 join::{
25 JoinExpiryDue, JoinLeft, JoinPin, JoinPublished, JoinRight, JoinRowExpiry,
26 JoinRowExpiryState, JoinRowExpirySuffix, JoinRowMapping, JoinSchema,
27 join_expiry_due_key,
28 },
29 ringbuffer::{
30 PartitionedRingbufferEntry, PartitionedRingbufferExpiry,
31 PartitionedRingbufferMeta, PartitionedRingbufferTtlArm, RingbufferEntry,
32 RingbufferExpiry, RingbufferForward, RingbufferMeta, RingbufferTtlArm,
33 },
34 root::{
35 CustomNotCached, GateVisibility, GroupRowMapping, GuestRowMapping, NodeCounter,
36 SealLedger, SourceWatermark,
37 },
38 timer::{TimerIndex, TimerWheel},
39 window::{
40 Accumulator, Buffer, Count, Emit, EngineMeta, GuestAccumulator, GuestBuffer,
41 GuestRunning, RollingMeta, RowIndex, Running, Session, WindowMeta,
42 },
43 },
44 state::{GroupId, GroupStateKey, KeyspaceId, OperatorStateKey},
45 traits::{Keyspace, group_scoped},
46 },
47 typed::layout::{KeyColumn, KeyLayout},
48 },
49 state::typed::SuffixBytes,
50};
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct KeyspaceSpec {
54 pub name: &'static str,
55 pub id: KeyspaceId,
56 pub range_cached: bool,
57 pub columns: &'static [KeyColumn],
58 pub suffix: &'static [KeyColumn],
59}
60
61pub const fn columns_width(columns: &[KeyColumn]) -> usize {
62 let mut width = 0;
63 let mut index = 0;
64 while index < columns.len() {
65 width += columns[index].ty.width();
66 index += 1;
67 }
68 width
69}
70
71impl KeyspaceSpec {
72 pub const fn suffix_width(&self) -> usize {
73 columns_width(self.suffix)
74 }
75}
76
77struct GroupScoped;
78
79impl KeyspaceVisitor for GroupScoped {
80 type Output = bool;
81
82 fn visit<K: Keyspace>(self) -> Self::Output {
83 const { group_scoped::<K>() }
84 }
85}
86
87pub fn group_scoped_id(id: KeyspaceId) -> Option<bool> {
90 dispatch(id, GroupScoped)
91}
92
93pub trait KeyspaceVisitor {
94 type Output;
95
96 fn visit<K: Keyspace>(self) -> Self::Output;
97}
98
99macro_rules! catalogue {
100 ($($keyspace:ty),* $(,)?) => {
101 pub const KEYSPACES: &[KeyspaceSpec] = &[
102 $(KeyspaceSpec {
103 name: <$keyspace as Keyspace>::NAME,
104 id: <$keyspace as Keyspace>::ID,
105 range_cached: <$keyspace as Keyspace>::RANGE_CACHED,
106 columns: <<$keyspace as Keyspace>::GroupedKey as KeyLayout>::COLUMNS,
107 suffix: <<$keyspace as Keyspace>::Suffix as KeyLayout>::COLUMNS,
108 }),*
109 ];
110
111 pub const REGISTERED: [u64; 4] = {
112 let mut bits = [0u64; 4];
113 $({
114 let id = <$keyspace as Keyspace>::ID.0;
115 bits[(id >> 6) as usize] |= 1u64 << (id & 63);
116 })*
117 bits
118 };
119
120 pub fn suffix_width_of(id: KeyspaceId) -> Option<usize> {
121 $(if id == <$keyspace as Keyspace>::ID {
122 return Some(columns_width(<<$keyspace as Keyspace>::Suffix as KeyLayout>::COLUMNS));
123 })*
124 None
125 }
126
127 pub fn dispatch<V: KeyspaceVisitor>(id: KeyspaceId, visitor: V) -> Option<V::Output> {
128 $(if id == <$keyspace as Keyspace>::ID {
129 return Some(visitor.visit::<$keyspace>());
130 })*
131 None
132 }
133
134 #[cfg(test)]
135 fn every_keyspace_round_trips() {
136 $(round_trips::<$keyspace>();)*
137 }
138
139 #[cfg(test)]
140 fn every_keyspace_carries_its_group() {
141 $(carries_its_group::<$keyspace>();)*
142 }
143
144 #[cfg(test)]
145 fn group_scoped_keyspaces() -> usize {
146 let mut count = 0;
147 $(if group_scoped::<$keyspace>() {
148 count += 1;
149 })*
150 count
151 }
152 };
153}
154
155catalogue!(
156 Accumulator,
157 Buffer,
158 Running,
159 Count,
160 Session,
161 RollingMeta,
162 EngineMeta,
163 Emit,
164 RowIndex,
165 WindowMeta,
166 GuestAccumulator,
167 GuestBuffer,
168 GuestRunning,
169 JoinLeft,
170 JoinRight,
171 JoinPublished,
172 JoinPin,
173 JoinSchema,
174 JoinRowExpiry,
175 JoinExpiryDue,
176 JoinRowMapping,
177 RingbufferForward,
178 RingbufferEntry,
179 RingbufferExpiry,
180 RingbufferTtlArm,
181 RingbufferMeta,
182 PartitionedRingbufferEntry,
183 PartitionedRingbufferExpiry,
184 PartitionedRingbufferTtlArm,
185 PartitionedRingbufferMeta,
186 TimerWheel,
187 TimerIndex,
188 Expiry,
189 TumblingExpiry,
190 ReapQueue,
191 DistinctEntry,
192 DistinctLayout,
193 SourceWatermark,
194 SealLedger,
195 NodeCounter,
196 GateVisibility,
197 GroupRowMapping,
198 GuestRowMapping,
199 CustomNotCached,
200);
201
202#[derive(Clone, Debug)]
203pub enum RootSibling {
204 Derived(GroupStateKey),
205 OwnerCleared,
206 None,
207}
208
209pub fn root_sibling(group: GroupId, keyspace: KeyspaceId, suffix: &[u8], row: &EncodedPodRow) -> RootSibling {
210 match keyspace {
211 KeyspaceId::JOIN_ROW_EXPIRY => join_row_expiry_sibling(group, suffix, row),
212
213 KeyspaceId::ACCUMULATOR
214 | KeyspaceId::BUFFER
215 | KeyspaceId::RUNNING
216 | KeyspaceId::COUNT
217 | KeyspaceId::SESSION
218 | KeyspaceId::ROLLING_META
219 | KeyspaceId::ENGINE_META
220 | KeyspaceId::EMIT
221 | KeyspaceId::ROW_INDEX
222 | KeyspaceId::WINDOW_META
223 | KeyspaceId::GUEST_ACCUMULATOR
224 | KeyspaceId::GUEST_BUFFER
225 | KeyspaceId::GUEST_RUNNING
226 | KeyspaceId::JOIN_LEFT
227 | KeyspaceId::JOIN_RIGHT => RootSibling::OwnerCleared,
228
229 KeyspaceId::JOIN_PUBLISHED
230 | KeyspaceId::JOIN_PIN
231 | KeyspaceId::JOIN_SCHEMA
232 | KeyspaceId::JOIN_EXPIRY_DUE
233 | KeyspaceId::JOIN_ROW_MAPPING
234 | KeyspaceId::DISTINCT_ENTRY
235 | KeyspaceId::DISTINCT_LAYOUT
236 | KeyspaceId::ROLLING_EXPIRY
237 | KeyspaceId::TUMBLING_EXPIRY
238 | KeyspaceId::REAP_QUEUE
239 | KeyspaceId::SOURCE_WATERMARK
240 | KeyspaceId::SEAL_LEDGER
241 | KeyspaceId::NODE_COUNTER
242 | KeyspaceId::GATE_VISIBILITY
243 | KeyspaceId::GROUP_ROW_MAPPING
244 | KeyspaceId::GUEST_ROW_MAPPING
245 | KeyspaceId::CUSTOM_NOT_CACHED
246 | KeyspaceId::RINGBUFFER_FORWARD
247 | KeyspaceId::RINGBUFFER_ENTRY
248 | KeyspaceId::RINGBUFFER_EXPIRY
249 | KeyspaceId::RINGBUFFER_TTL_ARM
250 | KeyspaceId::RINGBUFFER_META
251 | KeyspaceId::PARTITIONED_RINGBUFFER_ENTRY
252 | KeyspaceId::PARTITIONED_RINGBUFFER_EXPIRY
253 | KeyspaceId::PARTITIONED_RINGBUFFER_TTL_ARM
254 | KeyspaceId::PARTITIONED_RINGBUFFER_META
255 | KeyspaceId::TIMER_WHEEL
256 | KeyspaceId::TIMER_INDEX => RootSibling::None,
257
258 other => panic!(
259 "keyspace {} answers nothing about root siblings; a keyspace that reaches a group sweep \
260 unclassified would leave whatever it points at outside the group orphaned behind a group \
261 id nothing can resolve again",
262 other.name()
263 ),
264 }
265}
266
267fn join_row_expiry_sibling(group: GroupId, suffix: &[u8], row: &EncodedPodRow) -> RootSibling {
268 let Some(suffix) = JoinRowExpirySuffix::from_suffix_bytes(suffix) else {
269 panic!("a join row expiry key carries a suffix that keyspace cannot decode");
270 };
271 let at = match JoinRowExpiryState::decode_state(row) {
272 Ok(state) => state.at,
273 Err(err) => {
274 panic!("a join row expiry row will not decode, so its due index key cannot be derived: {err}")
275 }
276 };
277 RootSibling::Derived(join_expiry_due_key(at, group, suffix.side.0, suffix.row.0))
278}
279
280pub fn root_sibling_of(key: &GroupStateKey, row: &EncodedPodRow) -> Option<RootSibling> {
281 let (group, keyspace, suffix) = OperatorStateKey::decode_inner(key.as_encoded().as_bytes())?;
282 Some(root_sibling(group, keyspace, suffix, row))
283}
284
285#[cfg(test)]
286fn round_trips<K: Keyspace>() {
287 let mut suffix = <K::Suffix as BoundedKey>::low();
291 for step in 0..4 {
292 let key = K::join(GroupId::hashed(Hash128(9)), suffix.clone());
293 let (group, split) = K::split(&key);
294 assert_eq!(split, suffix, "{}: step {step}: a suffix must survive join then split", K::NAME);
295 assert_eq!(
296 K::join(group, split.clone()),
297 key,
298 "{}: step {step}: split then join must return the same key",
299 K::NAME
300 );
301 let (again_group, again_split) = K::split(&K::join(group, split.clone()));
302 assert_eq!(
303 (again_group, again_split),
304 (group, split),
305 "{}: step {step}: a second round trip must not drift, or the container's identity is \
306 lost on every rewrite",
307 K::NAME
308 );
309 match suffix.successor() {
310 Some(next) => suffix = next,
311 None => break,
312 }
313 }
314}
315
316#[cfg(test)]
317fn carries_its_group<K: Keyspace>() {
318 let inside_one_group = group_scoped::<K>();
325 let mut suffix = <K::Suffix as BoundedKey>::low();
326 for step in 0..4 {
327 for group in [
328 GroupId::ROOT,
329 GroupId::hashed(Hash128(1)),
330 GroupId::hashed(Hash128(9)),
331 GroupId::hashed(Hash128(u128::MAX)),
332 ] {
333 let (back, _) = K::split(&K::join(group, suffix.clone()));
334 if inside_one_group {
335 assert_eq!(
336 back,
337 group,
338 "{}: step {step}: a group must survive join then split",
339 K::NAME
340 );
341 } else {
342 assert_eq!(
343 back,
344 GroupId::ROOT,
345 "{}: step {step}: a keyspace with no group column must collapse every group to \
346 ROOT, or its writers believe a group is kept that the key cannot hold",
347 K::NAME
348 );
349 }
350 }
351 if inside_one_group {
352 let distinct = K::join(GroupId::hashed(Hash128(1)), suffix.clone());
353 let other = K::join(GroupId::hashed(Hash128(9)), suffix.clone());
354 assert_ne!(
355 distinct.to_suffix_bytes(),
356 other.to_suffix_bytes(),
357 "{}: step {step}: two groups holding the same suffix must not encode to one primary key",
358 K::NAME
359 );
360 }
361 match suffix.successor() {
362 Some(next) => suffix = next,
363 None => break,
364 }
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use std::collections::HashSet;
371
372 use reifydb_codec::row::operator::state::OperatorState;
373 use reifydb_value::{
374 util::hash::Hash128,
375 value::{datetime::DateTime, row_number::RowNumber},
376 };
377
378 use super::{
379 KEYSPACES, RootSibling, every_keyspace_carries_its_group, every_keyspace_round_trips,
380 group_scoped_keyspaces, root_sibling,
381 };
382 use crate::{
383 key::{
384 operator::{
385 keyspace::join::{JoinRowExpiryState, JoinRowExpirySuffix},
386 state::{GroupId, KeyspaceId},
387 },
388 typed::direction::Asc,
389 },
390 state::typed::SuffixBytes,
391 };
392
393 fn catalogue() -> Vec<(&'static str, KeyspaceId, bool)> {
394 KEYSPACES.iter().map(|spec| (spec.name, spec.id, spec.range_cached)).collect()
395 }
396
397 #[test]
398 fn every_registered_keyspace_answers_what_it_implies_outside_its_group() {
399 let suffix = JoinRowExpirySuffix {
402 side: Asc(0),
403 row: Asc(RowNumber(1)),
404 }
405 .to_suffix_bytes();
406 let row = JoinRowExpiryState {
407 at: DateTime::default(),
408 }
409 .encode_state()
410 .unwrap();
411 let mut derived = Vec::new();
412 for spec in KEYSPACES {
413 if let RootSibling::Derived(_) =
414 root_sibling(GroupId::hashed(Hash128(7)), spec.id, &suffix, &row)
415 {
416 derived.push(spec.name);
417 }
418 }
419 assert_eq!(
420 derived,
421 vec!["JOIN_ROW_EXPIRY"],
422 "only a keyspace whose ROOT key is a function of its own key and row may be reaped by \
423 construction; any other name here claims a derivation it does not have"
424 );
425 }
426
427 #[test]
428 fn every_keyspace_names_and_tiers_itself_the_way_its_id_does() {
429 for (name, id, range_cached) in catalogue() {
433 assert_eq!(name, id.name(), "{name} and its id disagree on the name");
434 assert_eq!(range_cached, id.caches_ranges(), "{name} and its id disagree on the range tier");
435 }
436 }
437
438 #[test]
439 fn no_two_keyspaces_claim_the_same_id() {
440 let mut seen = HashSet::new();
443 for (name, id, _) in catalogue() {
444 assert!(seen.insert(id), "{name} reuses an id another keyspace already claims");
445 }
446 assert_eq!(seen.len(), 44, "the catalogue is forty four keyspaces");
447 }
448
449 #[test]
450 fn join_and_split_are_inverse_for_every_keyspace() {
451 every_keyspace_round_trips();
454 }
455
456 #[test]
457 fn a_group_survives_join_and_split_for_every_keyspace() {
458 every_keyspace_carries_its_group();
463 }
464
465 #[test]
466 fn exactly_twenty_four_of_the_forty_four_keyspaces_are_group_scoped() {
467 assert_eq!(
469 KEYSPACES.len(),
470 44,
471 "a keyspace was added or removed without revisiting the group scope split"
472 );
473 assert_eq!(
474 group_scoped_keyspaces(),
475 24,
476 "a keyspace changed group scope; confirm its key layout meant to"
477 );
478 }
479
480 #[test]
481 fn the_catalogue_covers_every_id_the_substrate_declares() {
482 let source = include_str!("../state.rs");
487 let body = source
488 .split("impl KeyspaceId {")
489 .nth(1)
490 .expect("the KeyspaceId impl block is where the constants are declared");
491 let declared = body
492 .split("\n}\n")
493 .next()
494 .expect("the impl block is closed")
495 .lines()
496 .filter(|line| {
497 let line = line.trim_start();
498 line.starts_with("pub const") && line.contains("Self(")
499 })
500 .count();
501 assert_eq!(
502 catalogue().len(),
503 declared,
504 "the substrate declares {declared} keyspace ids and the catalogue types {}",
505 catalogue().len()
506 );
507 }
508}