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