1use std::ops::{Bound, Range};
5
6use bytes::Bytes;
7use sui_inverted_index::ScanDirection;
8use sui_rpc::proto::sui::rpc::v2::Ordering as ProtoOrdering;
9use sui_rpc::proto::sui::rpc::v2::QueryOptions as ProtoQueryOptions;
10use sui_rpc_cursor::CursorToken;
11use sui_rpc_cursor::Position;
12
13use crate::ErrorReason;
14use crate::RpcError;
15use crate::proto::google::rpc::bad_request::FieldViolation;
16
17const ORDERING_ASCENDING: i32 = ProtoOrdering::Ascending as i32;
18const ORDERING_DESCENDING: i32 = ProtoOrdering::Descending as i32;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
21pub enum Ordering {
22 Ascending,
23 Descending,
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
30pub struct IntraTxCoordinate {
31 pub tx_seq: u64,
32 pub index: u32,
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum RangeExhaustion {
41 LedgerTip,
43 CheckpointBound,
45 CursorBound { kind: sui_rpc_cursor::CursorKind },
50 ServingFloor { floor_checkpoint: u64 },
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct QueryOptions {
60 pub limit_items: usize,
61 pub ordering: Ordering,
62 after: Option<CursorToken>,
63 before: Option<CursorToken>,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct ResolvedCheckpointRange {
73 pub range: Range<u64>,
76 pub exhaustion: RangeExhaustion,
78 pub reaches_ledger_tip: bool,
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct ScanBounds<P> {
87 pub lo: Bound<P>,
88 pub hi: Bound<P>,
89}
90
91pub type IntraTxScanBounds = ScanBounds<IntraTxCoordinate>;
92
93#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct ResolvedScan<P> {
97 pub bounds: ScanBounds<P>,
100 pub entry_checkpoint: u64,
105 pub end_checkpoint: u64,
107 pub end_position: P,
109 pub exhaustion: RangeExhaustion,
111 pub window_terminal_checkpoint: Option<u64>,
118}
119
120pub trait ScanCursor<P> {
123 fn coordinate(&self) -> P;
124}
125
126pub trait ScanPosition: Copy + Ord {
129 fn successor(self) -> Option<Self>;
131}
132
133impl ScanPosition for u64 {
134 fn successor(self) -> Option<Self> {
135 self.checked_add(1)
136 }
137}
138
139impl ScanPosition for IntraTxCoordinate {
140 fn successor(self) -> Option<Self> {
141 match self.index.checked_add(1) {
142 Some(index) => Some(Self {
143 tx_seq: self.tx_seq,
144 index,
145 }),
146 None => self.tx_seq.checked_add(1).map(Self::start_of_tx),
147 }
148 }
149}
150
151impl IntraTxCoordinate {
152 pub fn start_of_tx(tx_seq: u64) -> Self {
155 Self { tx_seq, index: 0 }
156 }
157
158 pub fn tx_window(range: Range<u64>) -> Range<Self> {
160 Self::start_of_tx(range.start)..Self::start_of_tx(range.end)
161 }
162}
163
164pub fn below_serving_floor(floor_checkpoint: u64) -> RpcError {
167 RpcError::new(
168 tonic::Code::OutOfRange,
169 format!(
170 "requested data below earliest available; lowest available checkpoint is {floor_checkpoint}"
171 ),
172 )
173}
174
175impl QueryOptions {
176 pub fn checkpoints_from_proto(
177 request: Option<&ProtoQueryOptions>,
178 default_limit_items: u32,
179 max_limit_items: u32,
180 ) -> Result<Self, RpcError> {
181 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
182 matches!(position, Position::Checkpoints { .. })
183 })
184 }
185
186 pub fn transactions_from_proto(
187 request: Option<&ProtoQueryOptions>,
188 default_limit_items: u32,
189 max_limit_items: u32,
190 ) -> Result<Self, RpcError> {
191 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
192 matches!(position, Position::Transactions { .. })
193 })
194 }
195
196 pub fn events_from_proto(
197 request: Option<&ProtoQueryOptions>,
198 default_limit_items: u32,
199 max_limit_items: u32,
200 ) -> Result<Self, RpcError> {
201 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
202 matches!(position, Position::Events { .. })
203 })
204 }
205
206 fn from_proto_with_position(
207 request: Option<&ProtoQueryOptions>,
208 default_limit_items: u32,
209 max_limit_items: u32,
210 position_matches: fn(&Position) -> bool,
211 ) -> Result<Self, RpcError> {
212 let limit_items = request
213 .and_then(|options| options.limit)
214 .unwrap_or(default_limit_items)
215 .clamp(1, max_limit_items) as usize;
216
217 let ordering = match request.and_then(|options| options.ordering) {
218 None | Some(ORDERING_ASCENDING) => Ordering::Ascending,
219 Some(ORDERING_DESCENDING) => Ordering::Descending,
220 Some(_) => {
221 return Err(FieldViolation::new("options.ordering")
222 .with_description("invalid ordering")
223 .with_reason(ErrorReason::FieldInvalid)
224 .into());
225 }
226 };
227
228 let after = parse_cursor(
229 "options.after",
230 request.and_then(|options| options.after.as_ref()),
231 position_matches,
232 )?;
233 let before = parse_cursor(
234 "options.before",
235 request.and_then(|options| options.before.as_ref()),
236 position_matches,
237 )?;
238
239 Ok(Self {
240 limit_items,
241 ordering,
242 after,
243 before,
244 })
245 }
246
247 pub fn subscription() -> Self {
251 Self {
252 limit_items: usize::MAX,
253 ordering: Ordering::Ascending,
254 after: None,
255 before: None,
256 }
257 }
258
259 pub fn scan_direction(&self) -> ScanDirection {
260 match self.ordering {
261 Ordering::Ascending => ScanDirection::Ascending,
262 Ordering::Descending => ScanDirection::Descending,
263 }
264 }
265
266 pub fn is_ascending(&self) -> bool {
267 matches!(self.ordering, Ordering::Ascending)
268 }
269
270 pub fn has_after_cursor(&self) -> bool {
277 self.after.is_some()
278 }
279}
280
281impl ResolvedCheckpointRange {
282 pub fn empty_at(
283 checkpoint: u64,
284 exhaustion: RangeExhaustion,
285 reaches_ledger_tip: bool,
286 ) -> Self {
287 Self {
288 range: checkpoint..checkpoint,
289 exhaustion,
290 reaches_ledger_tip,
291 }
292 }
293
294 pub fn is_empty(&self) -> bool {
295 self.range.is_empty()
296 }
297
298 pub fn terminal_checkpoint(&self, ordering: Ordering) -> u64 {
299 match ordering {
300 Ordering::Ascending => self.range.end,
301 Ordering::Descending => self.range.start,
302 }
303 }
304}
305
306impl ResolvedScan<u64> {
307 pub fn range(&self) -> Range<u64> {
309 self.bounds.to_range()
310 }
311}
312
313impl<P: Copy + Ord> ScanBounds<P> {
314 pub fn from_range(range: Range<P>) -> Self {
315 Self {
316 lo: Bound::Included(range.start),
317 hi: Bound::Excluded(range.end),
318 }
319 }
320
321 pub fn empty_at(position: P) -> Self {
322 Self {
323 lo: Bound::Included(position),
324 hi: Bound::Excluded(position),
325 }
326 }
327
328 pub fn is_empty(&self) -> bool {
329 match (self.lo, self.hi) {
330 (Bound::Included(a), Bound::Excluded(b))
331 | (Bound::Excluded(a), Bound::Excluded(b))
332 | (Bound::Excluded(a), Bound::Included(b)) => a >= b,
333 (Bound::Included(a), Bound::Included(b)) => a > b,
334 (Bound::Unbounded, _) | (_, Bound::Unbounded) => false,
335 }
336 }
337
338 pub fn contains(&self, position: P) -> bool {
339 let above_lo = match self.lo {
340 Bound::Included(lo) => position >= lo,
341 Bound::Excluded(lo) => position > lo,
342 Bound::Unbounded => true,
343 };
344 let below_hi = match self.hi {
345 Bound::Included(hi) => position <= hi,
346 Bound::Excluded(hi) => position < hi,
347 Bound::Unbounded => true,
348 };
349 above_lo && below_hi
350 }
351}
352
353impl<P: ScanPosition> ScanBounds<P> {
354 pub fn admits_no_position(&self) -> bool {
358 let lo = match self.lo {
359 Bound::Excluded(position) => match position.successor() {
360 Some(successor) => Bound::Included(successor),
361 None => return true,
362 },
363 lo => lo,
364 };
365 ScanBounds { lo, hi: self.hi }.is_empty()
366 }
367}
368
369impl ScanBounds<u64> {
370 pub fn to_range(&self) -> Range<u64> {
371 let start = match self.lo {
372 Bound::Included(position) => position,
373 Bound::Excluded(position) => position.saturating_add(1),
374 Bound::Unbounded => 0,
375 };
376 let end = match self.hi {
377 Bound::Included(position) => position.saturating_add(1),
378 Bound::Excluded(position) => position,
379 Bound::Unbounded => u64::MAX,
380 };
381 start..end
382 }
383}
384
385impl ScanBounds<IntraTxCoordinate> {
386 pub fn tx_range(&self) -> Option<Range<u64>> {
391 let start_tx = match self.lo {
392 Bound::Included(position) | Bound::Excluded(position) => position.tx_seq,
393 Bound::Unbounded => 0,
394 };
395 let end_tx = match self.hi {
396 Bound::Excluded(position) if position.index == 0 => position.tx_seq,
397 Bound::Included(position) | Bound::Excluded(position) => {
398 position.tx_seq.saturating_add(1)
399 }
400 Bound::Unbounded => u64::MAX,
401 };
402 (start_tx < end_tx).then_some(start_tx..end_tx)
403 }
404}
405
406impl<P: ScanPosition> ResolvedScan<P>
407where
408 CursorToken: ScanCursor<P>,
409{
410 pub fn resolve(
415 cp_range: ResolvedCheckpointRange,
416 range: Range<P>,
417 options: &QueryOptions,
418 ) -> Result<Self, RpcError> {
419 if cp_range.reaches_ledger_tip {
420 reject_cursors_past_tip(options, range.end, cp_range.range.end)?;
421 }
422
423 let entry_checkpoint = if cp_range.is_empty() {
424 cp_range.range.end
426 } else if options.is_ascending() {
427 cp_range.range.start
428 } else {
429 cp_range.range.end - 1
430 };
431
432 let resolved = Self {
433 bounds: ScanBounds::from_range(range.start..range.end),
434 entry_checkpoint,
435 end_checkpoint: cp_range.terminal_checkpoint(options.ordering),
436 end_position: match options.ordering {
437 Ordering::Ascending => range.end,
438 Ordering::Descending => range.start,
439 },
440 exhaustion: cp_range.exhaustion,
441 window_terminal_checkpoint: (!cp_range.is_empty())
442 .then(|| cp_range.terminal_checkpoint(options.ordering)),
443 }
444 .apply_cursor_bounds(options);
445
446 if !cp_range.is_empty()
447 && resolved.bounds.admits_no_position()
448 && let Some((checkpoint, position)) =
449 starting_cursor_end(options, &cp_range.range, &range)
450 {
451 return Ok(Self {
455 bounds: ScanBounds::empty_at(position),
456 end_checkpoint: checkpoint,
457 end_position: position,
458 exhaustion: cp_range.exhaustion,
459 ..resolved
460 });
461 }
462 Ok(resolved)
463 }
464
465 pub fn is_empty(&self) -> bool {
466 self.bounds.is_empty()
467 }
468
469 pub fn natural_end_claim(&self, options: &QueryOptions) -> Option<u64> {
476 let window_terminal_checkpoint = self.window_terminal_checkpoint?;
477 match options.ordering {
478 Ordering::Ascending => window_terminal_checkpoint.checked_sub(1),
479 Ordering::Descending => Some(window_terminal_checkpoint),
480 }
481 }
482
483 fn apply_cursor_bounds(mut self, options: &QueryOptions) -> Self {
484 if self.is_empty() {
485 return self;
486 }
487
488 let mut cursor_terminal = self.apply_after_cursor(options);
489
490 if let Some(recording) = self.apply_before_cursor(options) {
495 cursor_terminal = Some(recording);
496 }
497
498 if let Some((checkpoint, position, kind)) = cursor_terminal {
499 self.set_terminal_record(checkpoint, position, RangeExhaustion::CursorBound { kind });
500
501 self.bounds = ScanBounds::empty_at(self.end_position);
502 }
503
504 self
505 }
506
507 fn set_terminal_record(&mut self, checkpoint: u64, position: P, exhaustion: RangeExhaustion) {
508 self.end_checkpoint = checkpoint;
509 self.end_position = position;
510 self.exhaustion = exhaustion;
511 }
512
513 fn apply_after_cursor(
515 &mut self,
516 options: &QueryOptions,
517 ) -> Option<(u64, P, sui_rpc_cursor::CursorKind)> {
518 let cursor = options.after.as_ref()?;
519 let checkpoint = cursor.position.checkpoint();
520 let position: P = cursor.coordinate();
521
522 let candidate = match cursor.kind {
525 sui_rpc_cursor::CursorKind::Item => Bound::Excluded(position),
526 sui_rpc_cursor::CursorKind::Boundary => Bound::Included(position),
527 };
528
529 if !lower_bound_gte(candidate, self.bounds.lo) {
530 return None;
531 }
532
533 let (descending_terminal, descending_terminal_kind) = match cursor.kind {
541 sui_rpc_cursor::CursorKind::Boundary => {
542 (position, sui_rpc_cursor::CursorKind::Boundary)
543 }
544 sui_rpc_cursor::CursorKind::Item => match position.successor() {
545 Some(next) => (next, sui_rpc_cursor::CursorKind::Boundary),
546 None => (position, sui_rpc_cursor::CursorKind::Item),
547 },
548 };
549 if !options.is_ascending() {
550 self.set_terminal_record(
551 checkpoint,
552 descending_terminal,
553 RangeExhaustion::CursorBound {
554 kind: descending_terminal_kind,
555 },
556 );
557 }
558
559 self.bounds.lo = candidate;
560
561 if !self.bounds.is_empty() {
562 return None;
563 }
564
565 if options.is_ascending() {
569 Some((checkpoint, position, cursor.kind))
570 } else {
571 Some((checkpoint, descending_terminal, descending_terminal_kind))
572 }
573 }
574
575 fn apply_before_cursor(
577 &mut self,
578 options: &QueryOptions,
579 ) -> Option<(u64, P, sui_rpc_cursor::CursorKind)> {
580 let cursor = options.before.as_ref()?;
581 let checkpoint = cursor.position.checkpoint();
582 let position: P = cursor.coordinate();
583
584 if !hi_admits_upper_bound(self.bounds.hi, position) {
585 return None;
586 }
587
588 if options.is_ascending() {
589 self.set_terminal_record(
590 checkpoint,
591 position,
592 RangeExhaustion::CursorBound {
593 kind: sui_rpc_cursor::CursorKind::Boundary,
594 },
595 );
596 }
597
598 self.bounds.hi = Bound::Excluded(position);
599
600 if !self.bounds.is_empty() {
601 return None;
602 }
603
604 Some((checkpoint, position, sui_rpc_cursor::CursorKind::Boundary))
605 }
606
607 pub fn apply_serving_floor(
615 &mut self,
616 floor: P,
617 floor_checkpoint: u64,
618 options: &QueryOptions,
619 ) -> Result<(), RpcError> {
620 let floored_lo = Bound::Included(floor);
621 let floored = ScanBounds {
622 lo: floored_lo,
623 hi: self.bounds.hi,
624 };
625 if floored.is_empty() {
626 return Err(below_serving_floor(floor_checkpoint));
627 }
628 self.bounds.lo = floored_lo;
629 if options.is_ascending() {
630 self.entry_checkpoint = self.entry_checkpoint.max(floor_checkpoint);
631 } else {
632 self.set_terminal_record(
633 floor_checkpoint,
634 floor,
635 RangeExhaustion::ServingFloor { floor_checkpoint },
636 );
637 }
638 Ok(())
639 }
640}
641
642pub fn validate_checkpoint_bounds(
647 start_checkpoint: Option<u64>,
648 end_checkpoint: Option<u64>,
649) -> Result<(), RpcError> {
650 let start = start_checkpoint.unwrap_or(0);
651 if let Some(end) = end_checkpoint
652 && end < start
653 {
654 return Err(FieldViolation::new("end_checkpoint")
655 .with_description("end_checkpoint must be greater than or equal to start_checkpoint")
656 .with_reason(ErrorReason::FieldInvalid)
657 .into());
658 }
659 Ok(())
660}
661
662impl ResolvedCheckpointRange {
663 pub fn from_request(
678 start_checkpoint: Option<u64>,
679 end_checkpoint: Option<u64>,
680 checkpoint_hi_exclusive: u64,
681 options: &QueryOptions,
682 ) -> Result<Self, RpcError> {
683 validate_checkpoint_bounds(start_checkpoint, end_checkpoint)?;
684 let start = start_checkpoint.unwrap_or(0);
685 let descending_past_tip = match end_checkpoint {
686 _ if options.is_ascending() => None,
687 Some(end) if end > checkpoint_hi_exclusive => Some(("end_checkpoint", end)),
688 None if start >= checkpoint_hi_exclusive => Some(("start_checkpoint", start)),
689 Some(_) | None => None,
690 };
691 if let Some((field, requested)) = descending_past_tip {
692 return Err(RpcError::new(
693 tonic::Code::OutOfRange,
694 format!(
695 "requested data above latest available; {field} is {requested}, and only \
696 checkpoints below {checkpoint_hi_exclusive} are available"
697 ),
698 ));
699 }
700 let requested_end = end_checkpoint.unwrap_or(checkpoint_hi_exclusive);
701 let window_reaches_tip =
703 end_checkpoint.is_none() || requested_end > checkpoint_hi_exclusive;
704 let end = requested_end.min(checkpoint_hi_exclusive);
705 let exhaustion = match options.ordering {
706 Ordering::Ascending if window_reaches_tip => RangeExhaustion::LedgerTip,
707 Ordering::Ascending | Ordering::Descending => RangeExhaustion::CheckpointBound,
708 };
709
710 if options.is_ascending() && start >= checkpoint_hi_exclusive {
711 return Ok(Self::empty_at(
712 checkpoint_hi_exclusive,
713 RangeExhaustion::LedgerTip,
714 window_reaches_tip,
715 ));
716 }
717 if start >= end {
718 let checkpoint = match options.ordering {
719 Ordering::Ascending => end,
720 Ordering::Descending => start,
721 };
722 return Ok(Self::empty_at(
723 checkpoint,
724 exhaustion,
725 window_reaches_tip && checkpoint == checkpoint_hi_exclusive,
726 ));
727 }
728 Ok(Self {
729 range: start..end,
730 exhaustion,
731 reaches_ledger_tip: window_reaches_tip && end == checkpoint_hi_exclusive,
732 })
733 }
734}
735
736fn starting_cursor_end<P: ScanPosition>(
755 options: &QueryOptions,
756 checkpoints: &Range<u64>,
757 window: &Range<P>,
758) -> Option<(u64, P)>
759where
760 CursorToken: ScanCursor<P>,
761{
762 let position = |cursor: &CursorToken| -> P { cursor.coordinate() };
763 match options.ordering {
764 Ordering::Ascending => {
765 if options
766 .before
767 .as_ref()
768 .is_some_and(|before| position(before) <= window.end)
769 {
770 return None;
771 }
772 let after = options.after.as_ref()?;
773 let first_admitted = match after.kind {
774 sui_rpc_cursor::CursorKind::Item => position(after).successor()?,
775 sui_rpc_cursor::CursorKind::Boundary => position(after),
776 };
777 match first_admitted.cmp(&window.end) {
778 std::cmp::Ordering::Less => None,
779 std::cmp::Ordering::Equal => Some((checkpoints.end, window.end)),
780 std::cmp::Ordering::Greater => Some((after.position.checkpoint(), first_admitted)),
781 }
782 }
783 Ordering::Descending => {
784 if options
785 .after
786 .as_ref()
787 .is_some_and(|after| position(after) >= window.start)
788 {
789 return None;
790 }
791 let before = options.before.as_ref()?;
792 let upper = position(before);
793 match upper.cmp(&window.start) {
794 std::cmp::Ordering::Greater => None,
795 std::cmp::Ordering::Equal => Some((checkpoints.start, window.start)),
796 std::cmp::Ordering::Less => Some((before.position.checkpoint(), upper)),
797 }
798 }
799 }
800}
801
802fn reject_cursors_past_tip<P: Ord>(
813 options: &QueryOptions,
814 tip: P,
815 checkpoint_hi_exclusive: u64,
816) -> Result<(), RpcError>
817where
818 CursorToken: ScanCursor<P>,
819{
820 let cursors = match options.ordering {
821 Ordering::Ascending => [options.after.as_ref(), None],
822 Ordering::Descending => [options.before.as_ref(), options.after.as_ref()],
823 };
824 for cursor in cursors.into_iter().flatten() {
825 let position = cursor.coordinate();
826 let past_tip = match cursor.kind {
829 sui_rpc_cursor::CursorKind::Item => position >= tip,
830 sui_rpc_cursor::CursorKind::Boundary => position > tip,
831 };
832 if past_tip {
833 return Err(RpcError::new(
834 tonic::Code::OutOfRange,
835 format!(
836 "requested data above latest available; cursor is at checkpoint {}, and only \
837 checkpoints below {checkpoint_hi_exclusive} are available",
838 cursor.position.checkpoint(),
839 ),
840 ));
841 }
842 }
843 Ok(())
844}
845
846impl ScanCursor<u64> for CursorToken {
847 fn coordinate(&self) -> u64 {
848 match self.position {
849 Position::Checkpoints { checkpoint } => checkpoint,
850 Position::Transactions { tx_seq, .. } => tx_seq,
851 Position::Events { .. } => unreachable!("validated at decode"),
852 }
853 }
854}
855
856impl ScanCursor<IntraTxCoordinate> for CursorToken {
857 fn coordinate(&self) -> IntraTxCoordinate {
858 match self.position {
859 Position::Events {
860 tx_seq,
861 event_index,
862 ..
863 } => IntraTxCoordinate {
864 tx_seq,
865 index: event_index,
866 },
867 _ => unreachable!("validated at decode"),
868 }
869 }
870}
871
872impl From<IntraTxCoordinate> for (u64, u32) {
873 fn from(position: IntraTxCoordinate) -> Self {
874 (position.tx_seq, position.index)
875 }
876}
877
878impl From<(u64, u32)> for IntraTxCoordinate {
879 fn from((tx_seq, index): (u64, u32)) -> Self {
880 Self { tx_seq, index }
881 }
882}
883
884fn lower_bound_gte<P: Ord + Copy>(candidate: Bound<P>, current: Bound<P>) -> bool {
885 let Some(candidate) = lower_bound_key(candidate) else {
886 return false;
887 };
888 match lower_bound_key(current) {
889 Some(current) => candidate >= current,
890 None => true,
891 }
892}
893
894fn lower_bound_key<P: Ord + Copy>(bound: Bound<P>) -> Option<(P, u8)> {
895 match bound {
896 Bound::Included(position) => Some((position, 0)),
897 Bound::Excluded(position) => Some((position, 1)),
898 Bound::Unbounded => None,
899 }
900}
901
902fn hi_admits_upper_bound<P: Ord + Copy>(current: Bound<P>, candidate: P) -> bool {
903 match current {
904 Bound::Included(position) | Bound::Excluded(position) => candidate <= position,
905 Bound::Unbounded => true,
906 }
907}
908
909fn parse_cursor(
910 field: &'static str,
911 cursor: Option<&Bytes>,
912 position_matches: fn(&Position) -> bool,
913) -> Result<Option<CursorToken>, RpcError> {
914 cursor
915 .map(|cursor| {
916 CursorToken::decode(cursor).map_err(|_| invalid_cursor(field, "invalid cursor"))
917 })
918 .transpose()?
919 .map(|token| {
920 if position_matches(&token.position) {
921 Ok(token)
922 } else {
923 Err(invalid_cursor(field, "invalid cursor"))
924 }
925 })
926 .transpose()
927}
928
929fn invalid_cursor(field: &'static str, description: impl Into<String>) -> RpcError {
930 FieldViolation::new(field)
931 .with_description(description)
932 .with_reason(ErrorReason::FieldInvalid)
933 .into()
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 fn query_options_from_proto(
941 request: Option<&ProtoQueryOptions>,
942 ) -> Result<QueryOptions, RpcError> {
943 QueryOptions::transactions_from_proto(request, 100, 1_000)
944 }
945
946 fn resolved_range(range: Range<u64>) -> ResolvedScan<u64> {
947 ResolvedScan {
948 bounds: ScanBounds::from_range(range),
949 end_checkpoint: 20,
950 end_position: 20,
951 exhaustion: RangeExhaustion::CheckpointBound,
952 entry_checkpoint: 0,
953 window_terminal_checkpoint: Some(20),
954 }
955 }
956
957 fn empty_resolved_range(
959 end_checkpoint: u64,
960 end_position: u64,
961 exhaustion: RangeExhaustion,
962 ) -> ResolvedScan<u64> {
963 ResolvedScan {
964 bounds: ScanBounds::empty_at(end_position),
965 end_checkpoint,
966 end_position,
967 exhaustion,
968 entry_checkpoint: 0,
969 window_terminal_checkpoint: Some(20),
970 }
971 }
972
973 fn tx_item(checkpoint: u64, tx_seq: u64) -> CursorToken {
974 CursorToken::item(Position::Transactions { checkpoint, tx_seq })
975 }
976
977 fn tx_boundary(checkpoint: u64, tx_seq: u64) -> CursorToken {
978 CursorToken::boundary(Position::Transactions { checkpoint, tx_seq })
979 }
980
981 fn cp_item(checkpoint: u64) -> CursorToken {
982 CursorToken::item(Position::Checkpoints { checkpoint })
983 }
984
985 fn ev_item(checkpoint: u64, tx_seq: u64, index: u32) -> CursorToken {
986 CursorToken::item(Position::Events {
987 checkpoint,
988 tx_seq,
989 event_index: index,
990 })
991 }
992
993 fn ev_boundary(checkpoint: u64, tx_seq: u64, index: u32) -> CursorToken {
994 CursorToken::boundary(Position::Events {
995 checkpoint,
996 tx_seq,
997 event_index: index,
998 })
999 }
1000
1001 fn resolved_intra_tx() -> ResolvedScan<IntraTxCoordinate> {
1002 ResolvedScan {
1003 bounds: IntraTxScanBounds::from_range(
1004 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10),
1005 ),
1006 entry_checkpoint: 2,
1007 end_checkpoint: 9,
1008 end_position: IntraTxCoordinate::start_of_tx(10),
1009 exhaustion: RangeExhaustion::CheckpointBound,
1010 window_terminal_checkpoint: Some(9),
1011 }
1012 }
1013
1014 fn after_options(ordering: Ordering, cursor: CursorToken) -> QueryOptions {
1015 QueryOptions {
1016 limit_items: 100,
1017 ordering,
1018 after: Some(cursor),
1019 before: None,
1020 }
1021 }
1022
1023 fn before_options(ordering: Ordering, cursor: CursorToken) -> QueryOptions {
1024 QueryOptions {
1025 limit_items: 100,
1026 ordering,
1027 after: None,
1028 before: Some(cursor),
1029 }
1030 }
1031
1032 fn directional_options(ascending: bool) -> QueryOptions {
1033 let mut request = ProtoQueryOptions::default();
1034 if !ascending {
1035 request.ordering = Some(ProtoOrdering::Descending as i32);
1036 }
1037 query_options_from_proto(Some(&request)).unwrap()
1038 }
1039
1040 fn assert_below_serving_floor(result: Result<(), RpcError>, floor_checkpoint: u64) {
1043 let status = result
1044 .expect_err("expected OutOfRange below the serving floor")
1045 .into_status_proto();
1046 assert_eq!(status.code, tonic::Code::OutOfRange as i32, "{status:?}");
1047 assert_eq!(
1048 status.message,
1049 format!(
1050 "requested data below earliest available; lowest available checkpoint is {floor_checkpoint}"
1051 )
1052 );
1053 }
1054
1055 #[test]
1060 fn tx_serving_floor_reconciles_or_rejects_consumed_scan() {
1061 let asc = directional_options(true);
1062 let desc = directional_options(false);
1063
1064 let mut resolved = ResolvedScan {
1067 bounds: ScanBounds::from_range(0..100),
1068 end_checkpoint: 20,
1069 end_position: 100,
1070 exhaustion: RangeExhaustion::CheckpointBound,
1071 entry_checkpoint: 0,
1072 window_terminal_checkpoint: Some(20),
1073 };
1074 resolved.apply_serving_floor(50, 10, &asc).unwrap();
1075 assert_eq!(resolved.natural_end_claim(&asc), Some(19));
1076 assert_eq!(resolved.range(), 50..100);
1077 assert_eq!(resolved.entry_checkpoint, 10);
1078 assert_eq!(resolved.end_checkpoint, 20);
1079 assert_eq!(resolved.end_position, 100);
1080 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
1081
1082 let mut resolved = ResolvedScan {
1085 bounds: ScanBounds::from_range(0..100),
1086 end_checkpoint: 0,
1087 end_position: 0,
1088 exhaustion: RangeExhaustion::CheckpointBound,
1089 entry_checkpoint: 20,
1090 window_terminal_checkpoint: Some(0),
1091 };
1092 resolved.apply_serving_floor(50, 10, &desc).unwrap();
1093 assert_eq!(resolved.range(), 50..100);
1094 assert_eq!(resolved.entry_checkpoint, 20);
1095 assert_eq!(resolved.end_checkpoint, 10);
1096 assert_eq!(resolved.end_position, 50);
1097 assert_eq!(
1098 resolved.exhaustion,
1099 RangeExhaustion::ServingFloor {
1100 floor_checkpoint: 10
1101 }
1102 );
1103
1104 for floor_tx in [40, 50] {
1107 let mut resolved = ResolvedScan {
1108 bounds: ScanBounds::from_range(0..40),
1109 end_checkpoint: 8,
1110 end_position: 40,
1111 exhaustion: RangeExhaustion::CheckpointBound,
1112 entry_checkpoint: 0,
1113 window_terminal_checkpoint: Some(8),
1114 };
1115 assert_below_serving_floor(resolved.apply_serving_floor(floor_tx, 10, &asc), 10);
1116
1117 let mut resolved = ResolvedScan {
1118 bounds: ScanBounds::from_range(0..40),
1119 end_checkpoint: 0,
1120 end_position: 0,
1121 exhaustion: RangeExhaustion::CheckpointBound,
1122 entry_checkpoint: 8,
1123 window_terminal_checkpoint: Some(0),
1124 };
1125 assert_below_serving_floor(resolved.apply_serving_floor(floor_tx, 10, &desc), 10);
1126 }
1127 }
1128
1129 #[test]
1132 fn event_serving_floor_reconciles_or_rejects_consumed_scan() {
1133 let asc = directional_options(true);
1134 let desc = directional_options(false);
1135
1136 let mut resolved = ResolvedScan {
1139 bounds: IntraTxScanBounds::from_range(
1140 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(100),
1141 ),
1142 end_checkpoint: 20,
1143 end_position: IntraTxCoordinate::start_of_tx(100),
1144 exhaustion: RangeExhaustion::CheckpointBound,
1145 entry_checkpoint: 0,
1146 window_terminal_checkpoint: Some(20),
1147 };
1148 resolved
1149 .apply_serving_floor(IntraTxCoordinate::start_of_tx(50), 10, &asc)
1150 .unwrap();
1151 assert_eq!(resolved.natural_end_claim(&asc), Some(19));
1152 assert_eq!(
1153 resolved.bounds.lo,
1154 Bound::Included(IntraTxCoordinate::start_of_tx(50))
1155 );
1156 assert_eq!(resolved.entry_checkpoint, 10);
1157 assert_eq!(resolved.end_checkpoint, 20);
1158 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(100));
1159
1160 let mut resolved = ResolvedScan {
1163 bounds: IntraTxScanBounds::from_range(
1164 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(100),
1165 ),
1166 end_checkpoint: 0,
1167 end_position: IntraTxCoordinate::start_of_tx(0),
1168 exhaustion: RangeExhaustion::CheckpointBound,
1169 entry_checkpoint: 20,
1170 window_terminal_checkpoint: Some(0),
1171 };
1172 resolved
1173 .apply_serving_floor(IntraTxCoordinate::start_of_tx(50), 10, &desc)
1174 .unwrap();
1175 assert_eq!(
1176 resolved.bounds.lo,
1177 Bound::Included(IntraTxCoordinate::start_of_tx(50))
1178 );
1179 assert_eq!(resolved.entry_checkpoint, 20);
1180 assert_eq!(resolved.end_checkpoint, 10);
1181 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(50));
1182 assert_eq!(
1183 resolved.exhaustion,
1184 RangeExhaustion::ServingFloor {
1185 floor_checkpoint: 10
1186 }
1187 );
1188
1189 for floor_tx in [40, 50] {
1192 let mut resolved = ResolvedScan {
1193 bounds: IntraTxScanBounds::from_range(
1194 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(40),
1195 ),
1196 end_checkpoint: 8,
1197 end_position: IntraTxCoordinate::start_of_tx(40),
1198 exhaustion: RangeExhaustion::CheckpointBound,
1199 entry_checkpoint: 0,
1200 window_terminal_checkpoint: Some(8),
1201 };
1202 assert_below_serving_floor(
1203 resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(floor_tx), 10, &asc),
1204 10,
1205 );
1206
1207 let mut resolved = ResolvedScan {
1208 bounds: IntraTxScanBounds::from_range(
1209 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(40),
1210 ),
1211 end_checkpoint: 0,
1212 end_position: IntraTxCoordinate::start_of_tx(0),
1213 exhaustion: RangeExhaustion::CheckpointBound,
1214 entry_checkpoint: 8,
1215 window_terminal_checkpoint: Some(0),
1216 };
1217 assert_below_serving_floor(
1218 resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(floor_tx), 10, &desc),
1219 10,
1220 );
1221 }
1222 }
1223
1224 #[test]
1229 fn natural_end_claims_the_window_edge() {
1230 let asc = directional_options(true);
1231 let desc = directional_options(false);
1232 let resolved = ResolvedScan {
1233 end_checkpoint: 3,
1236 window_terminal_checkpoint: Some(10),
1237 ..resolved_range(0..100)
1238 };
1239 assert_eq!(resolved.natural_end_claim(&asc), Some(9));
1240 assert_eq!(resolved.natural_end_claim(&desc), Some(10));
1241
1242 let at_genesis = ResolvedScan {
1243 window_terminal_checkpoint: Some(0),
1244 ..resolved_range(0..100)
1245 };
1246 assert_eq!(at_genesis.natural_end_claim(&asc), None);
1247 assert_eq!(at_genesis.natural_end_claim(&desc), Some(0));
1248
1249 let empty_window = ResolvedScan {
1250 window_terminal_checkpoint: None,
1251 ..resolved
1252 };
1253 assert_eq!(empty_window.natural_end_claim(&asc), None);
1254 assert_eq!(empty_window.natural_end_claim(&desc), None);
1255 }
1256
1257 #[test]
1262 fn descending_walk_ends_at_serving_floor_and_resuming_below_it_is_out_of_range() {
1263 let floor_checkpoint = 10;
1264 let floor = first_tx(floor_checkpoint);
1265
1266 let walk = directional_options(false);
1267 let mut reached_floor = resolve_tx_request((None, None), &walk).unwrap();
1268 reached_floor
1269 .apply_serving_floor(floor, floor_checkpoint, &walk)
1270 .unwrap();
1271 assert_eq!(reached_floor.range(), floor..first_tx(TIP));
1272 assert_eq!(
1273 (
1274 reached_floor.exhaustion,
1275 reached_floor.end_checkpoint,
1276 reached_floor.end_position,
1277 ),
1278 (
1279 RangeExhaustion::ServingFloor { floor_checkpoint },
1280 floor_checkpoint,
1281 floor,
1282 )
1283 );
1284
1285 let at_floor = tx_boundary(reached_floor.end_checkpoint, reached_floor.end_position);
1286 let below_floor = tx_boundary(6, 33);
1287 for ordering in [Ordering::Descending, Ordering::Ascending] {
1288 for before in [at_floor.clone(), below_floor.clone()] {
1289 let resume = before_options(ordering, before);
1290 let mut resumed = resolve_tx_request((None, None), &resume).unwrap();
1291 assert_below_serving_floor(
1292 resumed.apply_serving_floor(floor, floor_checkpoint, &resume),
1293 floor_checkpoint,
1294 );
1295 }
1296 }
1297 }
1298
1299 #[test]
1300 fn tx_range_covers_partial_endpoint_transactions() {
1301 let bounds = IntraTxScanBounds {
1302 lo: Bound::Included(IntraTxCoordinate {
1303 tx_seq: 10,
1304 index: 2,
1305 }),
1306 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(13)),
1307 };
1308
1309 assert_eq!(bounds.tx_range(), Some(10..13));
1310 }
1311
1312 #[test]
1313 fn tx_range_keeps_tx_of_nonzero_exclusive_hi() {
1314 let bounds = IntraTxScanBounds {
1315 lo: Bound::Unbounded,
1316 hi: Bound::Excluded(IntraTxCoordinate {
1317 tx_seq: 13,
1318 index: 1,
1319 }),
1320 };
1321
1322 assert_eq!(bounds.tx_range(), Some(0..14));
1323 }
1324
1325 #[test]
1326 fn tx_range_empty_bounds_yield_none() {
1327 let bounds = IntraTxScanBounds::from_range(
1328 IntraTxCoordinate::start_of_tx(10)..IntraTxCoordinate::start_of_tx(10),
1329 );
1330 assert_eq!(bounds.tx_range(), None);
1331 }
1332
1333 #[test]
1336 fn to_range_collapses_excluded_lo_at_successor() {
1337 let bounds = ScanBounds {
1338 lo: Bound::Excluded(14u64),
1339 hi: Bound::Excluded(20u64),
1340 };
1341 assert_eq!(bounds.to_range(), 15..20);
1342
1343 let bounds = ScanBounds {
1346 lo: Bound::Excluded(u64::MAX),
1347 hi: Bound::Unbounded,
1348 };
1349 assert!(bounds.to_range().is_empty());
1350 }
1351
1352 #[test]
1353 fn parses_cursors_and_ordering() {
1354 let after = tx_item(2, 20).encode();
1355 let before = tx_item(3, 30).encode();
1356 let mut request = ProtoQueryOptions::default();
1357 request.limit = Some(500);
1358 request.after = Some(after);
1359 request.before = Some(before);
1360 request.ordering = Some(ProtoOrdering::Descending as i32);
1361
1362 let options = query_options_from_proto(Some(&request)).unwrap();
1363
1364 assert_eq!(options.limit_items, 500);
1365 assert_eq!(options.ordering, Ordering::Descending);
1366 assert_eq!(options.scan_direction(), ScanDirection::Descending);
1367 assert_eq!(
1368 resolved_range(0..100).apply_cursor_bounds(&options).range(),
1369 21..30
1370 );
1371 }
1372
1373 #[test]
1374 fn has_after_cursor_reflects_only_the_after_field() {
1375 let options = query_options_from_proto(Some(&ProtoQueryOptions::default())).unwrap();
1377 assert!(!options.has_after_cursor());
1378
1379 let mut request = ProtoQueryOptions::default();
1381 request.before = Some(tx_item(3, 30).encode());
1382 let options = query_options_from_proto(Some(&request)).unwrap();
1383 assert!(!options.has_after_cursor());
1384
1385 let mut request = ProtoQueryOptions::default();
1387 request.after = Some(tx_item(2, 20).encode());
1388 let options = query_options_from_proto(Some(&request)).unwrap();
1389 assert!(options.has_after_cursor());
1390 }
1391
1392 #[test]
1393 fn clamps_limit_items_and_defaults_to_ascending() {
1394 let mut request = ProtoQueryOptions::default();
1395 request.limit = Some(5_000);
1396
1397 let options = query_options_from_proto(Some(&request)).unwrap();
1398
1399 assert_eq!(options.limit_items, 1_000);
1400 assert_eq!(options.ordering, Ordering::Ascending);
1401 assert_eq!(options.scan_direction(), ScanDirection::Ascending);
1402 }
1403
1404 #[test]
1405 fn rejects_malformed_cursors_and_unknown_ordering() {
1406 let mut request = ProtoQueryOptions::default();
1407 request.after = Some(Bytes::from_static(b"short"));
1408 assert!(query_options_from_proto(Some(&request)).is_err());
1409
1410 let mut request = ProtoQueryOptions::default();
1411 request.before = Some(Bytes::from_static(b"short"));
1412 assert!(query_options_from_proto(Some(&request)).is_err());
1413
1414 let mut request = ProtoQueryOptions::default();
1415 request.ordering = Some(99);
1416 assert!(query_options_from_proto(Some(&request)).is_err());
1417 }
1418
1419 #[test]
1420 fn rejects_cursor_for_different_position_variant() {
1421 let token = cp_item(9).encode();
1422 let mut request = ProtoQueryOptions::default();
1423 request.after = Some(token);
1424 assert!(query_options_from_proto(Some(&request)).is_err());
1425 }
1426
1427 #[test]
1428 fn accepts_cursor_regardless_of_filter_scope() {
1429 let after = tx_item(1, 9).encode();
1434 let before = tx_item(3, 30).encode();
1435 let mut request = ProtoQueryOptions::default();
1436 request.after = Some(after);
1437 request.before = Some(before);
1438 assert!(query_options_from_proto(Some(&request)).is_ok());
1439 }
1440
1441 #[test]
1442 fn accepts_cursors_for_different_checkpoint_range_and_ordering() {
1443 let token = tx_item(9, 9).encode();
1444 let mut request = ProtoQueryOptions::default();
1445 request.after = Some(token);
1446 request.ordering = Some(ProtoOrdering::Descending as i32);
1447
1448 let options = query_options_from_proto(Some(&request)).unwrap();
1449 let range =
1450 ResolvedCheckpointRange::from_request(Some(1_000), Some(1_100), 2_000, &options)
1451 .unwrap();
1452
1453 assert_eq!(range.range, 1_000..1_100);
1454 }
1455
1456 #[test]
1457 fn applies_canonical_cursor_bounds() {
1458 let options = QueryOptions {
1459 limit_items: 2,
1460 ordering: Ordering::Ascending,
1461 after: Some(tx_item(1, 11)),
1462 before: None,
1463 };
1464 assert_eq!(
1465 resolved_range(10..20).apply_cursor_bounds(&options).range(),
1466 12..20
1467 );
1468
1469 let options = QueryOptions {
1470 after: Some(tx_item(1, u64::MAX)),
1471 ..options
1472 };
1473 assert_eq!(
1474 resolved_range(10..20).apply_cursor_bounds(&options),
1475 empty_resolved_range(
1476 1,
1477 u64::MAX,
1478 RangeExhaustion::CursorBound {
1479 kind: sui_rpc_cursor::CursorKind::Item,
1480 },
1481 )
1482 );
1483
1484 let options = QueryOptions {
1485 ordering: Ordering::Descending,
1486 after: Some(tx_item(1, 11)),
1487 before: Some(tx_item(1, 19)),
1488 ..options
1489 };
1490 let bounded = resolved_range(10..20).apply_cursor_bounds(&options);
1491 assert_eq!(bounded.range(), 12..19);
1492 assert_eq!(
1493 bounded.exhaustion,
1494 RangeExhaustion::CursorBound {
1495 kind: sui_rpc_cursor::CursorKind::Boundary,
1496 }
1497 );
1498 assert_eq!(bounded.end_position, 12);
1500 }
1501
1502 #[test]
1507 fn descending_before_adjacent_to_after_empties_above_the_after_item() {
1508 let options = QueryOptions {
1509 limit_items: 2,
1510 ordering: Ordering::Descending,
1511 after: Some(tx_item(1, 11)),
1512 before: Some(tx_item(1, 12)),
1513 };
1514 let crossed = resolved_range(10..20).apply_cursor_bounds(&options);
1515 assert_eq!(
1516 crossed,
1517 ResolvedScan {
1518 bounds: ScanBounds {
1519 lo: Bound::Excluded(11),
1520 hi: Bound::Excluded(12),
1521 },
1522 entry_checkpoint: 0,
1523 end_checkpoint: 1,
1524 end_position: 12,
1525 exhaustion: RangeExhaustion::CursorBound {
1526 kind: sui_rpc_cursor::CursorKind::Boundary,
1527 },
1528 window_terminal_checkpoint: Some(20),
1529 }
1530 );
1531 assert!(crossed.range().is_empty());
1532 }
1533
1534 #[test]
1535 fn applies_boundary_cursor_bounds_without_item_offset() {
1536 let options = QueryOptions {
1537 limit_items: 2,
1538 ordering: Ordering::Ascending,
1539 after: Some(tx_boundary(2, 20)),
1540 before: None,
1541 };
1542 assert_eq!(
1543 resolved_range(10..30).apply_cursor_bounds(&options).range(),
1544 20..30
1545 );
1546
1547 let options = QueryOptions {
1548 ordering: Ordering::Descending,
1549 after: None,
1550 before: Some(tx_boundary(2, 20)),
1551 ..options
1552 };
1553 assert_eq!(
1554 resolved_range(10..30).apply_cursor_bounds(&options).range(),
1555 10..20
1556 );
1557 }
1558
1559 #[test]
1560 fn resolves_checkpoint_range_with_terminal_reason() {
1561 let options = query_options_from_proto(None).unwrap();
1562 assert_eq!(
1563 ResolvedCheckpointRange::from_request(None, None, 20, &options)
1564 .unwrap()
1565 .exhaustion,
1566 RangeExhaustion::LedgerTip
1567 );
1568 assert!(ResolvedCheckpointRange::from_request(Some(10), Some(9), 20, &options).is_err());
1569
1570 let resolved = ResolvedCheckpointRange::from_request(Some(10), None, 20, &options).unwrap();
1571 assert_eq!(resolved.range, 10..20);
1572 assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip);
1573
1574 assert_eq!(
1575 ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap(),
1576 ResolvedCheckpointRange::empty_at(20, RangeExhaustion::LedgerTip, true)
1577 );
1578 }
1579
1580 #[test]
1584 fn resolves_checkpoint_range_no_longer_clamped_by_width() {
1585 let options = query_options_from_proto(None).unwrap();
1586 let resolved =
1587 ResolvedCheckpointRange::from_request(Some(10), Some(10_000_000), 10_000_000, &options)
1588 .unwrap();
1589 assert_eq!(resolved.range, 10..10_000_000);
1590 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
1591 }
1592
1593 #[test]
1596 fn ascending_window_past_tip_empties_with_ledger_tip() {
1597 let options = QueryOptions::events_from_proto(None, 100, 100).unwrap();
1598 let cp_range = ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap();
1599 assert!(cp_range.is_empty());
1600 assert_eq!(
1601 ResolvedScan::<IntraTxCoordinate>::resolve(
1602 cp_range,
1603 IntraTxCoordinate::tx_window(100..100),
1604 &options
1605 )
1606 .unwrap(),
1607 ResolvedScan {
1608 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(100)),
1609 entry_checkpoint: 20,
1610 end_checkpoint: 20,
1611 end_position: IntraTxCoordinate::start_of_tx(100),
1612 exhaustion: RangeExhaustion::LedgerTip,
1613 window_terminal_checkpoint: None,
1614 }
1615 );
1616 }
1617
1618 #[test]
1623 fn descending_window_past_tip_is_out_of_range() {
1624 let mut request = ProtoQueryOptions::default();
1625 request.ordering = Some(ProtoOrdering::Descending as i32);
1626 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1627 for (start, end) in [
1628 (Some(30), None),
1629 (Some(20), None),
1630 (Some(10), Some(21)),
1631 (None, Some(u64::MAX)),
1632 (Some(25), Some(30)),
1633 ] {
1634 let error = ResolvedCheckpointRange::from_request(start, end, 20, &options)
1635 .expect_err("descending window past the tip");
1636 assert_eq!(
1637 error.into_status_proto().code,
1638 tonic::Code::OutOfRange as i32,
1639 "window {start:?}..{end:?}"
1640 );
1641 }
1642
1643 let resolved = ResolvedCheckpointRange::from_request(Some(10), Some(20), 20, &options);
1644 assert_eq!(resolved.unwrap().range, 10..20);
1645 assert_eq!(
1646 ResolvedCheckpointRange::from_request(Some(20), Some(20), 20, &options).unwrap(),
1647 ResolvedCheckpointRange::empty_at(20, RangeExhaustion::CheckpointBound, false)
1648 );
1649 }
1650
1651 #[test]
1654 fn resolve_zero_width_window_empties_with_checkpoint_bound() {
1655 for ascending in [true, false] {
1656 let mut request = ProtoQueryOptions::default();
1657 if !ascending {
1658 request.ordering = Some(ProtoOrdering::Descending as i32);
1659 }
1660 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1661 let cp_range =
1662 ResolvedCheckpointRange::from_request(Some(10), Some(10), 20, &options).unwrap();
1663 assert!(cp_range.is_empty());
1664 assert_eq!(
1665 ResolvedScan::<IntraTxCoordinate>::resolve(
1666 cp_range,
1667 IntraTxCoordinate::tx_window(100..100),
1668 &options
1669 )
1670 .unwrap(),
1671 ResolvedScan {
1672 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(100)),
1673 entry_checkpoint: 10,
1674 end_checkpoint: 10,
1675 end_position: IntraTxCoordinate::start_of_tx(100),
1676 exhaustion: RangeExhaustion::CheckpointBound,
1677 window_terminal_checkpoint: None,
1678 }
1679 );
1680 }
1681 }
1682
1683 #[test]
1685 fn cursor_bounds_pass_empty_resolution_through_unchanged() {
1686 let position = Position::Events {
1687 checkpoint: 4,
1688 tx_seq: 50,
1689 event_index: 2,
1690 };
1691 for ascending in [true, false] {
1692 let mut request = ProtoQueryOptions::default();
1693 if !ascending {
1694 request.ordering = Some(ProtoOrdering::Descending as i32);
1695 }
1696 request.after = Some(CursorToken::item(position).encode());
1697 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1698 let cp_range =
1699 ResolvedCheckpointRange::from_request(Some(10), Some(10), 20, &options).unwrap();
1700 let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1701 cp_range,
1702 IntraTxCoordinate::tx_window(100..100),
1703 &options,
1704 )
1705 .unwrap();
1706 assert_eq!(resolved.clone().apply_cursor_bounds(&options), resolved);
1707 }
1708 }
1709
1710 #[test]
1713 fn resolve_orients_entry_and_terminal_by_ordering() {
1714 let options = directional_options(true);
1715 let cp_range =
1716 ResolvedCheckpointRange::from_request(Some(3), Some(10), 20, &options).unwrap();
1717 assert_eq!(cp_range.range, 3..10);
1718 let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1719 cp_range.clone(),
1720 IntraTxCoordinate::tx_window(100..200),
1721 &options,
1722 )
1723 .unwrap();
1724 assert_eq!(
1725 resolved.bounds,
1726 IntraTxScanBounds::from_range(
1727 IntraTxCoordinate::start_of_tx(100)..IntraTxCoordinate::start_of_tx(200)
1728 )
1729 );
1730 assert_eq!(resolved.entry_checkpoint, 3);
1731 assert_eq!(resolved.end_checkpoint, 10);
1732 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(200));
1733
1734 let options = directional_options(false);
1735 let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1736 cp_range,
1737 IntraTxCoordinate::tx_window(100..200),
1738 &options,
1739 )
1740 .unwrap();
1741 assert_eq!(resolved.entry_checkpoint, 9);
1742 assert_eq!(resolved.end_checkpoint, 3);
1743 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(100));
1744 }
1745
1746 #[test]
1751 fn event_after_cursor_past_window_ends_where_it_resumes() {
1752 let window = (Some(1), Some(4));
1754 for (cursor, end_position) in [
1755 (
1756 ev_item(5, 25, 0),
1757 IntraTxCoordinate {
1758 tx_seq: 25,
1759 index: 1,
1760 },
1761 ),
1762 (ev_boundary(5, 25, 0), IntraTxCoordinate::start_of_tx(25)),
1763 ] {
1764 let options = after_options(Ordering::Ascending, cursor.clone());
1765 let resolved = resolve_event_request(window, &options).unwrap();
1766 assert!(resolved.is_empty(), "{cursor:?}");
1767 assert_eq!(
1768 resolved.exhaustion,
1769 RangeExhaustion::CheckpointBound,
1770 "{cursor:?}"
1771 );
1772 assert_eq!(
1773 (resolved.end_checkpoint, resolved.end_position),
1774 (5, end_position),
1775 "{cursor:?}"
1776 );
1777 assert_eq!(resolved.natural_end_claim(&options), Some(3), "{cursor:?}");
1778 }
1779 }
1780
1781 #[test]
1783 fn event_before_cursor_empties_descending_interval() {
1784 let resolved = ResolvedScan {
1786 bounds: IntraTxScanBounds::from_range(
1788 IntraTxCoordinate::start_of_tx(100)..IntraTxCoordinate::start_of_tx(200),
1789 ),
1790 entry_checkpoint: 9,
1792 end_checkpoint: 3,
1793 end_position: IntraTxCoordinate::start_of_tx(100),
1794 exhaustion: RangeExhaustion::CheckpointBound,
1795 window_terminal_checkpoint: Some(3),
1796 };
1797
1798 let cursor = Position::Events {
1800 checkpoint: 2,
1801 tx_seq: 90,
1802 event_index: 0,
1803 };
1804
1805 for token in [CursorToken::item(cursor), CursorToken::boundary(cursor)] {
1806 let mut request = ProtoQueryOptions::default();
1807 request.ordering = Some(ProtoOrdering::Descending as i32);
1808 request.before = Some(token.encode());
1809 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1810
1811 let new_coordinate = IntraTxCoordinate::start_of_tx(90);
1812 assert_eq!(
1813 resolved.clone().apply_cursor_bounds(&options),
1814 ResolvedScan {
1815 bounds: IntraTxScanBounds::empty_at(new_coordinate),
1816 entry_checkpoint: 9,
1817 end_checkpoint: 2,
1818 end_position: new_coordinate,
1820 exhaustion: RangeExhaustion::CursorBound {
1821 kind: sui_rpc_cursor::CursorKind::Boundary,
1822 },
1823 window_terminal_checkpoint: Some(3),
1824 }
1825 );
1826 }
1827 }
1828
1829 #[test]
1832 fn after_cursor_tightens_ascending_lower_bound() {
1833 let options = after_options(Ordering::Ascending, ev_item(3, 5, 1));
1834 assert_eq!(
1835 resolved_intra_tx().apply_cursor_bounds(&options),
1836 ResolvedScan {
1837 bounds: IntraTxScanBounds {
1838 lo: Bound::Excluded(IntraTxCoordinate {
1839 tx_seq: 5,
1840 index: 1,
1841 }),
1842 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1843 },
1844 ..resolved_intra_tx()
1845 }
1846 );
1847
1848 let options = after_options(Ordering::Ascending, ev_boundary(3, 5, 1));
1849 assert_eq!(
1850 resolved_intra_tx().apply_cursor_bounds(&options),
1851 ResolvedScan {
1852 bounds: IntraTxScanBounds {
1853 lo: Bound::Included(IntraTxCoordinate {
1854 tx_seq: 5,
1855 index: 1,
1856 }),
1857 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1858 },
1859 ..resolved_intra_tx()
1860 }
1861 );
1862 }
1863
1864 #[test]
1865 fn after_cursor_below_lower_bound_changes_nothing() {
1866 let seed = ResolvedScan {
1867 bounds: IntraTxScanBounds {
1868 lo: Bound::Included(IntraTxCoordinate::start_of_tx(5)),
1869 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1870 },
1871 ..resolved_intra_tx()
1872 };
1873 let options = after_options(Ordering::Ascending, ev_boundary(4, 2, 0));
1874 assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
1875 let options = after_options(Ordering::Ascending, ev_item(4, 2, 0));
1876 assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
1877 }
1878
1879 #[test]
1883 fn descending_after_cursor_sets_terminal_and_tightens() {
1884 let options = after_options(Ordering::Descending, ev_item(3, 5, 1));
1885 assert_eq!(
1886 resolved_intra_tx().apply_cursor_bounds(&options),
1887 ResolvedScan {
1888 bounds: IntraTxScanBounds {
1889 lo: Bound::Excluded(IntraTxCoordinate {
1890 tx_seq: 5,
1891 index: 1,
1892 }),
1893 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1894 },
1895 end_checkpoint: 3,
1896 end_position: IntraTxCoordinate {
1897 tx_seq: 5,
1898 index: 2,
1899 },
1900 exhaustion: RangeExhaustion::CursorBound {
1901 kind: sui_rpc_cursor::CursorKind::Boundary,
1902 },
1903 ..resolved_intra_tx()
1904 }
1905 );
1906
1907 let options = after_options(Ordering::Descending, ev_boundary(3, 5, 1));
1908 assert_eq!(
1909 resolved_intra_tx().apply_cursor_bounds(&options),
1910 ResolvedScan {
1911 bounds: IntraTxScanBounds {
1912 lo: Bound::Included(IntraTxCoordinate {
1913 tx_seq: 5,
1914 index: 1,
1915 }),
1916 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1917 },
1918 end_checkpoint: 3,
1919 end_position: IntraTxCoordinate {
1920 tx_seq: 5,
1921 index: 1,
1922 },
1923 exhaustion: RangeExhaustion::CursorBound {
1924 kind: sui_rpc_cursor::CursorKind::Boundary,
1925 },
1926 ..resolved_intra_tx()
1927 }
1928 );
1929 }
1930
1931 #[test]
1935 fn after_cursor_empties_descending_interval() {
1936 let seed = ResolvedScan {
1937 bounds: IntraTxScanBounds::from_range(
1938 IntraTxCoordinate::start_of_tx(3)..IntraTxCoordinate::start_of_tx(9),
1939 ),
1940 entry_checkpoint: 9,
1941 end_checkpoint: 3,
1942 end_position: IntraTxCoordinate::start_of_tx(3),
1943 exhaustion: RangeExhaustion::CheckpointBound,
1944 window_terminal_checkpoint: Some(3),
1945 };
1946
1947 let emptied_at = |coordinate| ResolvedScan {
1948 bounds: IntraTxScanBounds::empty_at(coordinate),
1949 entry_checkpoint: 9,
1950 end_checkpoint: 100,
1951 end_position: coordinate,
1952 exhaustion: RangeExhaustion::CursorBound {
1953 kind: sui_rpc_cursor::CursorKind::Boundary,
1954 },
1955 window_terminal_checkpoint: Some(3),
1956 };
1957
1958 let options = after_options(Ordering::Descending, ev_item(100, 100, 10));
1959 assert_eq!(
1960 seed.clone().apply_cursor_bounds(&options),
1961 emptied_at(IntraTxCoordinate {
1962 tx_seq: 100,
1963 index: 11,
1964 })
1965 );
1966 let options = after_options(Ordering::Descending, ev_boundary(100, 100, 10));
1967 assert_eq!(
1968 seed.apply_cursor_bounds(&options),
1969 emptied_at(IntraTxCoordinate {
1970 tx_seq: 100,
1971 index: 10,
1972 })
1973 );
1974 }
1975
1976 #[test]
1980 fn before_cursor_tightens_descending_upper_bound() {
1981 let seed = ResolvedScan {
1982 entry_checkpoint: 8,
1983 ..resolved_intra_tx()
1984 };
1985 let expected = ResolvedScan {
1986 bounds: IntraTxScanBounds {
1987 lo: Bound::Included(IntraTxCoordinate::start_of_tx(0)),
1988 hi: Bound::Excluded(IntraTxCoordinate {
1989 tx_seq: 5,
1990 index: 1,
1991 }),
1992 },
1993 ..seed.clone()
1994 };
1995 let options = before_options(Ordering::Descending, ev_item(6, 5, 1));
1996 assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
1997 let options = before_options(Ordering::Descending, ev_boundary(6, 5, 1));
1998 assert_eq!(seed.apply_cursor_bounds(&options), expected);
1999 }
2000
2001 #[test]
2002 fn before_cursor_above_window_never_tightens() {
2003 for cursor in [ev_boundary(1, 12, 0), ev_item(1, 12, 0)] {
2004 let options = before_options(Ordering::Descending, cursor.clone());
2005 assert_eq!(
2006 resolved_intra_tx().apply_cursor_bounds(&options),
2007 resolved_intra_tx()
2008 );
2009
2010 let options = before_options(Ordering::Ascending, cursor);
2011 assert_eq!(
2012 resolved_intra_tx().apply_cursor_bounds(&options),
2013 resolved_intra_tx()
2014 );
2015 }
2016 }
2017
2018 #[test]
2019 fn ascending_before_cursor_sets_terminal_and_tightens() {
2020 let expected = ResolvedScan {
2021 bounds: IntraTxScanBounds {
2022 lo: Bound::Included(IntraTxCoordinate::start_of_tx(0)),
2023 hi: Bound::Excluded(IntraTxCoordinate {
2024 tx_seq: 5,
2025 index: 1,
2026 }),
2027 },
2028 entry_checkpoint: 2,
2029 end_checkpoint: 6,
2030 end_position: IntraTxCoordinate {
2031 tx_seq: 5,
2032 index: 1,
2033 },
2034 exhaustion: RangeExhaustion::CursorBound {
2035 kind: sui_rpc_cursor::CursorKind::Boundary,
2036 },
2037 window_terminal_checkpoint: Some(9),
2038 };
2039 let options = before_options(Ordering::Ascending, ev_boundary(6, 5, 1));
2040 assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
2041 let options = before_options(Ordering::Ascending, ev_item(6, 5, 1));
2042 assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
2043 }
2044
2045 #[test]
2048 fn before_cursor_empties_ascending_interval() {
2049 let seed = ResolvedScan {
2050 bounds: IntraTxScanBounds::from_range(
2051 IntraTxCoordinate::start_of_tx(3)..IntraTxCoordinate::start_of_tx(10),
2052 ),
2053 entry_checkpoint: 2,
2054 end_checkpoint: 9,
2055 ..resolved_intra_tx()
2056 };
2057 let expected = ResolvedScan {
2058 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(3)),
2059 end_checkpoint: 1,
2060 end_position: IntraTxCoordinate::start_of_tx(3),
2061 exhaustion: RangeExhaustion::CursorBound {
2062 kind: sui_rpc_cursor::CursorKind::Boundary,
2063 },
2064 ..seed.clone()
2065 };
2066 let options = before_options(Ordering::Ascending, ev_boundary(1, 3, 0));
2067 assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
2068 let options = before_options(Ordering::Ascending, ev_item(1, 3, 0));
2069 assert_eq!(seed.apply_cursor_bounds(&options), expected);
2070 }
2071
2072 #[test]
2076 fn ascending_with_both_cursors_picks_the_terminal_owner() {
2077 let options = QueryOptions {
2080 limit_items: 100,
2081 ordering: Ordering::Ascending,
2082 after: Some(ev_item(5, 25, 0)),
2083 before: Some(ev_boundary(6, 30, 0)),
2084 };
2085 let resolved = resolve_event_request((Some(1), Some(4)), &options).unwrap();
2086 assert!(resolved.is_empty());
2087 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
2088 assert_eq!(
2089 (resolved.end_checkpoint, resolved.end_position),
2090 (
2091 5,
2092 IntraTxCoordinate {
2093 tx_seq: 25,
2094 index: 1
2095 }
2096 )
2097 );
2098
2099 let options = QueryOptions {
2101 limit_items: 100,
2102 ordering: Ordering::Ascending,
2103 after: Some(ev_boundary(12, 12, 0)),
2104 before: Some(ev_boundary(3, 3, 0)),
2105 };
2106 assert_eq!(
2107 resolved_intra_tx().apply_cursor_bounds(&options),
2108 ResolvedScan {
2109 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(3)),
2110 entry_checkpoint: 2,
2111 end_checkpoint: 3,
2112 end_position: IntraTxCoordinate::start_of_tx(3),
2113 exhaustion: RangeExhaustion::CursorBound {
2114 kind: sui_rpc_cursor::CursorKind::Boundary,
2115 },
2116 window_terminal_checkpoint: Some(9),
2117 }
2118 );
2119 }
2120
2121 #[test]
2125 fn descending_with_both_cursors_picks_the_terminal_owner() {
2126 let options = QueryOptions {
2128 limit_items: 100,
2129 ordering: Ordering::Descending,
2130 after: Some(ev_boundary(9, 12, 0)),
2131 before: Some(ev_boundary(1, 15, 0)),
2132 };
2133 assert_eq!(
2134 resolved_intra_tx().apply_cursor_bounds(&options),
2135 ResolvedScan {
2136 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(12)),
2137 entry_checkpoint: 2,
2138 end_checkpoint: 9,
2139 end_position: IntraTxCoordinate::start_of_tx(12),
2140 exhaustion: RangeExhaustion::CursorBound {
2141 kind: sui_rpc_cursor::CursorKind::Boundary,
2142 },
2143 window_terminal_checkpoint: Some(9),
2144 }
2145 );
2146
2147 let options = QueryOptions {
2149 limit_items: 100,
2150 ordering: Ordering::Descending,
2151 after: Some(ev_boundary(9, 12, 0)),
2152 before: Some(ev_item(1, 5, 2)),
2153 };
2154 assert_eq!(
2155 resolved_intra_tx().apply_cursor_bounds(&options),
2156 ResolvedScan {
2157 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate {
2158 tx_seq: 5,
2159 index: 2,
2160 }),
2161 entry_checkpoint: 2,
2162 end_checkpoint: 1,
2163 end_position: IntraTxCoordinate {
2164 tx_seq: 5,
2165 index: 2,
2166 },
2167 exhaustion: RangeExhaustion::CursorBound {
2168 kind: sui_rpc_cursor::CursorKind::Boundary,
2169 },
2170 window_terminal_checkpoint: Some(9),
2171 }
2172 );
2173
2174 let options = QueryOptions {
2177 limit_items: 100,
2178 ordering: Ordering::Descending,
2179 after: Some(ev_item(8, 5, 0)),
2180 before: Some(ev_item(1, 5, 0)),
2181 };
2182 assert_eq!(
2183 resolved_intra_tx().apply_cursor_bounds(&options),
2184 ResolvedScan {
2185 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(5)),
2186 entry_checkpoint: 2,
2187 end_checkpoint: 1,
2188 end_position: IntraTxCoordinate::start_of_tx(5),
2189 exhaustion: RangeExhaustion::CursorBound {
2190 kind: sui_rpc_cursor::CursorKind::Boundary,
2191 },
2192 window_terminal_checkpoint: Some(9),
2193 }
2194 );
2195 }
2196
2197 #[test]
2199 fn after_cursor_rejected_descending_is_noop() {
2200 let seed = ResolvedScan {
2201 bounds: IntraTxScanBounds {
2202 lo: Bound::Included(IntraTxCoordinate::start_of_tx(5)),
2203 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
2204 },
2205 ..resolved_intra_tx()
2206 };
2207 let options = after_options(Ordering::Descending, ev_boundary(4, 2, 0));
2208 assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
2209 }
2210
2211 #[test]
2215 fn cursor_at_the_window_bound_still_takes_the_terminal() {
2216 let options = before_options(Ordering::Ascending, ev_boundary(6, 10, 0));
2220 assert_eq!(
2221 resolved_intra_tx().apply_cursor_bounds(&options),
2222 ResolvedScan {
2223 bounds: IntraTxScanBounds::from_range(
2224 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10)
2225 ),
2226 entry_checkpoint: 2,
2227 end_checkpoint: 6,
2228 end_position: IntraTxCoordinate::start_of_tx(10),
2229 exhaustion: RangeExhaustion::CursorBound {
2230 kind: sui_rpc_cursor::CursorKind::Boundary,
2231 },
2232 window_terminal_checkpoint: Some(9),
2233 }
2234 );
2235
2236 let options = after_options(Ordering::Descending, ev_boundary(6, 0, 0));
2237 assert_eq!(
2238 resolved_intra_tx().apply_cursor_bounds(&options),
2239 ResolvedScan {
2240 bounds: IntraTxScanBounds::from_range(
2241 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10)
2242 ),
2243 entry_checkpoint: 2,
2244 end_checkpoint: 6,
2245 end_position: IntraTxCoordinate::start_of_tx(0),
2246 exhaustion: RangeExhaustion::CursorBound {
2247 kind: sui_rpc_cursor::CursorKind::Boundary,
2248 },
2249 window_terminal_checkpoint: Some(9),
2250 }
2251 );
2252 }
2253
2254 #[test]
2257 fn tx_after_cursor_tightens_ascending_lower_bound() {
2258 let options = after_options(Ordering::Ascending, tx_item(3, 12));
2261 let tightened = resolved_range(10..20).apply_cursor_bounds(&options);
2263 assert_eq!(
2264 tightened,
2265 ResolvedScan {
2266 bounds: ScanBounds {
2267 lo: Bound::Excluded(12),
2268 hi: Bound::Excluded(20),
2269 },
2270 ..resolved_range(10..20)
2271 }
2272 );
2273 assert_eq!(tightened.range(), 13..20);
2275
2276 let options = after_options(Ordering::Ascending, tx_boundary(3, 12));
2277 assert_eq!(
2278 resolved_range(10..20).apply_cursor_bounds(&options),
2279 ResolvedScan {
2280 bounds: ScanBounds {
2281 lo: Bound::Included(12),
2282 hi: Bound::Excluded(20),
2283 },
2284 ..resolved_range(10..20)
2285 }
2286 );
2287 }
2288
2289 #[test]
2293 fn tx_descending_after_cursor_sets_terminal_and_tightens() {
2294 let options = after_options(Ordering::Descending, tx_item(3, 12));
2297 assert_eq!(
2298 resolved_range(10..20).apply_cursor_bounds(&options),
2299 ResolvedScan {
2300 bounds: ScanBounds {
2301 lo: Bound::Excluded(12),
2302 hi: Bound::Excluded(20),
2303 },
2304 entry_checkpoint: 0,
2305 end_checkpoint: 3,
2306 end_position: 13,
2307 exhaustion: RangeExhaustion::CursorBound {
2308 kind: sui_rpc_cursor::CursorKind::Boundary,
2309 },
2310 window_terminal_checkpoint: Some(20),
2311 }
2312 );
2313 let options = after_options(Ordering::Descending, tx_boundary(3, 13));
2314 assert_eq!(
2315 resolved_range(10..20).apply_cursor_bounds(&options),
2316 ResolvedScan {
2317 bounds: ScanBounds::from_range(13..20),
2318 entry_checkpoint: 0,
2319 end_checkpoint: 3,
2320 end_position: 13,
2321 exhaustion: RangeExhaustion::CursorBound {
2322 kind: sui_rpc_cursor::CursorKind::Boundary,
2323 },
2324 window_terminal_checkpoint: Some(20),
2325 }
2326 );
2327 }
2328
2329 #[test]
2332 fn tx_after_cursor_empties_descending_interval() {
2333 let options = after_options(Ordering::Descending, tx_boundary(9, 25));
2336 assert_eq!(
2337 resolved_range(10..20).apply_cursor_bounds(&options),
2338 ResolvedScan {
2339 bounds: ScanBounds::from_range(25..25),
2340 entry_checkpoint: 0,
2341 end_checkpoint: 9,
2342 end_position: 25,
2343 exhaustion: RangeExhaustion::CursorBound {
2344 kind: sui_rpc_cursor::CursorKind::Boundary,
2345 },
2346 window_terminal_checkpoint: Some(20),
2347 }
2348 );
2349 let options = after_options(Ordering::Descending, tx_item(9, 24));
2350 assert_eq!(
2351 resolved_range(10..20).apply_cursor_bounds(&options),
2352 ResolvedScan {
2353 bounds: ScanBounds::from_range(25..25),
2354 entry_checkpoint: 0,
2355 end_checkpoint: 9,
2356 end_position: 25,
2357 exhaustion: RangeExhaustion::CursorBound {
2358 kind: sui_rpc_cursor::CursorKind::Boundary,
2359 },
2360 window_terminal_checkpoint: Some(20),
2361 }
2362 );
2363 }
2364
2365 #[test]
2368 fn tx_after_item_and_successor_boundary_admit_the_same_interval() {
2369 let item = after_options(Ordering::Ascending, tx_item(4, 24));
2370 let successor = after_options(Ordering::Ascending, tx_boundary(4, 25));
2371
2372 let wide = (Some(1), Some(6));
2374 assert_eq!(resolve_tx_request(wide, &item).unwrap().range(), 25..30);
2375 assert_eq!(
2376 resolve_tx_request(wide, &successor).unwrap().range(),
2377 25..30
2378 );
2379
2380 let narrow = (Some(1), Some(4));
2383 let resolved = resolve_tx_request(narrow, &item).unwrap();
2384 assert_eq!(resolved, resolve_tx_request(narrow, &successor).unwrap());
2385 assert!(resolved.is_empty());
2386 assert_eq!(
2387 (
2388 resolved.exhaustion,
2389 resolved.end_checkpoint,
2390 resolved.end_position
2391 ),
2392 (RangeExhaustion::CheckpointBound, 4, 25)
2393 );
2394 }
2395
2396 #[test]
2399 fn tx_after_boundary_at_max_empties_interval() {
2400 let options = after_options(Ordering::Ascending, tx_boundary(5, u64::MAX));
2401 assert_eq!(
2402 resolved_range(10..20).apply_cursor_bounds(&options),
2403 empty_resolved_range(
2404 5,
2405 u64::MAX,
2406 RangeExhaustion::CursorBound {
2407 kind: sui_rpc_cursor::CursorKind::Boundary,
2408 },
2409 )
2410 );
2411 }
2412
2413 #[test]
2415 fn tx_after_item_at_max_lets_before_cursor_win() {
2416 let options = QueryOptions {
2420 limit_items: 100,
2421 ordering: Ordering::Ascending,
2422 after: Some(tx_item(5, u64::MAX)),
2423 before: Some(tx_boundary(3, 15)),
2424 };
2425 assert_eq!(
2426 resolved_range(10..20).apply_cursor_bounds(&options),
2427 ResolvedScan {
2428 bounds: ScanBounds::from_range(15..15),
2429 entry_checkpoint: 0,
2430 end_checkpoint: 3,
2431 end_position: 15,
2432 exhaustion: RangeExhaustion::CursorBound {
2433 kind: sui_rpc_cursor::CursorKind::Boundary,
2434 },
2435 window_terminal_checkpoint: Some(20),
2436 }
2437 );
2438
2439 let seed = ResolvedScan {
2441 entry_checkpoint: 7,
2442 ..resolved_range(10..20)
2443 };
2444 let options = QueryOptions {
2445 limit_items: 100,
2446 ordering: Ordering::Descending,
2447 after: Some(tx_item(5, u64::MAX)),
2448 before: Some(tx_boundary(3, 15)),
2449 };
2450 assert_eq!(
2451 seed.apply_cursor_bounds(&options),
2452 ResolvedScan {
2453 bounds: ScanBounds::from_range(15..15),
2454 entry_checkpoint: 7,
2455 end_checkpoint: 3,
2456 end_position: 15,
2457 exhaustion: RangeExhaustion::CursorBound {
2458 kind: sui_rpc_cursor::CursorKind::Boundary,
2459 },
2460 window_terminal_checkpoint: Some(20),
2461 }
2462 );
2463 }
2464
2465 #[test]
2470 fn tx_after_item_at_window_edge_defers_to_drain() {
2471 let options = after_options(Ordering::Ascending, tx_item(5, 19));
2472 let initial_range = resolved_range(10..20);
2473 let resolved = initial_range.clone().apply_cursor_bounds(&options);
2474 assert_eq!(
2475 resolved,
2476 ResolvedScan {
2477 bounds: ScanBounds {
2478 lo: Bound::Excluded(19),
2479 hi: Bound::Excluded(20),
2480 },
2481 ..initial_range
2482 }
2483 );
2484 assert!(!resolved.is_empty());
2485 assert!(resolved.range().is_empty());
2486 }
2487
2488 #[test]
2491 fn tx_after_item_at_window_edge_descending_keeps_stamped_terminal() {
2492 let options = after_options(Ordering::Descending, tx_item(5, 19));
2493 let initial_range = resolved_range(10..20);
2494 let resolved = initial_range.clone().apply_cursor_bounds(&options);
2495 assert_eq!(
2496 resolved,
2497 ResolvedScan {
2498 bounds: ScanBounds {
2499 lo: Bound::Excluded(19),
2500 hi: Bound::Excluded(20),
2501 },
2502 end_checkpoint: 5,
2503 end_position: 20,
2504 exhaustion: RangeExhaustion::CursorBound {
2505 kind: sui_rpc_cursor::CursorKind::Boundary,
2506 },
2507 ..initial_range
2508 }
2509 );
2510 assert!(resolved.range().is_empty());
2511 }
2512
2513 #[test]
2517 fn tx_after_item_below_window_start_descending_is_noop() {
2518 let options = after_options(Ordering::Descending, tx_item(5, 9));
2519 assert_eq!(
2520 resolved_range(10..20).apply_cursor_bounds(&options),
2521 resolved_range(10..20)
2522 );
2523 }
2524
2525 #[test]
2526 fn item_cursor_can_be_used_as_after_or_before() {
2527 let token = CursorToken::item(Position::Transactions {
2528 checkpoint: 1,
2529 tx_seq: 11,
2530 })
2531 .encode();
2532
2533 let mut request = ProtoQueryOptions::default();
2534 request.after = Some(token.clone());
2535 let options = query_options_from_proto(Some(&request)).unwrap();
2536 assert_eq!(
2537 resolved_range(10..20).apply_cursor_bounds(&options).range(),
2538 12..20
2539 );
2540
2541 request.after = None;
2542 request.before = Some(token);
2543 let options = query_options_from_proto(Some(&request)).unwrap();
2544 assert_eq!(
2545 resolved_range(10..20).apply_cursor_bounds(&options).range(),
2546 10..11
2547 );
2548 }
2549
2550 const TIP: u64 = 20;
2553
2554 fn first_tx(checkpoint: u64) -> u64 {
2555 5 * checkpoint
2556 }
2557
2558 fn resolve_checkpoints(
2559 end_checkpoint: Option<u64>,
2560 options: &QueryOptions,
2561 ) -> Result<ResolvedScan<u64>, RpcError> {
2562 let cp_range = ResolvedCheckpointRange::from_request(None, end_checkpoint, TIP, options)?;
2563 let range = cp_range.range.clone();
2564 ResolvedScan::<u64>::resolve(cp_range, range, options)
2565 }
2566
2567 fn resolve_transactions(options: &QueryOptions) -> Result<ResolvedScan<u64>, RpcError> {
2568 let cp_range = ResolvedCheckpointRange::from_request(None, None, TIP, options)?;
2569 let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2570 ResolvedScan::<u64>::resolve(cp_range, tx_range, options)
2571 }
2572
2573 fn resolve_events(options: &QueryOptions) -> Result<ResolvedScan<IntraTxCoordinate>, RpcError> {
2574 let cp_range = ResolvedCheckpointRange::from_request(None, None, TIP, options)?;
2575 let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2576 ResolvedScan::<IntraTxCoordinate>::resolve(
2577 cp_range,
2578 IntraTxCoordinate::tx_window(tx_range),
2579 options,
2580 )
2581 }
2582
2583 fn is_out_of_range<P>(resolved: Result<ResolvedScan<P>, RpcError>) -> bool {
2584 resolved.is_err_and(|e| e.into_status_proto().code == tonic::Code::OutOfRange as i32)
2585 }
2586
2587 #[test]
2588 fn ascending_after_cursor_past_the_tip_is_out_of_range() {
2589 let ascending = |cursor| after_options(Ordering::Ascending, cursor);
2590
2591 assert!(resolve_checkpoints(None, &ascending(cp_item(TIP - 1))).is_ok());
2593 let caught_up = resolve_checkpoints(
2594 None,
2595 &ascending(CursorToken::boundary(Position::Checkpoints {
2596 checkpoint: TIP,
2597 })),
2598 )
2599 .unwrap();
2600 assert!(caught_up.is_empty());
2601 assert_eq!(caught_up.end_position, TIP);
2602
2603 assert!(is_out_of_range(resolve_checkpoints(
2605 None,
2606 &ascending(cp_item(TIP))
2607 )));
2608 assert!(is_out_of_range(resolve_checkpoints(
2609 None,
2610 &ascending(CursorToken::boundary(Position::Checkpoints {
2611 checkpoint: TIP + 1,
2612 })),
2613 )));
2614
2615 let past_window = resolve_checkpoints(Some(TIP), &ascending(cp_item(TIP + 5))).unwrap();
2618 assert!(past_window.is_empty());
2619 assert!(is_out_of_range(resolve_checkpoints(
2620 Some(TIP + 1),
2621 &ascending(cp_item(TIP + 5)),
2622 )));
2623 }
2624
2625 #[test]
2626 fn descending_cursors_past_the_tip_are_out_of_range() {
2627 let before = |cursor| before_options(Ordering::Descending, cursor);
2628 let after = |cursor| after_options(Ordering::Descending, cursor);
2629 let tip_boundary = || CursorToken::boundary(Position::Checkpoints { checkpoint: TIP });
2630
2631 assert!(resolve_checkpoints(None, &before(cp_item(TIP - 1))).is_ok());
2632 assert!(resolve_checkpoints(None, &before(tip_boundary())).is_ok());
2633 assert!(is_out_of_range(resolve_checkpoints(
2634 None,
2635 &before(cp_item(TIP))
2636 )));
2637
2638 let at_tip = resolve_checkpoints(None, &after(tip_boundary())).unwrap();
2641 assert!(at_tip.is_empty());
2642 assert_eq!(
2643 at_tip.exhaustion,
2644 RangeExhaustion::CursorBound {
2645 kind: sui_rpc_cursor::CursorKind::Boundary
2646 }
2647 );
2648 assert!(is_out_of_range(resolve_checkpoints(
2649 None,
2650 &after(cp_item(TIP))
2651 )));
2652 assert!(is_out_of_range(resolve_checkpoints(
2653 None,
2654 &after(cp_item(TIP + 5))
2655 )));
2656
2657 let past_window = resolve_checkpoints(Some(TIP), &after(cp_item(TIP + 5))).unwrap();
2659 assert!(past_window.is_empty());
2660 }
2661
2662 #[test]
2665 fn cursor_inside_the_first_unindexed_checkpoint_is_out_of_range() {
2666 let ascending = |cursor| after_options(Ordering::Ascending, cursor);
2667 let tip_tx = first_tx(TIP);
2668
2669 assert!(resolve_transactions(&ascending(tx_item(TIP - 1, tip_tx - 1))).is_ok());
2670 assert!(resolve_transactions(&ascending(tx_boundary(TIP, tip_tx))).is_ok());
2671 assert!(is_out_of_range(resolve_transactions(&ascending(
2672 tx_boundary(TIP, tip_tx + 2,)
2673 ))));
2674 assert!(is_out_of_range(resolve_transactions(&ascending(tx_item(
2675 TIP, tip_tx
2676 )))));
2677
2678 assert!(resolve_events(&ascending(ev_boundary(TIP, tip_tx, 0))).is_ok());
2679 assert!(is_out_of_range(resolve_events(&ascending(ev_item(
2680 TIP, tip_tx, 0
2681 )))));
2682 assert!(is_out_of_range(resolve_events(&ascending(ev_boundary(
2683 TIP, tip_tx, 1
2684 )))));
2685 }
2686
2687 #[derive(Clone, Copy, Debug)]
2693 enum Issuer {
2694 Item,
2696 ExactBoundary,
2699 AscendingFrontier,
2701 DescendingFrontier,
2703 }
2704
2705 const ISSUERS: [Issuer; 4] = [
2706 Issuer::Item,
2707 Issuer::ExactBoundary,
2708 Issuer::AscendingFrontier,
2709 Issuer::DescendingFrontier,
2710 ];
2711
2712 type Window = (Option<u64>, Option<u64>);
2714
2715 const WINDOWS: [Window; 5] = [
2717 (None, None),
2718 (Some(4), Some(13)),
2719 (Some(7), None),
2720 (None, Some(7)),
2721 (Some(5), Some(6)),
2722 ];
2723
2724 const PAST_TIP_WINDOWS: [Window; 2] = [(Some(15), Some(TIP + 1)), (Some(TIP), None)];
2728
2729 const ORDERINGS: [Ordering; 2] = [Ordering::Ascending, Ordering::Descending];
2730
2731 fn containing_checkpoint(tx: u64) -> u64 {
2732 tx / 5
2733 }
2734
2735 fn issued_tx_cursor(issuer: Issuer, tx: u64) -> Option<CursorToken> {
2737 let indexed = tx < first_tx(TIP);
2738 let checkpoint = containing_checkpoint(tx);
2739 match issuer {
2740 Issuer::Item => indexed.then(|| tx_item(checkpoint, tx)),
2741 Issuer::ExactBoundary => Some(tx_boundary(checkpoint, tx)),
2742 Issuer::AscendingFrontier => Some(tx_boundary(
2743 tx.checked_sub(1).map_or(0, containing_checkpoint),
2744 tx,
2745 )),
2746 Issuer::DescendingFrontier => indexed.then(|| tx_boundary(checkpoint + 1, tx)),
2747 }
2748 }
2749
2750 fn issued_event_cursor(issuer: Issuer, tx: u64, index: u32) -> Option<CursorToken> {
2752 let indexed = tx < first_tx(TIP);
2753 let checkpoint = containing_checkpoint(tx);
2754 let slot_before = if index > 0 {
2755 Some(tx)
2756 } else {
2757 tx.checked_sub(1)
2758 };
2759 match issuer {
2760 Issuer::Item => (indexed && index < 2).then(|| ev_item(checkpoint, tx, index)),
2761 Issuer::ExactBoundary => {
2762 (indexed || index == 0).then(|| ev_boundary(checkpoint, tx, index))
2763 }
2764 Issuer::AscendingFrontier => (indexed || index == 0)
2765 .then(|| ev_boundary(slot_before.map_or(0, containing_checkpoint), tx, index)),
2766 Issuer::DescendingFrontier => indexed.then(|| ev_boundary(checkpoint + 1, tx, index)),
2767 }
2768 }
2769
2770 fn every_tx_cursor(txs: impl Iterator<Item = u64> + Clone) -> Vec<CursorToken> {
2771 ISSUERS
2772 .iter()
2773 .flat_map(|&issuer| {
2774 txs.clone()
2775 .filter_map(move |tx| issued_tx_cursor(issuer, tx))
2776 })
2777 .collect()
2778 }
2779
2780 fn every_event_cursor(txs: impl Iterator<Item = u64> + Clone) -> Vec<CursorToken> {
2781 ISSUERS
2782 .iter()
2783 .flat_map(|&issuer| {
2784 txs.clone().flat_map(move |tx| {
2785 (0..3).filter_map(move |index| issued_event_cursor(issuer, tx, index))
2786 })
2787 })
2788 .collect()
2789 }
2790
2791 fn edge_txs() -> impl Iterator<Item = u64> + Clone {
2793 [
2794 0, 1, 4, 5, 6, 19, 20, 21, 24, 25, 26, 29, 30, 34, 35, 36, 64, 65, 66, 94, 95, 96, 99,
2795 100,
2796 ]
2797 .into_iter()
2798 }
2799
2800 fn event_edge_txs() -> impl Iterator<Item = u64> + Clone {
2802 [0, 4, 5, 20, 24, 25, 29, 30, 34, 35, 64, 65, 95, 99, 100].into_iter()
2803 }
2804
2805 fn options_with(
2806 ordering: Ordering,
2807 after: Option<CursorToken>,
2808 before: Option<CursorToken>,
2809 ) -> QueryOptions {
2810 QueryOptions {
2811 limit_items: 100,
2812 ordering,
2813 after,
2814 before,
2815 }
2816 }
2817
2818 fn requests(
2821 cursors: &[CursorToken],
2822 pair_cursors: &[CursorToken],
2823 ) -> Vec<(Window, QueryOptions)> {
2824 let mut requests = Vec::new();
2825 for window in WINDOWS.into_iter().chain(PAST_TIP_WINDOWS) {
2826 for ordering in ORDERINGS {
2827 for cursor in cursors {
2828 requests.push((window, options_with(ordering, Some(cursor.clone()), None)));
2829 requests.push((window, options_with(ordering, None, Some(cursor.clone()))));
2830 }
2831 for after in pair_cursors {
2832 for before in pair_cursors {
2833 requests.push((
2834 window,
2835 options_with(ordering, Some(after.clone()), Some(before.clone())),
2836 ));
2837 }
2838 }
2839 }
2840 }
2841 requests
2842 }
2843
2844 fn resolve_tx_request(
2845 window: Window,
2846 options: &QueryOptions,
2847 ) -> Result<ResolvedScan<u64>, RpcError> {
2848 let cp_range = ResolvedCheckpointRange::from_request(window.0, window.1, TIP, options)?;
2849 let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2850 ResolvedScan::<u64>::resolve(cp_range, tx_range, options)
2851 }
2852
2853 fn resolve_event_request(
2854 window: Window,
2855 options: &QueryOptions,
2856 ) -> Result<ResolvedScan<IntraTxCoordinate>, RpcError> {
2857 let cp_range = ResolvedCheckpointRange::from_request(window.0, window.1, TIP, options)?;
2858 let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2859 ResolvedScan::<IntraTxCoordinate>::resolve(
2860 cp_range,
2861 IntraTxCoordinate::tx_window(tx_range),
2862 options,
2863 )
2864 }
2865
2866 fn resolve_checkpoint_request(
2867 window: Window,
2868 options: &QueryOptions,
2869 ) -> Result<ResolvedScan<u64>, RpcError> {
2870 let cp_range = ResolvedCheckpointRange::from_request(window.0, window.1, TIP, options)?;
2871 let range = cp_range.range.clone();
2872 ResolvedScan::<u64>::resolve(cp_range, range, options)
2873 }
2874
2875 fn ledger_txs() -> Vec<u64> {
2876 (0..first_tx(TIP)).collect()
2877 }
2878
2879 fn ledger_event_slots() -> Vec<IntraTxCoordinate> {
2882 (0..first_tx(TIP))
2883 .flat_map(|tx_seq| (0..3).map(move |index| IntraTxCoordinate { tx_seq, index }))
2884 .collect()
2885 }
2886
2887 fn admits(window: Window, options: &QueryOptions, key: (u64, u32)) -> bool {
2889 let start = first_tx(window.0.unwrap_or(0));
2890 let end = first_tx(window.1.unwrap_or(TIP).min(TIP));
2891 let in_window = start <= key.0 && key.0 < end;
2892 let after = options.after.as_ref().is_none_or(|cursor| {
2893 let cursor_key = scan_order(&cursor.position);
2894 match cursor.kind {
2895 sui_rpc_cursor::CursorKind::Item => key > cursor_key,
2896 sui_rpc_cursor::CursorKind::Boundary => key >= cursor_key,
2897 }
2898 });
2899 let before = options
2900 .before
2901 .as_ref()
2902 .is_none_or(|cursor| key < scan_order(&cursor.position));
2903 in_window && after && before
2904 }
2905
2906 fn scan_order(position: &Position) -> (u64, u32) {
2909 match *position {
2910 Position::Checkpoints { checkpoint } => (first_tx(checkpoint), 0),
2911 Position::Transactions { tx_seq, .. } => (tx_seq, 0),
2912 Position::Events {
2913 tx_seq,
2914 event_index,
2915 ..
2916 } => (tx_seq, event_index),
2917 }
2918 }
2919
2920 fn status(error: RpcError) -> (i32, String) {
2921 let status = error.into_status_proto();
2922 (status.code, status.message)
2923 }
2924
2925 fn check_request<P: ScanPosition + std::fmt::Debug>(
2940 window: Window,
2941 options: &QueryOptions,
2942 resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
2943 positions: &[P],
2944 key: impl Fn(P) -> (u64, u32),
2945 ) where
2946 CursorToken: ScanCursor<P>,
2947 {
2948 let context = format!("window {window:?}, {options:?}");
2949 let tip_checked_cursors = match options.ordering {
2950 Ordering::Ascending => [options.after.as_ref(), None],
2951 Ordering::Descending => [options.before.as_ref(), options.after.as_ref()],
2952 };
2953 let tip_key = (first_tx(TIP), 0);
2954 let window_reaches_tip = window.1.is_none_or(|end| end > TIP);
2955 let descending_window_past_tip = options.ordering == Ordering::Descending
2956 && match window.1 {
2957 Some(end) => end > TIP,
2958 None => window.0.unwrap_or(0) >= TIP,
2959 };
2960 let cursor_past_tip = window_reaches_tip
2961 && tip_checked_cursors.into_iter().flatten().any(|cursor| {
2962 let cursor_key = scan_order(&cursor.position);
2963 match cursor.kind {
2964 sui_rpc_cursor::CursorKind::Item => cursor_key >= tip_key,
2965 sui_rpc_cursor::CursorKind::Boundary => cursor_key > tip_key,
2966 }
2967 });
2968 let out_of_range = descending_window_past_tip || cursor_past_tip;
2969 let resolved = match resolve(window, options) {
2970 Ok(resolved) => resolved,
2971 Err(error) => {
2972 let (code, message) = status(error);
2973 assert!(out_of_range, "{code}: {message}: {context}");
2974 assert_eq!(code, tonic::Code::OutOfRange as i32, "{message}: {context}");
2975 return;
2976 }
2977 };
2978 assert!(!out_of_range, "a request past the tip resolves: {context}");
2979 let admitted: Vec<P> = positions
2980 .iter()
2981 .copied()
2982 .filter(|position| resolved.bounds.contains(*position))
2983 .collect();
2984 let expected: Vec<P> = positions
2985 .iter()
2986 .copied()
2987 .filter(|position| admits(window, options, key(*position)))
2988 .collect();
2989 assert_eq!(admitted, expected, "admitted positions: {context}");
2990
2991 let window_start = window.0.unwrap_or(0);
2992 let window_end = window.1.unwrap_or(TIP).min(TIP);
2993 if window_start >= window_end {
2994 return;
2995 }
2996 let window_lo = (first_tx(window_start), 0);
2997 let window_hi = (first_tx(window_end), 0);
2998 let cursor_key = |cursor: &CursorToken| key(cursor.coordinate());
2999 let first_admitted_key = |cursor: &CursorToken| {
3000 let position: P = cursor.coordinate();
3001 match cursor.kind {
3002 sui_rpc_cursor::CursorKind::Item => position.successor().map(&key),
3003 sui_rpc_cursor::CursorKind::Boundary => Some(key(position)),
3004 }
3005 };
3006 let (entry, claim, reason, window_reason_end) = match options.ordering {
3009 Ordering::Ascending => (
3010 window_start,
3011 window_end - 1,
3012 if window_reaches_tip {
3013 RangeExhaustion::LedgerTip
3014 } else {
3015 RangeExhaustion::CheckpointBound
3016 },
3017 if options
3018 .before
3019 .as_ref()
3020 .is_some_and(|before| cursor_key(before) <= window_hi)
3021 {
3022 None
3023 } else {
3024 match options.after.as_ref() {
3025 None => Some(window_hi),
3026 Some(after) => first_admitted_key(after).map(|first| first.max(window_hi)),
3027 }
3028 },
3029 ),
3030 Ordering::Descending => (
3031 window_end - 1,
3032 window_start,
3033 RangeExhaustion::CheckpointBound,
3034 if options
3035 .after
3036 .as_ref()
3037 .is_some_and(|after| cursor_key(after) >= window_lo)
3038 {
3039 None
3040 } else {
3041 Some(
3042 options
3043 .before
3044 .as_ref()
3045 .map_or(window_lo, |before| cursor_key(before).min(window_lo)),
3046 )
3047 },
3048 ),
3049 };
3050 assert_eq!(
3051 resolved.entry_checkpoint, entry,
3052 "entry checkpoint: {context}"
3053 );
3054 match window_reason_end {
3055 Some(end) => {
3056 assert_eq!(resolved.exhaustion, reason, "end reason: {context}");
3057 assert_eq!(key(resolved.end_position), end, "end position: {context}");
3058 assert_eq!(
3059 resolved.natural_end_claim(options),
3060 Some(claim),
3061 "checkpoint claim: {context}"
3062 );
3063 }
3064 None => assert!(
3065 matches!(resolved.exhaustion, RangeExhaustion::CursorBound { .. }),
3066 "end reason: {context}: {:?}",
3067 resolved.exhaustion
3068 ),
3069 }
3070 }
3071
3072 #[test]
3076 fn opposite_direction_frontier_admits_the_positions_next_to_it() {
3077 let options = before_options(Ordering::Descending, tx_boundary(4, 23));
3079 let resolved = resolve_transactions(&options).unwrap();
3080 assert_eq!(resolved.range(), 0..23);
3081
3082 let options = after_options(Ordering::Ascending, tx_boundary(5, 23));
3085 let resolved = resolve_transactions(&options).unwrap();
3086 assert_eq!(resolved.range(), 23..100);
3087
3088 let options = before_options(Ordering::Descending, ev_boundary(4, 23, 0));
3090 let resolved = resolve_events(&options).unwrap();
3091 assert_eq!(
3092 resolved.bounds.hi,
3093 Bound::Excluded(IntraTxCoordinate::start_of_tx(23))
3094 );
3095 assert_eq!(
3096 resolved.bounds.lo,
3097 Bound::Included(IntraTxCoordinate::start_of_tx(0))
3098 );
3099 }
3100
3101 #[test]
3102 fn transaction_cursors_admit_their_positions_in_either_direction() {
3103 let cursors = every_tx_cursor(0..=first_tx(TIP));
3104 let pair_cursors = every_tx_cursor(edge_txs());
3105 let positions = ledger_txs();
3106 for (window, options) in requests(&cursors, &pair_cursors) {
3107 check_request(window, &options, resolve_tx_request, &positions, |tx| {
3108 (tx, 0)
3109 });
3110 }
3111 }
3112
3113 #[test]
3114 fn event_cursors_admit_their_positions_in_either_direction() {
3115 let cursors = every_event_cursor(0..=first_tx(TIP));
3116 let pair_cursors = every_event_cursor(event_edge_txs());
3117 let positions = ledger_event_slots();
3118 for (window, options) in requests(&cursors, &pair_cursors) {
3119 check_request(
3120 window,
3121 &options,
3122 resolve_event_request,
3123 &positions,
3124 |position| (position.tx_seq, position.index),
3125 );
3126 }
3127 }
3128
3129 fn terminal_cursor<P: ScanPosition>(
3131 resolved: &ResolvedScan<P>,
3132 position_at: impl Fn(u64, P) -> Position,
3133 ) -> CursorToken {
3134 let kind = match resolved.exhaustion {
3135 RangeExhaustion::CursorBound { kind } => kind,
3136 RangeExhaustion::LedgerTip
3137 | RangeExhaustion::CheckpointBound
3138 | RangeExhaustion::ServingFloor { .. } => sui_rpc_cursor::CursorKind::Boundary,
3139 };
3140 CursorToken {
3141 kind,
3142 position: position_at(resolved.end_checkpoint, resolved.end_position),
3143 }
3144 }
3145
3146 fn check_closing_cursor<P: ScanPosition + std::fmt::Debug>(
3150 window: Window,
3151 options: &QueryOptions,
3152 resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
3153 positions: &[P],
3154 key: impl Fn(P) -> (u64, u32),
3155 position_at: impl Fn(u64, P) -> Position,
3156 ) {
3157 let context = format!("window {window:?}, {options:?}");
3158 let resolved = resolve(window, options).unwrap();
3159 let closing = terminal_cursor(&resolved, position_at);
3160 let resume = if options.is_ascending() {
3161 options_with(options.ordering, Some(closing.clone()), None)
3162 } else {
3163 options_with(options.ordering, None, Some(closing.clone()))
3164 };
3165 let resumed = resolve(window, &resume)
3166 .unwrap_or_else(|e| panic!("{closing:?} resumes: {:?}: {context}", status(e)));
3167 let unbounded = options_with(options.ordering, None, None);
3168 let excluded: Vec<P> = positions
3169 .iter()
3170 .copied()
3171 .filter(|position| {
3172 admits(window, &unbounded, key(*position))
3173 && !admits(window, options, key(*position))
3174 })
3175 .collect();
3176 let admitted: Vec<P> = positions
3177 .iter()
3178 .copied()
3179 .filter(|position| resumed.bounds.contains(*position))
3180 .collect();
3181 assert_eq!(admitted, excluded, "{closing:?} resumes: {context}");
3182 }
3183
3184 fn far_bounded_requests(cursors: &[CursorToken]) -> Vec<(Window, QueryOptions)> {
3186 let mut requests = Vec::new();
3187 for window in WINDOWS {
3188 for cursor in cursors {
3189 requests.push((
3190 window,
3191 options_with(Ordering::Descending, Some(cursor.clone()), None),
3192 ));
3193 requests.push((
3194 window,
3195 options_with(Ordering::Ascending, None, Some(cursor.clone())),
3196 ));
3197 }
3198 }
3199 requests
3200 }
3201
3202 #[test]
3203 fn transaction_closing_cursors_resume_without_their_far_bound() {
3204 let positions = ledger_txs();
3205 for (window, options) in far_bounded_requests(&every_tx_cursor(0..=first_tx(TIP))) {
3206 check_closing_cursor(
3207 window,
3208 &options,
3209 resolve_tx_request,
3210 &positions,
3211 |tx| (tx, 0),
3212 |checkpoint, tx_seq| Position::Transactions { checkpoint, tx_seq },
3213 );
3214 }
3215 }
3216
3217 #[test]
3218 fn event_closing_cursors_resume_without_their_far_bound() {
3219 let positions = ledger_event_slots();
3220 for (window, options) in far_bounded_requests(&every_event_cursor(0..=first_tx(TIP))) {
3221 check_closing_cursor(
3222 window,
3223 &options,
3224 resolve_event_request,
3225 &positions,
3226 |position| (position.tx_seq, position.index),
3227 |checkpoint, position| Position::Events {
3228 checkpoint,
3229 tx_seq: position.tx_seq,
3230 event_index: position.index,
3231 },
3232 );
3233 }
3234 }
3235
3236 fn check_empty_page_closing_cursor<P: ScanPosition + std::fmt::Debug>(
3241 window: Window,
3242 options: &QueryOptions,
3243 resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
3244 positions: &[P],
3245 position_at: impl Fn(u64, P) -> Position,
3246 ) where
3247 CursorToken: ScanCursor<P>,
3248 {
3249 let Ok(resolved) = resolve(window, options) else {
3250 return;
3251 };
3252 if !resolved.is_empty()
3253 || matches!(resolved.exhaustion, RangeExhaustion::CursorBound { .. })
3254 {
3255 return;
3256 }
3257 let context = format!("window {window:?}, {options:?}");
3258 let closing = terminal_cursor(&resolved, position_at);
3259 let resume = if options.is_ascending() {
3260 options_with(options.ordering, Some(closing.clone()), None)
3261 } else {
3262 options_with(options.ordering, None, Some(closing.clone()))
3263 };
3264 let admitted = |options: &QueryOptions| {
3265 resolve((None, None), options).map(|resolved| {
3266 positions
3267 .iter()
3268 .copied()
3269 .filter(|position| resolved.bounds.contains(*position))
3270 .collect::<Vec<P>>()
3271 })
3272 };
3273 match (admitted(options), admitted(&resume)) {
3274 (Ok(starting), Ok(resumed)) => {
3275 assert_eq!(resumed, starting, "{closing:?} resumes: {context}")
3276 }
3277 (Err(_), Err(_)) => {}
3278 (starting, resumed) => panic!(
3279 "{closing:?} resumes as {:?}, the starting cursor as {:?}: {context}",
3280 resumed.map_err(status),
3281 starting.map_err(status),
3282 ),
3283 }
3284 }
3285
3286 fn starting_cursor_requests(cursors: &[CursorToken]) -> Vec<(Window, QueryOptions)> {
3288 let mut requests = Vec::new();
3289 for window in WINDOWS {
3290 for cursor in cursors {
3291 requests.push((
3292 window,
3293 options_with(Ordering::Ascending, Some(cursor.clone()), None),
3294 ));
3295 requests.push((
3296 window,
3297 options_with(Ordering::Descending, None, Some(cursor.clone())),
3298 ));
3299 }
3300 }
3301 requests
3302 }
3303
3304 #[test]
3305 fn transaction_empty_pages_close_where_their_starting_cursor_resumes() {
3306 let positions = ledger_txs();
3307 for (window, options) in starting_cursor_requests(&every_tx_cursor(0..=first_tx(TIP))) {
3308 check_empty_page_closing_cursor(
3309 window,
3310 &options,
3311 resolve_tx_request,
3312 &positions,
3313 |checkpoint, tx_seq| Position::Transactions { checkpoint, tx_seq },
3314 );
3315 }
3316 }
3317
3318 #[test]
3319 fn event_empty_pages_close_where_their_starting_cursor_resumes() {
3320 let positions = ledger_event_slots();
3321 for (window, options) in starting_cursor_requests(&every_event_cursor(0..=first_tx(TIP))) {
3322 check_empty_page_closing_cursor(
3323 window,
3324 &options,
3325 resolve_event_request,
3326 &positions,
3327 |checkpoint, position| Position::Events {
3328 checkpoint,
3329 tx_seq: position.tx_seq,
3330 event_index: position.index,
3331 },
3332 );
3333 }
3334 }
3335
3336 #[test]
3337 fn checkpoint_empty_pages_close_where_their_starting_cursor_resumes() {
3338 let positions: Vec<u64> = (0..TIP).collect();
3339 for (window, options) in starting_cursor_requests(&every_checkpoint_cursor()) {
3340 check_empty_page_closing_cursor(
3341 window,
3342 &options,
3343 resolve_checkpoint_request,
3344 &positions,
3345 |_, checkpoint| Position::Checkpoints { checkpoint },
3346 );
3347 }
3348 }
3349
3350 #[test]
3354 fn descending_after_item_closes_at_the_next_position() {
3355 let resolved =
3357 resolve_transactions(&after_options(Ordering::Descending, tx_item(2, 12))).unwrap();
3358 assert_eq!(resolved.range(), 13..100);
3359 assert_eq!((resolved.end_checkpoint, resolved.end_position), (2, 13));
3360 let resumed =
3361 resolve_transactions(&before_options(Ordering::Descending, tx_boundary(2, 13)))
3362 .unwrap();
3363 assert_eq!(resumed.range(), 0..13);
3364
3365 let resolved =
3369 resolve_transactions(&after_options(Ordering::Descending, tx_item(2, 14))).unwrap();
3370 assert_eq!(resolved.range(), 15..100);
3371 assert_eq!((resolved.end_checkpoint, resolved.end_position), (2, 15));
3372 let resumed =
3373 resolve_transactions(&before_options(Ordering::Descending, tx_boundary(2, 15)))
3374 .unwrap();
3375 assert_eq!(resumed.range(), 0..15);
3376 assert!(resumed.bounds.contains(14));
3377
3378 let resolved =
3379 resolve_events(&after_options(Ordering::Descending, ev_item(2, 14, 1))).unwrap();
3380 assert_eq!(
3381 resolved.end_position,
3382 IntraTxCoordinate {
3383 tx_seq: 14,
3384 index: 2
3385 }
3386 );
3387 let resolved = resolve_events(&after_options(
3388 Ordering::Descending,
3389 ev_item(2, 14, u32::MAX),
3390 ))
3391 .unwrap();
3392 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(15));
3393
3394 let resolved = resolved_range(10..20)
3396 .apply_cursor_bounds(&after_options(Ordering::Descending, tx_item(1, u64::MAX)));
3397 assert_eq!(resolved.end_position, u64::MAX);
3398 assert_eq!(
3399 resolved.exhaustion,
3400 RangeExhaustion::CursorBound {
3401 kind: sui_rpc_cursor::CursorKind::Item
3402 }
3403 );
3404 }
3405
3406 fn every_checkpoint_cursor() -> Vec<CursorToken> {
3408 (0..=TIP)
3409 .flat_map(|checkpoint| {
3410 [
3411 cp_item(checkpoint),
3412 CursorToken::boundary(Position::Checkpoints { checkpoint }),
3413 ]
3414 })
3415 .collect()
3416 }
3417
3418 #[test]
3419 fn checkpoint_cursors_admit_their_positions_in_either_direction() {
3420 let cursors = every_checkpoint_cursor();
3421 let positions: Vec<u64> = (0..TIP).collect();
3422 for (window, options) in requests(&cursors, &cursors) {
3423 check_request(
3424 window,
3425 &options,
3426 resolve_checkpoint_request,
3427 &positions,
3428 |checkpoint| (first_tx(checkpoint), 0),
3429 );
3430 }
3431 }
3432
3433 #[test]
3437 fn caught_up_ascending_scan_ends_with_ledger_tip() {
3438 let tip_tx = first_tx(TIP);
3439 let cursor_bound = RangeExhaustion::CursorBound {
3440 kind: sui_rpc_cursor::CursorKind::Boundary,
3441 };
3442 for cursor in [
3444 tx_boundary(TIP, tip_tx),
3445 tx_boundary(TIP - 1, tip_tx),
3446 tx_item(TIP - 1, tip_tx - 1),
3447 ] {
3448 for window in [(None, None), (Some(3), None), (Some(3), Some(TIP + 5))] {
3449 let options = after_options(Ordering::Ascending, cursor.clone());
3450 let resolved = resolve_tx_request(window, &options).unwrap();
3451 let context = format!("{cursor:?} {window:?}");
3452 assert!(resolved.is_empty(), "{context}");
3453 assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip, "{context}");
3454 assert_eq!(
3455 (resolved.end_checkpoint, resolved.end_position),
3456 (TIP, tip_tx),
3457 "{context}"
3458 );
3459 assert_eq!(
3460 resolved.entry_checkpoint,
3461 window.0.unwrap_or(0),
3462 "{context}"
3463 );
3464 }
3465
3466 let before = |position| {
3467 options_with(
3468 Ordering::Ascending,
3469 Some(cursor.clone()),
3470 Some(tx_boundary(TIP, position)),
3471 )
3472 };
3473 assert_eq!(
3474 resolve_transactions(&before(tip_tx + 1))
3475 .unwrap()
3476 .exhaustion,
3477 RangeExhaustion::LedgerTip,
3478 "a `before` past the tip leaves the tip as the high end: {cursor:?}"
3479 );
3480 assert_eq!(
3481 resolve_transactions(&before(tip_tx)).unwrap().exhaustion,
3482 cursor_bound,
3483 "a `before` at the tip bounds the scan: {cursor:?}"
3484 );
3485 }
3486
3487 let events = resolve_events(&after_options(
3488 Ordering::Ascending,
3489 ev_boundary(TIP, tip_tx, 0),
3490 ))
3491 .unwrap();
3492 assert!(events.is_empty());
3493 assert_eq!(events.exhaustion, RangeExhaustion::LedgerTip);
3494 assert_eq!(events.end_position, IntraTxCoordinate::start_of_tx(tip_tx));
3495
3496 let descending = resolve_transactions(&after_options(
3497 Ordering::Descending,
3498 tx_boundary(TIP, tip_tx),
3499 ))
3500 .unwrap();
3501 assert!(descending.is_empty());
3502 assert_eq!(descending.exhaustion, cursor_bound);
3503 assert_eq!(
3504 (descending.end_checkpoint, descending.end_position),
3505 (TIP, tip_tx)
3506 );
3507 }
3508
3509 #[test]
3513 fn descending_resume_claims_what_its_cursor_vouches_for() {
3514 let options = before_options(Ordering::Descending, tx_boundary(19, 92));
3516 let resolved = resolve_transactions(&options).unwrap();
3517 assert_eq!(resolved.range(), 0..92);
3518 assert_eq!(resolved.entry_checkpoint, TIP - 1);
3519 assert_eq!(
3520 crate::ledger_history::watermark::advance_covered_bound_before_checkpoint(
3521 None,
3522 18,
3523 resolved.entry_checkpoint,
3524 &options
3525 ),
3526 Some(19),
3527 "the first item, in checkpoint 18, claims checkpoint 19"
3528 );
3529 }
3530
3531 #[test]
3535 fn checkpoint_field_alone_never_empties_a_page() {
3536 let resolved = resolve_tx_request(
3539 (Some(4), None),
3540 &before_options(Ordering::Descending, tx_boundary(4, 23)),
3541 )
3542 .unwrap();
3543 assert_eq!(resolved.range(), 20..23);
3544 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
3545 assert_eq!((resolved.end_checkpoint, resolved.end_position), (4, 20));
3546
3547 let resolved =
3550 resolve_transactions(&after_options(Ordering::Descending, tx_boundary(TIP, 97)))
3551 .unwrap();
3552 assert_eq!(resolved.range(), 97..100);
3553 assert_eq!(
3554 resolved.exhaustion,
3555 RangeExhaustion::CursorBound {
3556 kind: sui_rpc_cursor::CursorKind::Boundary
3557 }
3558 );
3559 assert_eq!((resolved.end_checkpoint, resolved.end_position), (TIP, 97));
3560
3561 let options = options_with(
3564 Ordering::Ascending,
3565 Some(tx_boundary(TIP, 97)),
3566 Some(tx_boundary(TIP, 99)),
3567 );
3568 let resolved = resolve_transactions(&options).unwrap();
3569 assert_eq!(resolved.range(), 97..99);
3570 assert_eq!(
3571 resolved.exhaustion,
3572 RangeExhaustion::CursorBound {
3573 kind: sui_rpc_cursor::CursorKind::Boundary
3574 }
3575 );
3576 assert_eq!((resolved.end_checkpoint, resolved.end_position), (TIP, 99));
3577
3578 let resolved =
3581 resolve_transactions(&after_options(Ordering::Ascending, tx_boundary(TIP, 97)))
3582 .unwrap();
3583 assert_eq!(resolved.range(), 97..100);
3584 assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip);
3585
3586 let resolved = resolve_tx_request(
3589 (None, Some(10)),
3590 &after_options(Ordering::Ascending, tx_boundary(10, 47)),
3591 )
3592 .unwrap();
3593 assert_eq!(resolved.range(), 47..50);
3594 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
3595 assert_eq!((resolved.end_checkpoint, resolved.end_position), (10, 50));
3596 }
3597
3598 #[test]
3600 fn cursors_at_numeric_edges_resolve_without_overflow() {
3601 let resolved =
3603 resolve_transactions(&after_options(Ordering::Ascending, tx_boundary(0, 0))).unwrap();
3604 assert_eq!(resolved.range(), 0..100);
3605 let resolved = resolve_transactions(&before_options(
3607 Ordering::Ascending,
3608 tx_boundary(u64::MAX, u64::MAX),
3609 ))
3610 .unwrap();
3611 assert_eq!(resolved.range(), 0..100);
3612 let options = after_options(Ordering::Descending, tx_boundary(u64::MAX, u64::MAX));
3614 assert!(is_out_of_range(resolve_transactions(&options)));
3615 let resolved = resolve_tx_request((None, Some(10)), &options).unwrap();
3616 assert!(resolved.is_empty());
3617 }
3618
3619 #[test]
3621 fn legacy_cursors_resolve_like_current_ones() {
3622 #[derive(Clone, PartialEq, prost::Message)]
3623 struct LegacyCursorToken {
3624 #[prost(int32, tag = "1")]
3625 query_type: i32,
3626 #[prost(int32, tag = "2")]
3627 kind: i32,
3628 #[prost(uint64, optional, tag = "3")]
3629 checkpoint: Option<u64>,
3630 #[prost(uint64, optional, tag = "4")]
3631 position: Option<u64>,
3632 }
3633 let legacy = prost::Message::encode_to_vec(&LegacyCursorToken {
3635 query_type: 2,
3636 kind: 2,
3637 checkpoint: Some(4),
3638 position: Some(23),
3639 });
3640 let mut proto = ProtoQueryOptions::default();
3641 proto.ordering = Some(ProtoOrdering::Descending as i32);
3642 proto.before = Some(legacy.into());
3643 let options = query_options_from_proto(Some(&proto)).unwrap();
3644 assert_eq!(options.before, Some(tx_boundary(4, 23)));
3645 assert_eq!(resolve_transactions(&options).unwrap().range(), 0..23);
3646 }
3647
3648 fn without_checkpoint_field(cursor: &CursorToken) -> CursorToken {
3651 let position = match cursor.position {
3652 Position::Transactions { tx_seq, .. } => Position::Transactions {
3653 checkpoint: 0,
3654 tx_seq,
3655 },
3656 Position::Events {
3657 tx_seq,
3658 event_index,
3659 ..
3660 } => Position::Events {
3661 checkpoint: 0,
3662 tx_seq,
3663 event_index,
3664 },
3665 Position::Checkpoints { .. } => panic!("checkpoint cursors keep their checkpoint"),
3666 };
3667 CursorToken {
3668 kind: cursor.kind,
3669 position,
3670 }
3671 }
3672
3673 fn assert_resolves_without_checkpoint_fields<P: Copy + Eq + std::fmt::Debug>(
3677 cursors: &[CursorToken],
3678 resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
3679 ) {
3680 let mut requests = Vec::new();
3681 for after in cursors {
3682 requests.push((Some(after), None));
3683 requests.push((None, Some(after)));
3684 for before in cursors.iter().step_by(7) {
3685 requests.push((Some(after), Some(before)));
3686 }
3687 }
3688 for window in WINDOWS {
3689 for ordering in [Ordering::Ascending, Ordering::Descending] {
3690 for &(after, before) in &requests {
3691 let labelled = options_with(ordering, after.cloned(), before.cloned());
3692 let unlabelled = options_with(
3693 ordering,
3694 after.map(without_checkpoint_field),
3695 before.map(without_checkpoint_field),
3696 );
3697 let context = format!("window {window:?}, {labelled:?}");
3698 match (resolve(window, &labelled), resolve(window, &unlabelled)) {
3699 (Ok(labelled), Ok(unlabelled)) => {
3700 assert_eq!(labelled.bounds, unlabelled.bounds, "{context}");
3701 assert_eq!(labelled.end_position, unlabelled.end_position, "{context}");
3702 assert_eq!(labelled.exhaustion, unlabelled.exhaustion, "{context}");
3703 assert_eq!(
3704 labelled.entry_checkpoint, unlabelled.entry_checkpoint,
3705 "{context}"
3706 );
3707 }
3708 (Err(labelled), Err(unlabelled)) => {
3709 assert_eq!(status(labelled).0, status(unlabelled).0, "{context}")
3710 }
3711 (labelled, unlabelled) => panic!(
3712 "{context}: resolves {:?} with its checkpoint fields, {:?} without",
3713 labelled.map(|_| ()).map_err(status),
3714 unlabelled.map(|_| ()).map_err(status),
3715 ),
3716 }
3717 }
3718 }
3719 }
3720 }
3721
3722 #[test]
3725 fn cursors_resolve_the_same_without_their_checkpoint_field() {
3726 let txs = 0..=first_tx(TIP) + 1;
3727 assert_resolves_without_checkpoint_fields(
3728 &every_tx_cursor(txs.clone()),
3729 resolve_tx_request,
3730 );
3731 assert_resolves_without_checkpoint_fields(&every_event_cursor(txs), resolve_event_request);
3732 }
3733}