Skip to main content

reifydb_core/key/
queue.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_codec::key::encoded::EncodedKey;
5use reifydb_macro::KeyCodec;
6use reifydb_value::value::{datetime::DateTime, row_number::RowNumber};
7
8use super::KeyTag;
9use crate::{
10	interface::catalog::id::QueueId,
11	key::{
12		any::{ByteEncoding, Field, KeyFields, Width},
13		bound::TaggedKeyBoundRange,
14	},
15};
16
17#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
18#[key(tag = Queue)]
19pub struct QueueKey {
20	pub queue: QueueId,
21}
22
23impl QueueKey {
24	pub fn new(queue: QueueId) -> Self {
25		Self {
26			queue,
27		}
28	}
29
30	pub fn encoded(queue: impl Into<QueueId>) -> EncodedKey {
31		Self::new(queue.into()).encode()
32	}
33
34	pub fn full_scan() -> TaggedKeyBoundRange {
35		TaggedKeyBoundRange::kind(Self::TAG)
36	}
37}
38
39#[cfg(test)]
40mod queue_key_tests {
41	use std::ops::Bound;
42
43	use reifydb_codec::key::serializer::KeySerializer;
44
45	use super::*;
46
47	#[test]
48	fn test_encode_decode_roundtrip() {
49		// A queue def row is addressed by this key alone, so a broken codec orphans every definition.
50		let encoded = QueueKey::encoded(QueueId(42));
51		let decoded = QueueKey::decode(&encoded).unwrap();
52		assert_eq!(decoded.queue, QueueId(42));
53	}
54
55	#[test]
56	fn test_decode_rejects_foreign_kind() {
57		// The kind byte guards the family: a foreign key must fail rather than have its payload
58		// reinterpreted as a queue id.
59		let mut serializer = KeySerializer::with_capacity(9);
60		serializer.extend_u8(KeyTag::NamespaceQueue as u8).extend_u64(7u64);
61		assert!(QueueKey::decode(&serializer.to_encoded_key()).is_none());
62	}
63
64	#[test]
65	fn test_full_scan_brackets_every_queue_key() {
66		// Keys are stored bitwise-inverted, so byte order runs opposite to the logical value; that is
67		// why the range ends at TAG - 1. Reversing the bound makes list_queues return nothing.
68		let range = QueueKey::full_scan().encode();
69
70		let Bound::Included(start) = &range.start else {
71			panic!("expected an included start bound")
72		};
73		let Bound::Included(end) = &range.end else {
74			panic!("expected an included end bound")
75		};
76
77		assert_eq!(start.as_slice(), &[!(KeyTag::Queue as u8)]);
78		assert_eq!(end.as_slice(), &[!(KeyTag::Queue as u8 - 1)]);
79		assert!(start.as_slice() < end.as_slice(), "the range must be non-empty under byte order");
80
81		for id in [QueueId(1), QueueId(u64::MAX)] {
82			let key = QueueKey::encoded(id);
83			assert!(
84				key.as_slice() >= start.as_slice() && key.as_slice() <= end.as_slice(),
85				"queue {id:?} must fall inside the scan range"
86			);
87		}
88	}
89
90	#[test]
91	fn test_full_scan_excludes_the_neighbouring_kind() {
92		// A neighbouring key family inside the range would let a full scan decode foreign rows as
93		// queue definitions.
94		let range = QueueKey::full_scan().encode();
95		let Bound::Included(start) = &range.start else {
96			panic!("expected an included start bound")
97		};
98		let Bound::Included(end) = &range.end else {
99			panic!("expected an included end bound")
100		};
101
102		let mut serializer = KeySerializer::with_capacity(9);
103		serializer.extend_u8(KeyTag::NamespaceQueue as u8).extend_u64(1u64);
104		let foreign = serializer.to_encoded_key();
105
106		assert!(
107			foreign.as_slice() < start.as_slice() || foreign.as_slice() > end.as_slice(),
108			"a NamespaceQueue key must fall outside the QueueKey scan range"
109		);
110	}
111}
112
113#[cfg(test)]
114mod byte_identical_check_queue_key {
115	use reifydb_codec::key::serializer::KeySerializer;
116
117	use super::*;
118
119	fn legacy_encode(key: &QueueKey) -> EncodedKey {
120		let mut serializer = KeySerializer::with_capacity(9);
121		serializer.extend_u8(KeyTag::Queue as u8).extend_u64(key.queue);
122		serializer.to_encoded_key()
123	}
124
125	#[test]
126	fn matches_legacy_byte_layout() {
127		for id in [QueueId(0), QueueId(1), QueueId(u64::MAX)] {
128			let key = QueueKey {
129				queue: id,
130			};
131			assert_eq!(legacy_encode(&key).as_slice(), key.encode().as_slice());
132		}
133	}
134}
135
136#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
137#[key(tag = QueueAttempt)]
138pub struct QueueAttemptKey {
139	pub queue: QueueId,
140	pub row: RowNumber,
141	pub attempt: u32,
142}
143
144impl QueueAttemptKey {
145	pub fn new(queue: impl Into<QueueId>, row: impl Into<RowNumber>, attempt: u32) -> Self {
146		Self {
147			queue: queue.into(),
148			row: row.into(),
149			attempt,
150		}
151	}
152
153	pub fn encoded(queue: impl Into<QueueId>, row: impl Into<RowNumber>, attempt: u32) -> EncodedKey {
154		Self::new(queue, row, attempt).encode()
155	}
156
157	pub fn item_scan(queue: QueueId, row: RowNumber) -> TaggedKeyBoundRange {
158		TaggedKeyBoundRange::prefix(
159			Self::TAG,
160			[Field::UDesc(Width::U64, queue.0 as u128), Field::UDesc(Width::U64, row.0 as u128)],
161		)
162	}
163
164	pub fn queue_scan(queue: QueueId) -> TaggedKeyBoundRange {
165		TaggedKeyBoundRange::prefix(Self::TAG, [Field::UDesc(Width::U64, queue.0 as u128)])
166	}
167
168	pub fn full_scan() -> TaggedKeyBoundRange {
169		TaggedKeyBoundRange::kind(Self::TAG)
170	}
171}
172
173#[cfg(test)]
174mod queue_item_state_key_tests {
175	use std::ops::Bound;
176
177	use reifydb_codec::key::encoded::EncodedKeyRange;
178
179	use super::*;
180
181	fn contains(range: &EncodedKeyRange, key: &EncodedKey) -> bool {
182		let after_start = match &range.start {
183			Bound::Included(start) => key.as_slice() >= start.as_slice(),
184			Bound::Excluded(start) => key.as_slice() > start.as_slice(),
185			Bound::Unbounded => true,
186		};
187		let before_end = match &range.end {
188			Bound::Included(end) => key.as_slice() <= end.as_slice(),
189			Bound::Excluded(end) => key.as_slice() < end.as_slice(),
190			Bound::Unbounded => true,
191		};
192		after_start && before_end
193	}
194
195	#[test]
196	fn test_attempt_key_roundtrips() {
197		// Attempt is the CAS discriminator the whole ack path turns on: a lost or widened
198		// attempt component would let attempt 2's record answer for attempt 1, which is
199		// exactly the "first outcome wins" guarantee acks rely on.
200		let key = QueueAttemptKey {
201			queue: QueueId(7),
202			row: RowNumber(42),
203			attempt: u32::MAX,
204		};
205
206		assert_eq!(QueueAttemptKey::decode(&key.encode()), Some(key));
207	}
208
209	#[test]
210	fn test_attempt_zero_roundtrips() {
211		// Attempt 0 never reaches storage today (claim leases at attempt 1), but the codec
212		// must not treat it as an absent component; step 5's reaper writes lost attempts and
213		// a zero-eliding encoding would collide with the item's own prefix.
214		let key = QueueAttemptKey {
215			queue: QueueId(0),
216			row: RowNumber(0),
217			attempt: 0,
218		};
219
220		assert_eq!(QueueAttemptKey::decode(&key.encode()), Some(key));
221	}
222
223	#[test]
224	fn test_item_scan_excludes_neighbouring_items_and_queues() {
225		// Retention and repeat-detection both enumerate one item's attempts. If the scan
226		// leaked into the adjacent row, acking item 5 would observe item 6's history and
227		// report a repeat for work that was never done.
228		let range = QueueAttemptKey::item_scan(QueueId(3), RowNumber(5)).encode();
229
230		assert!(contains(&range, &QueueAttemptKey::encoded(QueueId(3), RowNumber(5), 0)));
231		assert!(contains(&range, &QueueAttemptKey::encoded(QueueId(3), RowNumber(5), u32::MAX)));
232		assert!(!contains(&range, &QueueAttemptKey::encoded(QueueId(3), RowNumber(6), 0)));
233		assert!(!contains(&range, &QueueAttemptKey::encoded(QueueId(3), RowNumber(4), 0)));
234		assert!(!contains(&range, &QueueAttemptKey::encoded(QueueId(4), RowNumber(5), 0)));
235	}
236
237	#[test]
238	fn test_queue_scan_covers_every_item_of_one_queue_only() {
239		// DROP QUEUE teardown and step-5 retention both sweep by queue; a range that missed
240		// row 0 or spilled into the next queue would either leak audit rows forever or delete
241		// another queue's history.
242		let range = QueueAttemptKey::queue_scan(QueueId(3)).encode();
243
244		assert!(contains(&range, &QueueAttemptKey::encoded(QueueId(3), RowNumber(0), 0)));
245		assert!(contains(&range, &QueueAttemptKey::encoded(QueueId(3), RowNumber(u64::MAX), 9)));
246		assert!(!contains(&range, &QueueAttemptKey::encoded(QueueId(2), RowNumber(1), 0)));
247		assert!(!contains(&range, &QueueAttemptKey::encoded(QueueId(4), RowNumber(1), 0)));
248	}
249
250	#[test]
251	fn test_a_foreign_kind_does_not_decode() {
252		// Every family shares the single-lane and MVCC keyspace; decoding a neighbour's key
253		// as an attempt record would attribute another object's bytes to a queue item.
254		let foreign = QueueItemStateKey::encoded(QueueId(1), 0, RowNumber(1));
255
256		assert_eq!(QueueAttemptKey::decode(&foreign), None);
257	}
258}
259
260#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
261#[key(tag = QueueDeduplication)]
262pub struct QueueDeduplicationKey {
263	pub queue: QueueId,
264	pub tail: EncodedKey,
265}
266
267impl QueueDeduplicationKey {
268	pub fn new(queue: impl Into<QueueId>, tail: impl AsRef<[u8]>) -> Self {
269		Self {
270			queue: queue.into(),
271			tail: EncodedKey::new(tail),
272		}
273	}
274
275	pub fn encoded(queue: impl Into<QueueId>, tail: impl AsRef<[u8]>) -> EncodedKey {
276		Self::new(queue, tail).encode()
277	}
278
279	pub fn full_scan(queue: QueueId) -> TaggedKeyBoundRange {
280		TaggedKeyBoundRange::prefix(Self::TAG, [Field::UDesc(Width::U64, queue.0 as u128)])
281	}
282}
283
284#[cfg(test)]
285mod queue_deduplication_key_tests {
286	use std::ops::Bound;
287
288	use super::*;
289
290	#[test]
291	fn test_encode_decode_roundtrip() {
292		// A lossy codec either resurrects a claimed key or fails to recognise one, and both turn a
293		// duplicate enqueue into a second work item.
294		let encoded = QueueDeduplicationKey::encoded(QueueId(3), b"invoice-42");
295		let decoded = QueueDeduplicationKey::decode(&encoded).unwrap();
296		assert_eq!(decoded.queue, QueueId(3));
297		assert_eq!(decoded.tail.as_slice(), b"invoice-42");
298	}
299
300	#[test]
301	fn test_arbitrary_bytes_survive_the_tail_encoding() {
302		// The tail is user-supplied, so it must survive the bytes the key codec treats as structural
303		// as well as embedded nul and multi-byte utf8; a mangled key dedups against the wrong record.
304		for key in [
305			vec![],
306			vec![0x00],
307			vec![0xff],
308			vec![0xff, 0x00, 0xff],
309			"order/\u{00e9}\u{4e2d}".as_bytes().to_vec(),
310		] {
311			let encoded = QueueDeduplicationKey::encoded(QueueId(1), key.clone());
312			let decoded = QueueDeduplicationKey::decode(&encoded).unwrap();
313			assert_eq!(decoded.tail.as_slice(), key.as_slice(), "tail {key:?} must round-trip unchanged");
314		}
315	}
316
317	#[test]
318	fn test_the_same_key_in_two_queues_encodes_differently() {
319		// Two queues may legitimately use the same dedup key, so without the queue id discriminating,
320		// enqueueing "invoice-1" on one queue would suppress it on every other queue.
321		let a = QueueDeduplicationKey::encoded(QueueId(1), b"same");
322		let b = QueueDeduplicationKey::encoded(QueueId(2), b"same");
323		assert_ne!(a, b);
324	}
325
326	#[test]
327	fn test_full_scan_contains_only_the_target_queue() {
328		// Keys are stored bitwise-inverted, so a bound derived with the wrong sign makes the retention
329		// sweep either miss its own records or delete a neighbouring queue's.
330		let range = QueueDeduplicationKey::full_scan(QueueId(3)).encode();
331		let Bound::Included(start) = &range.start else {
332			panic!("expected an included start bound")
333		};
334		let Bound::Excluded(end) = &range.end else {
335			panic!("expected an excluded end bound")
336		};
337
338		assert!(start.as_slice() < end.as_slice(), "the range must be non-empty under byte order");
339
340		for key in [vec![], b"a".to_vec(), vec![0xff; 64]] {
341			let inside = QueueDeduplicationKey::encoded(QueueId(3), key.clone());
342			assert!(
343				inside.as_slice() >= start.as_slice() && inside.as_slice() < end.as_slice(),
344				"key {key:?} in queue 3 must fall inside the scan range"
345			);
346		}
347
348		for queue in [QueueId(2), QueueId(4)] {
349			let neighbour = QueueDeduplicationKey::encoded(queue, b"a");
350			assert!(
351				neighbour.as_slice() < start.as_slice() || neighbour.as_slice() >= end.as_slice(),
352				"queue {queue:?} must fall outside queue 3's scan range"
353			);
354		}
355	}
356
357	#[test]
358	fn test_a_foreign_or_truncated_key_does_not_decode() {
359		// A partial record would collapse every key in the queue onto one dedup slot.
360		let encoded = QueueDeduplicationKey::encoded(QueueId(3), b"invoice-42");
361
362		let mut wrong_kind = encoded.as_slice().to_vec();
363		wrong_kind[0] = KeyTag::Queue as u8;
364		assert_eq!(QueueDeduplicationKey::decode(&EncodedKey::new(wrong_kind)), None);
365
366		let truncated = encoded.as_slice()[..5].to_vec();
367		assert_eq!(QueueDeduplicationKey::decode(&EncodedKey::new(truncated)), None);
368	}
369}
370
371#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
372#[key(tag = QueuePartition)]
373pub struct QueuePartitionKey {
374	pub queue: QueueId,
375	pub partition: u16,
376}
377
378impl QueuePartitionKey {
379	pub fn new(queue: impl Into<QueueId>, partition: u16) -> Self {
380		Self {
381			queue: queue.into(),
382			partition,
383		}
384	}
385
386	pub fn encoded(queue: impl Into<QueueId>, partition: u16) -> EncodedKey {
387		Self {
388			queue: queue.into(),
389			partition,
390		}
391		.encode()
392	}
393
394	pub fn queue_scan(queue: QueueId) -> TaggedKeyBoundRange {
395		TaggedKeyBoundRange::prefix(Self::TAG, [Field::UDesc(Width::U64, queue.0 as u128)])
396	}
397
398	pub fn full_scan() -> TaggedKeyBoundRange {
399		TaggedKeyBoundRange::kind(Self::TAG)
400	}
401}
402
403#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
404#[key(tag = QueueItemState)]
405pub struct QueueItemStateKey {
406	pub queue: QueueId,
407	pub partition: u16,
408	pub row: RowNumber,
409}
410
411impl QueueItemStateKey {
412	pub fn new(queue: impl Into<QueueId>, partition: u16, row: impl Into<RowNumber>) -> Self {
413		Self {
414			queue: queue.into(),
415			partition,
416			row: row.into(),
417		}
418	}
419
420	pub fn encoded(queue: impl Into<QueueId>, partition: u16, row: impl Into<RowNumber>) -> EncodedKey {
421		Self {
422			queue: queue.into(),
423			partition,
424			row: row.into(),
425		}
426		.encode()
427	}
428
429	pub fn partition_scan(queue: QueueId, partition: u16) -> TaggedKeyBoundRange {
430		TaggedKeyBoundRange::prefix(
431			Self::TAG,
432			[Field::UDesc(Width::U64, queue.0 as u128), Field::UDesc(Width::U16, partition as u128)],
433		)
434	}
435
436	pub fn queue_scan(queue: QueueId) -> TaggedKeyBoundRange {
437		TaggedKeyBoundRange::prefix(Self::TAG, [Field::UDesc(Width::U64, queue.0 as u128)])
438	}
439
440	pub fn full_scan() -> TaggedKeyBoundRange {
441		TaggedKeyBoundRange::kind(Self::TAG)
442	}
443}
444
445#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
446#[key(tag = QueueDue)]
447pub struct QueueDueKey {
448	pub queue: QueueId,
449	pub partition: u16,
450	pub due: DateTime,
451	pub row: RowNumber,
452}
453
454impl QueueDueKey {
455	pub fn new(queue: impl Into<QueueId>, partition: u16, due: DateTime, row: impl Into<RowNumber>) -> Self {
456		Self {
457			queue: queue.into(),
458			partition,
459			due,
460			row: row.into(),
461		}
462	}
463
464	pub fn encoded(
465		queue: impl Into<QueueId>,
466		partition: u16,
467		due: DateTime,
468		row: impl Into<RowNumber>,
469	) -> EncodedKey {
470		Self::new(queue, partition, due, row).encode()
471	}
472
473	pub fn partition_scan(queue: QueueId, partition: u16) -> TaggedKeyBoundRange {
474		TaggedKeyBoundRange::prefix(
475			Self::TAG,
476			[Field::UDesc(Width::U64, queue.0 as u128), Field::UDesc(Width::U16, partition as u128)],
477		)
478	}
479
480	pub fn queue_scan(queue: QueueId) -> TaggedKeyBoundRange {
481		TaggedKeyBoundRange::prefix(Self::TAG, [Field::UDesc(Width::U64, queue.0 as u128)])
482	}
483
484	pub fn full_scan() -> TaggedKeyBoundRange {
485		TaggedKeyBoundRange::kind(Self::TAG)
486	}
487}
488
489#[derive(Debug, Clone, PartialEq, KeyCodec, Hash)]
490#[key(tag = QueueKeyActive)]
491pub struct QueueKeyActiveKey {
492	pub queue: QueueId,
493	pub partition: u16,
494	pub key_hash: u64,
495	pub row: RowNumber,
496}
497
498impl QueueKeyActiveKey {
499	pub fn new(queue: impl Into<QueueId>, partition: u16, key_hash: u64, row: impl Into<RowNumber>) -> Self {
500		Self {
501			queue: queue.into(),
502			partition,
503			key_hash,
504			row: row.into(),
505		}
506	}
507
508	pub fn encoded(
509		queue: impl Into<QueueId>,
510		partition: u16,
511		key_hash: u64,
512		row: impl Into<RowNumber>,
513	) -> EncodedKey {
514		Self {
515			queue: queue.into(),
516			partition,
517			key_hash,
518			row: row.into(),
519		}
520		.encode()
521	}
522
523	pub fn key_scan(queue: QueueId, partition: u16, key_hash: u64) -> TaggedKeyBoundRange {
524		TaggedKeyBoundRange::prefix(
525			Self::TAG,
526			[
527				Field::UDesc(Width::U64, queue.0 as u128),
528				Field::UDesc(Width::U16, partition as u128),
529				Field::UDesc(Width::U64, key_hash as u128),
530			],
531		)
532	}
533
534	pub fn partition_scan(queue: QueueId, partition: u16) -> TaggedKeyBoundRange {
535		TaggedKeyBoundRange::prefix(
536			Self::TAG,
537			[Field::UDesc(Width::U64, queue.0 as u128), Field::UDesc(Width::U16, partition as u128)],
538		)
539	}
540
541	pub fn queue_scan(queue: QueueId) -> TaggedKeyBoundRange {
542		TaggedKeyBoundRange::prefix(Self::TAG, [Field::UDesc(Width::U64, queue.0 as u128)])
543	}
544
545	pub fn full_scan() -> TaggedKeyBoundRange {
546		TaggedKeyBoundRange::kind(Self::TAG)
547	}
548}
549
550#[cfg(test)]
551mod queue_partition_key_tests {
552	use std::ops::Bound;
553
554	use reifydb_codec::key::{encoded::EncodedKeyRange, serializer::KeySerializer};
555
556	use super::*;
557
558	fn contains(range: &EncodedKeyRange, key: &EncodedKey) -> bool {
559		let after_start = match &range.start {
560			Bound::Included(start) => key.as_slice() >= start.as_slice(),
561			Bound::Excluded(start) => key.as_slice() > start.as_slice(),
562			Bound::Unbounded => true,
563		};
564		let before_end = match &range.end {
565			Bound::Included(end) => key.as_slice() <= end.as_slice(),
566			Bound::Excluded(end) => key.as_slice() < end.as_slice(),
567			Bound::Unbounded => true,
568		};
569		after_start && before_end
570	}
571
572	#[test]
573	fn test_partition_key_roundtrips_at_both_partition_bounds() {
574		// The counter row is addressed by this key and is also the lock key every claim
575		// serialises on. A codec that loses the partition would point two partitions at one
576		// counter, so depth accounting and the lock would silently merge.
577		for partition in [0u16, 1, 1023] {
578			let encoded = QueuePartitionKey::encoded(QueueId(7), partition);
579			let decoded = QueuePartitionKey::decode(&encoded).unwrap();
580			assert_eq!(decoded.queue, QueueId(7));
581			assert_eq!(decoded.partition, partition);
582		}
583	}
584
585	#[test]
586	fn test_item_state_key_roundtrips() {
587		// The state record is the compare-and-set target for every transition; a key that
588		// decodes to the wrong row would transition somebody else's item.
589		let encoded = QueueItemStateKey::encoded(QueueId(3), 5, RowNumber(42));
590		let decoded = QueueItemStateKey::decode(&encoded).unwrap();
591		assert_eq!(decoded.queue, QueueId(3));
592		assert_eq!(decoded.partition, 5);
593		assert_eq!(decoded.row, RowNumber(42));
594	}
595
596	#[test]
597	fn test_due_key_roundtrips_at_epoch_and_far_future() {
598		// Epoch is the due time of every immediately-ready item, and a far-future value is
599		// what a long not_before produces; both ends must survive the varint encoding or the
600		// due index would resolve to the wrong instant.
601		for nanos in [0i64, 1, 4_102_444_800_000_000_000, i64::MAX] {
602			let due = DateTime::from_nanos(nanos);
603			let encoded = QueueDueKey::encoded(QueueId(1), 2, due, RowNumber(9));
604			let decoded = QueueDueKey::decode(&encoded).unwrap();
605			assert_eq!(decoded.due.to_nanos(), nanos);
606			assert_eq!(decoded.row, RowNumber(9));
607			assert_eq!(decoded.partition, 2);
608		}
609	}
610
611	#[test]
612	fn test_partition_scan_excludes_neighbouring_partitions() {
613		// The obvious "partition - 1" end bound underflows at partition 0 and would scan the
614		// whole queue; this pins the prefix-derived range instead. A scan that leaked into a
615		// neighbouring partition would let a claim take work it does not hold the lock for.
616		for partition in [0u16, 1, 1023] {
617			let range = QueueItemStateKey::partition_scan(QueueId(4), partition).encode();
618
619			assert!(contains(&range, &QueueItemStateKey::encoded(QueueId(4), partition, RowNumber(0))));
620			assert!(contains(
621				&range,
622				&QueueItemStateKey::encoded(QueueId(4), partition, RowNumber(u64::MAX))
623			));
624
625			for other in [partition.wrapping_sub(1), partition + 1] {
626				if other == partition {
627					continue;
628				}
629				let neighbour = QueueItemStateKey::encoded(QueueId(4), other, RowNumber(1));
630				assert!(
631					!contains(&range, &neighbour),
632					"partition {other} must fall outside {partition}"
633				);
634			}
635
636			let other_queue = QueueItemStateKey::encoded(QueueId(5), partition, RowNumber(1));
637			assert!(!contains(&range, &other_queue), "queue 5 must fall outside queue 4's partition scan");
638		}
639	}
640
641	#[test]
642	fn test_queue_scan_covers_every_partition_of_one_queue_only() {
643		// DROP QUEUE wipes the scheduling keyspace through this range: missing a partition
644		// leaks records that hydration would later re-admit into a queue that no longer exists.
645		let range = QueueDueKey::queue_scan(QueueId(4)).encode();
646
647		for partition in [0u16, 1, 1023] {
648			let inside = QueueDueKey::encoded(QueueId(4), partition, DateTime::from_nanos(7), RowNumber(1));
649			assert!(contains(&range, &inside), "partition {partition} must fall inside the queue scan");
650		}
651
652		for queue in [QueueId(3), QueueId(5)] {
653			let outside = QueueDueKey::encoded(queue, 0, DateTime::from_nanos(7), RowNumber(1));
654			assert!(!contains(&range, &outside), "queue {queue:?} must fall outside queue 4's scan");
655		}
656	}
657
658	#[test]
659	fn test_due_keys_sort_latest_due_first() {
660		// Keys are stored bitwise-inverted, so forward iteration yields the LATEST due time
661		// first. Step 4's claim must therefore scan in reverse; if this inversion ever
662		// changes, claims would silently drain newest-first and starve the oldest work.
663		let earlier = QueueDueKey::encoded(QueueId(1), 0, DateTime::from_nanos(1_000), RowNumber(1));
664		let later = QueueDueKey::encoded(QueueId(1), 0, DateTime::from_nanos(2_000), RowNumber(1));
665
666		assert!(later.as_slice() < earlier.as_slice(), "the later due time must encode to the smaller key");
667	}
668
669	#[test]
670	fn test_a_foreign_kind_does_not_decode() {
671		// The three families share a prefix layout, so a mis-tagged key would otherwise
672		// decode cleanly and address the wrong record entirely.
673		let encoded = QueueItemStateKey::encoded(QueueId(1), 0, RowNumber(1));
674
675		assert_eq!(QueuePartitionKey::decode(&encoded), None);
676		assert_eq!(QueueDueKey::decode(&encoded), None);
677		assert_eq!(QueueKeyActiveKey::decode(&EncodedKey::new(&encoded.as_slice()[..3])), None);
678	}
679
680	#[test]
681	fn test_key_active_key_roundtrips() {
682		let encoded = QueueKeyActiveKey::encoded(QueueId(3), 5, 0xDEAD_BEEF_CAFE_F00D, RowNumber(42));
683		let decoded = QueueKeyActiveKey::decode(&encoded).unwrap();
684		assert_eq!(decoded.queue, QueueId(3));
685		assert_eq!(decoded.partition, 5);
686		assert_eq!(decoded.key_hash, 0xDEAD_BEEF_CAFE_F00D);
687		assert_eq!(decoded.row, RowNumber(42));
688	}
689
690	#[test]
691	fn test_key_active_keys_sort_largest_row_first() {
692		let first = QueueKeyActiveKey::encoded(QueueId(1), 0, 77, RowNumber(1));
693		let middle = QueueKeyActiveKey::encoded(QueueId(1), 0, 77, RowNumber(5));
694		let last = QueueKeyActiveKey::encoded(QueueId(1), 0, 77, RowNumber(9));
695
696		assert!(last.as_slice() < middle.as_slice());
697		assert!(middle.as_slice() < first.as_slice());
698	}
699
700	#[test]
701	fn test_key_scan_excludes_neighbouring_keys_and_partitions() {
702		let range = QueueKeyActiveKey::key_scan(QueueId(4), 2, 77).encode();
703
704		assert!(contains(&range, &QueueKeyActiveKey::encoded(QueueId(4), 2, 77, RowNumber(0))));
705		assert!(contains(&range, &QueueKeyActiveKey::encoded(QueueId(4), 2, 77, RowNumber(u64::MAX))));
706
707		for other_hash in [76u64, 78, 0, u64::MAX] {
708			let neighbour = QueueKeyActiveKey::encoded(QueueId(4), 2, other_hash, RowNumber(1));
709			assert!(!contains(&range, &neighbour), "key hash {other_hash} must fall outside key 77");
710		}
711
712		let other_partition = QueueKeyActiveKey::encoded(QueueId(4), 3, 77, RowNumber(1));
713		assert!(!contains(&range, &other_partition), "partition 3 must fall outside partition 2");
714
715		let other_queue = QueueKeyActiveKey::encoded(QueueId(5), 2, 77, RowNumber(1));
716		assert!(!contains(&range, &other_queue), "queue 5 must fall outside queue 4");
717	}
718
719	#[test]
720	fn test_partition_scan_covers_every_key_of_one_partition_only() {
721		let range = QueueKeyActiveKey::partition_scan(QueueId(4), 2).encode();
722
723		for key_hash in [0u64, 77, u64::MAX] {
724			let inside = QueueKeyActiveKey::encoded(QueueId(4), 2, key_hash, RowNumber(1));
725			assert!(contains(&range, &inside), "key hash {key_hash} must fall inside the partition scan");
726		}
727
728		let other_partition = QueueKeyActiveKey::encoded(QueueId(4), 3, 77, RowNumber(1));
729		assert!(!contains(&range, &other_partition));
730	}
731
732	#[test]
733	fn test_schedule_keys_match_legacy_byte_layout() {
734		let queue = QueueId(7);
735		let partition = 3u16;
736		let row = RowNumber(42);
737
738		let mut legacy = KeySerializer::with_capacity(11);
739		legacy.extend_u8(KeyTag::QueuePartition as u8).extend_u64(queue).extend_u16(partition);
740		assert_eq!(legacy.to_encoded_key().as_slice(), QueuePartitionKey::encoded(queue, partition).as_slice());
741
742		let mut legacy = KeySerializer::with_capacity(19);
743		legacy.extend_u8(KeyTag::QueueItemState as u8)
744			.extend_u64(queue)
745			.extend_u16(partition)
746			.extend_u64(row.0);
747		assert_eq!(
748			legacy.to_encoded_key().as_slice(),
749			QueueItemStateKey::encoded(queue, partition, row).as_slice()
750		);
751
752		let due = DateTime::from_nanos(1_000);
753		let mut legacy = KeySerializer::with_capacity(27);
754		legacy.extend_u8(KeyTag::QueueDue as u8)
755			.extend_u64(queue)
756			.extend_u16(partition)
757			.extend_datetime(&due)
758			.extend_u64(row.0);
759		assert_eq!(
760			legacy.to_encoded_key().as_slice(),
761			QueueDueKey::encoded(queue, partition, due, row).as_slice()
762		);
763
764		let key_hash = 0xDEAD_BEEFu64;
765		let mut legacy = KeySerializer::with_capacity(28);
766		legacy.extend_u8(KeyTag::QueueKeyActive as u8)
767			.extend_u64(queue)
768			.extend_u16(partition)
769			.extend_u64(key_hash)
770			.extend_u64(row.0);
771		assert_eq!(
772			legacy.to_encoded_key().as_slice(),
773			QueueKeyActiveKey::encoded(queue, partition, key_hash, row).as_slice()
774		);
775	}
776
777	#[test]
778	fn queue_due_key_derive_matches_extend_datetime_before_the_epoch() {
779		// a derive that diverges from extend_datetime for negative nanos would sort due timers differently
780		let queue = QueueId(7);
781		let partition = 3u16;
782		let row = RowNumber(42);
783		let due = DateTime::from_nanos(-1);
784
785		let mut legacy = KeySerializer::with_capacity(27);
786		legacy.extend_u8(KeyTag::QueueDue as u8)
787			.extend_u64(queue)
788			.extend_u16(partition)
789			.extend_datetime(&due)
790			.extend_u64(row.0);
791		assert_eq!(
792			legacy.to_encoded_key().as_slice(),
793			QueueDueKey::encoded(queue, partition, due, row).as_slice()
794		);
795	}
796}