1use std::fmt;
2use std::sync::Arc;
3
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7use crate::error::{Error, Result};
8use crate::partition::Partition;
9
10#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
11pub struct CursorId(Arc<str>);
12
13impl CursorId {
14 pub fn new(value: impl Into<Arc<str>>) -> Result<Self> {
15 let value: Arc<str> = value.into();
16 if value.is_empty() || value.len() > 128 {
17 return Err(Error::Config(format!(
18 "invalid cursor id: {:?}",
19 value.as_ref()
20 )));
21 }
22 if !value
23 .chars()
24 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')
25 {
26 return Err(Error::Config(format!(
27 "invalid cursor id: {:?}",
28 value.as_ref()
29 )));
30 }
31 Ok(Self(value))
32 }
33
34 pub fn global() -> Self {
35 Self(Arc::from("global"))
36 }
37
38 pub fn partition(partition: Partition) -> Self {
39 Self(Arc::from(format!(
40 "partition:{count}:{id}",
41 count = partition.count(),
42 id = partition.id(),
43 )))
44 }
45
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49
50 pub fn prefixed(&self, prefix: impl AsRef<str>) -> Result<Self> {
51 Self::new(format!("{}:{}", prefix.as_ref(), self.as_str()))
52 }
53}
54
55impl fmt::Display for CursorId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(&self.0)
58 }
59}
60
61impl serde::Serialize for CursorId {
62 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
63 s.serialize_str(&self.0)
64 }
65}
66
67impl<'de> serde::Deserialize<'de> for CursorId {
68 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
69 let value = String::deserialize(d)?;
70 CursorId::new(Arc::from(value)).map_err(serde::de::Error::custom)
71 }
72}
73
74#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
75pub struct CursorOrder(Arc<[u8]>);
76
77impl CursorOrder {
78 pub fn min() -> Self {
79 Self(Arc::from(&[][..]))
80 }
81
82 pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Self {
83 Self(bytes.into())
84 }
85
86 pub fn from_u64(value: u64) -> Self {
87 Self(Arc::from(value.to_be_bytes().as_slice()))
88 }
89
90 pub fn from_i64(value: i64) -> Self {
91 let mapped = (value as u64) ^ (1u64 << 63);
92 Self::from_u64(mapped)
93 }
94
95 pub fn as_bytes(&self) -> &[u8] {
96 &self.0
97 }
98}
99
100impl serde::Serialize for CursorOrder {
101 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
102 use base64::Engine;
103 use base64::engine::general_purpose::STANDARD;
104 s.serialize_str(&STANDARD.encode(&self.0))
105 }
106}
107
108impl<'de> serde::Deserialize<'de> for CursorOrder {
109 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
110 use base64::Engine;
111 use base64::engine::general_purpose::STANDARD;
112 let raw = String::deserialize(d)?;
113 let bytes = STANDARD
114 .decode(raw.as_bytes())
115 .map_err(serde::de::Error::custom)?;
116 Ok(Self(Arc::from(bytes.as_slice())))
117 }
118}
119
120#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
121pub struct CursorKind(Arc<str>);
122
123impl CursorKind {
124 pub fn new(value: impl Into<Arc<str>>) -> Result<Self> {
125 let value: Arc<str> = value.into();
126 if value.is_empty() || value.len() > 128 {
127 return Err(Error::Config(format!(
128 "invalid cursor kind: {:?}",
129 value.as_ref()
130 )));
131 }
132 if !value
133 .chars()
134 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')
135 {
136 return Err(Error::Config(format!(
137 "invalid cursor kind: {:?}",
138 value.as_ref()
139 )));
140 }
141 Ok(Self(value))
142 }
143
144 pub fn as_str(&self) -> &str {
145 &self.0
146 }
147}
148
149impl fmt::Display for CursorKind {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.write_str(&self.0)
152 }
153}
154
155impl Serialize for CursorKind {
156 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
157 s.serialize_str(&self.0)
158 }
159}
160
161impl<'de> Deserialize<'de> for CursorKind {
162 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
163 let value = String::deserialize(d)?;
164 CursorKind::new(Arc::from(value)).map_err(serde::de::Error::custom)
165 }
166}
167
168#[derive(Debug, Clone, Eq, PartialEq, Hash)]
169pub struct EncodedCursor {
170 id: CursorId,
171 kind: CursorKind,
172 order: CursorOrder,
173 payload: Arc<str>,
174}
175
176#[derive(Serialize, Deserialize)]
177struct EncodedCursorRepr {
178 id: CursorId,
179 kind: CursorKind,
180 order: CursorOrder,
181 payload: String,
182}
183
184impl Serialize for EncodedCursor {
185 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
186 let repr = EncodedCursorRepr {
187 id: self.id.clone(),
188 kind: self.kind.clone(),
189 order: self.order.clone(),
190 payload: self.payload.as_ref().to_owned(),
191 };
192 repr.serialize(s)
193 }
194}
195
196impl<'de> Deserialize<'de> for EncodedCursor {
197 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
198 let repr = EncodedCursorRepr::deserialize(d)?;
199 Ok(Self {
200 id: repr.id,
201 kind: repr.kind,
202 order: repr.order,
203 payload: Arc::from(repr.payload),
204 })
205 }
206}
207
208impl EncodedCursor {
209 pub fn new(
210 id: CursorId,
211 kind: CursorKind,
212 order: CursorOrder,
213 payload: impl Into<Arc<str>>,
214 ) -> Result<Self> {
215 let payload: Arc<str> = payload.into();
216 if payload.is_empty() {
217 return Err(Error::InvalidCursor(
218 "encoded cursor payload must not be empty".to_owned(),
219 ));
220 }
221
222 use base64::Engine;
223 use base64::engine::general_purpose::STANDARD;
224 STANDARD.decode(payload.as_bytes()).map_err(|e| {
225 Error::InvalidCursor(format!("encoded cursor payload is not base64: {e}"))
226 })?;
227
228 Ok(Self {
229 id,
230 kind,
231 order,
232 payload,
233 })
234 }
235
236 pub fn from_bytes(
237 id: CursorId,
238 kind: CursorKind,
239 order: CursorOrder,
240 bytes: impl AsRef<[u8]>,
241 ) -> Self {
242 use base64::Engine;
243 use base64::engine::general_purpose::STANDARD;
244 Self {
245 id,
246 kind,
247 order,
248 payload: Arc::from(STANDARD.encode(bytes.as_ref())),
249 }
250 }
251
252 pub fn from_json<T: Serialize>(
253 id: CursorId,
254 kind: CursorKind,
255 order: CursorOrder,
256 value: &T,
257 ) -> Result<Self> {
258 let bytes = serde_json::to_vec(value)
259 .map_err(|e| Error::Serialization(format!("encode cursor json: {e}")))?;
260 Ok(Self::from_bytes(id, kind, order, bytes))
261 }
262
263 pub fn decode_bytes(&self) -> Result<Vec<u8>> {
264 use base64::Engine;
265 use base64::engine::general_purpose::STANDARD;
266 STANDARD
267 .decode(self.payload.as_bytes())
268 .map_err(|e| Error::InvalidCursor(format!("decode cursor payload: {e}")))
269 }
270
271 pub fn decode_json<T: DeserializeOwned>(&self, expected_kind: &CursorKind) -> Result<T> {
272 if &self.kind != expected_kind {
273 return Err(Error::InvalidCursor(format!(
274 "expected cursor kind `{}`, got `{}`",
275 expected_kind.as_str(),
276 self.kind.as_str()
277 )));
278 }
279 serde_json::from_slice(&self.decode_bytes()?)
280 .map_err(|e| Error::InvalidCursor(format!("decode cursor json: {e}")))
281 }
282
283 pub fn id_ref(&self) -> &CursorId {
284 &self.id
285 }
286
287 pub fn kind(&self) -> &CursorKind {
288 &self.kind
289 }
290
291 pub fn order(&self) -> &CursorOrder {
292 &self.order
293 }
294
295 pub fn payload(&self) -> &str {
296 &self.payload
297 }
298}
299
300impl PartialOrd for EncodedCursor {
301 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
302 Some(self.cmp(other))
303 }
304}
305
306impl Ord for EncodedCursor {
307 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
308 self.id.cmp(&other.id).then(self.order.cmp(&other.order))
309 }
310}
311
312impl Cursor for EncodedCursor {
313 fn id(&self) -> CursorId {
314 self.id.clone()
315 }
316
317 fn order_key(&self) -> CursorOrder {
318 self.order.clone()
319 }
320}
321
322pub trait CursorCodec<C>: Clone + Send + Sync + 'static {
323 fn encode(&self, cursor: &C) -> Result<EncodedCursor>;
324
325 fn decode(&self, cursor: &EncodedCursor) -> Result<C>;
326}
327
328#[derive(Debug)]
329pub struct JsonCursorCodec<C> {
330 kind: CursorKind,
331 _cursor: std::marker::PhantomData<fn() -> C>,
332}
333
334impl<C> Clone for JsonCursorCodec<C> {
335 fn clone(&self) -> Self {
336 Self {
337 kind: self.kind.clone(),
338 _cursor: std::marker::PhantomData,
339 }
340 }
341}
342
343impl<C> JsonCursorCodec<C> {
344 pub fn new(kind: impl Into<Arc<str>>) -> Result<Self> {
345 Ok(Self {
346 kind: CursorKind::new(kind)?,
347 _cursor: std::marker::PhantomData,
348 })
349 }
350
351 pub fn kind(&self) -> &CursorKind {
352 &self.kind
353 }
354}
355
356impl<C> CursorCodec<C> for JsonCursorCodec<C>
357where
358 C: Cursor + Serialize + DeserializeOwned + Send + Sync + 'static,
359{
360 fn encode(&self, cursor: &C) -> Result<EncodedCursor> {
361 EncodedCursor::from_json(cursor.id(), self.kind.clone(), cursor.order_key(), cursor)
362 }
363
364 fn decode(&self, cursor: &EncodedCursor) -> Result<C> {
365 cursor.decode_json(&self.kind)
366 }
367}
368
369#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
370pub struct NoCursor;
371
372pub trait Cursor {
373 fn id(&self) -> CursorId {
374 CursorId::global()
375 }
376
377 fn order_key(&self) -> CursorOrder {
378 CursorOrder::min()
379 }
380}
381
382impl Cursor for NoCursor {}
383
384#[cfg(test)]
385mod tests {
386 use std::num::NonZeroU32;
387
388 use super::*;
389
390 fn partition(count: u32, id: u32) -> Partition {
391 Partition::new(id, NonZeroU32::new(count).unwrap()).unwrap()
392 }
393
394 #[test]
395 fn global_returns_static_global_id() {
396 assert_eq!(CursorId::global().as_str(), "global");
397 assert_eq!(CursorId::global(), CursorId::global());
398 }
399
400 #[test]
401 fn partition_constructs_stable_format() {
402 let id = CursorId::partition(partition(100, 17));
403 assert_eq!(id.as_str(), "partition:100:17");
404 }
405
406 #[test]
407 fn new_validates_and_stores_value() {
408 let id = CursorId::new("custom.stream").unwrap();
409 assert_eq!(id.as_str(), "custom.stream");
410 }
411
412 #[test]
413 fn new_rejects_empty() {
414 assert!(CursorId::new("").is_err());
415 }
416
417 #[test]
418 fn new_rejects_too_long() {
419 let long = "a".repeat(129);
420 assert!(CursorId::new(long).is_err());
421 }
422
423 #[test]
424 fn new_rejects_invalid_chars() {
425 assert!(CursorId::new("bad space").is_err());
426 assert!(CursorId::new("bad/char").is_err());
427 }
428
429 #[test]
430 fn new_accepts_valid_chars() {
431 assert!(CursorId::new("valid-name_01.v2:tag").is_ok());
432 }
433
434 #[test]
435 fn equality_by_value() {
436 let a = CursorId::new("test").unwrap();
437 let b = CursorId::new("test").unwrap();
438 assert_eq!(a, b);
439 }
440
441 #[test]
442 fn distinct_values_differ() {
443 let a = CursorId::new("a").unwrap();
444 let b = CursorId::new("b").unwrap();
445 assert_ne!(a, b);
446 }
447
448 #[test]
449 fn cursor_id_orders_lexically() {
450 let a = CursorId::new("a").unwrap();
451 let b = CursorId::new("b").unwrap();
452 assert!(a < b);
453 }
454
455 #[test]
456 fn prefixed_adds_stable_prefix() {
457 let id = CursorId::global().prefixed("left").unwrap();
458 assert_eq!(id.as_str(), "left:global");
459 }
460
461 #[test]
462 fn prefixed_rejects_invalid_prefix() {
463 let err = CursorId::global().prefixed("bad prefix").unwrap_err();
464 assert!(err.to_string().contains("invalid cursor id"));
465 }
466
467 #[test]
468 fn cursor_order_from_i64_preserves_signed_ordering() {
469 let lo = CursorOrder::from_i64(i64::MIN);
470 let zero = CursorOrder::from_i64(0);
471 let hi = CursorOrder::from_i64(i64::MAX);
472 assert!(lo < zero);
473 assert!(zero < hi);
474 assert!(CursorOrder::from_i64(-1) < CursorOrder::from_i64(1));
475 assert!(CursorOrder::from_i64(9) < CursorOrder::from_i64(10));
476 }
477
478 #[test]
479 fn cursor_order_from_u64_preserves_ordering() {
480 assert!(CursorOrder::from_u64(9) < CursorOrder::from_u64(10));
481 }
482
483 #[test]
484 fn cursor_order_roundtrips_via_json() {
485 let v = CursorOrder::from_i64(-42);
486 let s = serde_json::to_string(&v).unwrap();
487 let back: CursorOrder = serde_json::from_str(&s).unwrap();
488 assert_eq!(v, back);
489 }
490
491 #[test]
492 fn cursor_order_min_is_smallest() {
493 assert!(CursorOrder::min() < CursorOrder::from_u64(0));
494 }
495
496 #[test]
497 fn cursor_kind_validates() {
498 assert!(CursorKind::new("eventuary.postgres.cursor.v1").is_ok());
499 assert!(CursorKind::new("").is_err());
500 assert!(CursorKind::new("bad kind").is_err());
501 }
502
503 #[test]
504 fn encoded_cursor_roundtrips_json() {
505 #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
506 struct TestCursor {
507 sequence: i64,
508 }
509
510 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
511 let cursor = EncodedCursor::from_json(
512 CursorId::global(),
513 kind.clone(),
514 CursorOrder::from_i64(42),
515 &TestCursor { sequence: 42 },
516 )
517 .unwrap();
518
519 assert_eq!(cursor.id_ref(), &CursorId::global());
520 assert_eq!(cursor.kind(), &kind);
521 assert_eq!(cursor.order(), &CursorOrder::from_i64(42));
522 let decoded: TestCursor = cursor.decode_json(&kind).unwrap();
523 assert_eq!(decoded, TestCursor { sequence: 42 });
524 }
525
526 #[test]
527 fn encoded_cursor_rejects_wrong_kind() {
528 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
529 let other = CursorKind::new("eventuary.other.cursor.v1").unwrap();
530 let cursor =
531 EncodedCursor::from_json(CursorId::global(), kind, CursorOrder::from_i64(0), &42_i64)
532 .unwrap();
533
534 let err = cursor.decode_json::<i64>(&other).unwrap_err();
535 assert!(err.to_string().contains("expected cursor kind"));
536 }
537
538 #[test]
539 fn encoded_cursor_orders_by_id_then_order() {
540 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
541 let a9 = EncodedCursor::from_bytes(
542 CursorId::new("a").unwrap(),
543 kind.clone(),
544 CursorOrder::from_i64(9),
545 b"x",
546 );
547 let a10 = EncodedCursor::from_bytes(
548 CursorId::new("a").unwrap(),
549 kind.clone(),
550 CursorOrder::from_i64(10),
551 b"x",
552 );
553 let b0 = EncodedCursor::from_bytes(
554 CursorId::new("b").unwrap(),
555 kind,
556 CursorOrder::from_i64(0),
557 b"x",
558 );
559
560 assert!(a9 < a10);
561 assert!(a10 < b0);
562 }
563
564 #[test]
565 fn encoded_cursor_new_validates_payload() {
566 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
567 let err = EncodedCursor::new(
568 CursorId::global(),
569 kind.clone(),
570 CursorOrder::from_i64(0),
571 "",
572 )
573 .unwrap_err();
574 assert!(err.to_string().contains("must not be empty"));
575
576 let err = EncodedCursor::new(
577 CursorId::global(),
578 kind.clone(),
579 CursorOrder::from_i64(0),
580 "not-base64-$$",
581 )
582 .unwrap_err();
583 assert!(err.to_string().contains("not base64"));
584
585 let ok =
586 EncodedCursor::from_bytes(CursorId::global(), kind, CursorOrder::from_i64(0), b"x");
587 let rebuilt = EncodedCursor::new(
588 ok.id_ref().clone(),
589 ok.kind().clone(),
590 ok.order().clone(),
591 ok.payload().to_owned(),
592 )
593 .unwrap();
594 assert_eq!(rebuilt, ok);
595 }
596
597 #[test]
598 fn encoded_cursor_implements_cursor_trait() {
599 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
600 let cursor = EncodedCursor::from_bytes(
601 CursorId::new("custom").unwrap(),
602 kind,
603 CursorOrder::from_u64(7),
604 b"p",
605 );
606 assert_eq!(<EncodedCursor as Cursor>::id(&cursor).as_str(), "custom");
607 assert_eq!(
608 <EncodedCursor as Cursor>::order_key(&cursor),
609 CursorOrder::from_u64(7)
610 );
611 }
612
613 #[test]
614 fn json_cursor_codec_roundtrips() {
615 #[derive(
616 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
617 )]
618 struct TestCursor(i64);
619
620 impl Cursor for TestCursor {
621 fn order_key(&self) -> CursorOrder {
622 CursorOrder::from_i64(self.0)
623 }
624 }
625
626 let codec = JsonCursorCodec::<TestCursor>::new("eventuary.test.cursor.v1").unwrap();
627 let encoded = codec.encode(&TestCursor(7)).unwrap();
628 assert_eq!(encoded.id_ref(), &CursorId::global());
629 assert_eq!(encoded.kind(), codec.kind());
630 assert_eq!(encoded.order(), &CursorOrder::from_i64(7));
631
632 let decoded = codec.decode(&encoded).unwrap();
633 assert_eq!(decoded, TestCursor(7));
634 }
635
636 #[test]
637 fn json_cursor_codec_preserves_typed_ordering_under_erasure() {
638 #[derive(
639 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
640 )]
641 struct TestCursor(i64);
642
643 impl Cursor for TestCursor {
644 fn order_key(&self) -> CursorOrder {
645 CursorOrder::from_i64(self.0)
646 }
647 }
648
649 let codec = JsonCursorCodec::<TestCursor>::new("eventuary.test.cursor.v1").unwrap();
650 for (a, b) in [(-5_i64, 0), (0, 1), (9, 10), (i64::MIN, i64::MAX)] {
651 let ea = codec.encode(&TestCursor(a)).unwrap();
652 let eb = codec.encode(&TestCursor(b)).unwrap();
653 assert_eq!(
654 TestCursor(a).cmp(&TestCursor(b)),
655 ea.cmp(&eb),
656 "a={a} b={b}"
657 );
658 }
659 }
660
661 #[test]
662 fn encoded_cursor_roundtrips_via_serde() {
663 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
664 let cursor =
665 EncodedCursor::from_json(CursorId::global(), kind, CursorOrder::from_i64(42), &42_i64)
666 .unwrap();
667
668 let s = serde_json::to_string(&cursor).unwrap();
669 let back: EncodedCursor = serde_json::from_str(&s).unwrap();
670 assert_eq!(cursor, back);
671 }
672
673 #[test]
674 fn cursor_trait_default_is_global() {
675 struct SomeCursor;
676 impl Cursor for SomeCursor {}
677 assert_eq!(SomeCursor.id(), CursorId::global());
678 }
679
680 #[test]
681 fn cursor_trait_named_example() {
682 struct NamedCursor;
683 impl Cursor for NamedCursor {
684 fn id(&self) -> CursorId {
685 CursorId::new("partition:100:17").unwrap()
686 }
687 }
688 assert_eq!(NamedCursor.id(), CursorId::partition(partition(100, 17)));
689 }
690
691 #[test]
692 fn cursor_trait_default_order_key_is_min() {
693 struct Probe;
694 impl Cursor for Probe {}
695 assert_eq!(Probe.order_key(), CursorOrder::min());
696 }
697
698 #[test]
699 fn cursor_trait_named_example_supports_custom_order() {
700 struct OrderedCursor(i64);
701 impl Cursor for OrderedCursor {
702 fn order_key(&self) -> CursorOrder {
703 CursorOrder::from_i64(self.0)
704 }
705 }
706 assert!(OrderedCursor(9).order_key() < OrderedCursor(10).order_key());
707 }
708
709 #[test]
710 fn cursor_trait_does_not_require_ord() {
711 struct UnorderedCursor;
712
713 impl Cursor for UnorderedCursor {}
714
715 assert_eq!(UnorderedCursor.id(), CursorId::global());
716 assert_eq!(UnorderedCursor.order_key(), CursorOrder::min());
717 }
718
719 #[test]
720 fn serializes_as_plain_string() {
721 let id = CursorId::global();
722 let v = serde_json::to_value(id).unwrap();
723 assert_eq!(v.as_str(), Some("global"));
724
725 let id = CursorId::partition(partition(4, 1));
726 let v = serde_json::to_value(id).unwrap();
727 assert_eq!(v.as_str(), Some("partition:4:1"));
728 }
729
730 #[test]
731 fn roundtrips_via_json() {
732 let id = CursorId::global();
733 let v = serde_json::to_value(id.clone()).unwrap();
734 let back: CursorId = serde_json::from_value(v).unwrap();
735 assert_eq!(back, id);
736
737 let id = CursorId::partition(partition(4, 2));
738 let v = serde_json::to_value(id.clone()).unwrap();
739 let back: CursorId = serde_json::from_value(v).unwrap();
740 assert_eq!(back, id);
741 }
742
743 #[test]
744 fn display_output_matches_as_str() {
745 let id = CursorId::global();
746 assert_eq!(id.to_string(), "global");
747
748 let id = CursorId::partition(partition(4, 1));
749 assert_eq!(id.to_string(), "partition:4:1");
750 }
751
752 #[test]
753 fn no_cursor_uses_global_cursor_id() {
754 assert_eq!(NoCursor.id(), CursorId::global());
755 }
756}