Skip to main content

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