Skip to main content

eventuary_core/io/
cursor.rs

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}