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::QueryEndReason;
10use sui_rpc::proto::sui::rpc::v2::QueryOptions as ProtoQueryOptions;
11use sui_rpc_cursor::CursorToken;
12use sui_rpc_cursor::Position;
13
14use crate::ErrorReason;
15use crate::RpcError;
16use crate::proto::google::rpc::bad_request::FieldViolation;
17
18const ORDERING_ASCENDING: i32 = ProtoOrdering::Ascending as i32;
19const ORDERING_DESCENDING: i32 = ProtoOrdering::Descending as i32;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
22pub enum Ordering {
23 Ascending,
24 Descending,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
31pub struct IntraTxCoordinate {
32 pub tx_seq: u64,
33 pub index: u32,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum RangeExhaustion {
42 LedgerTip,
44 CheckpointBound,
46 CursorBound { kind: sui_rpc_cursor::CursorKind },
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct QueryOptions {
56 pub limit_items: usize,
57 pub ordering: Ordering,
58 after: Option<CursorToken>,
59 before: Option<CursorToken>,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct ResolvedCheckpointRange {
68 pub range: Range<u64>,
71 pub exhaustion: RangeExhaustion,
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub struct ScanBounds<P> {
79 pub lo: Bound<P>,
80 pub hi: Bound<P>,
81}
82
83pub type IntraTxScanBounds = ScanBounds<IntraTxCoordinate>;
84
85#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct ResolvedScan<P> {
89 pub bounds: ScanBounds<P>,
92 pub entry_checkpoint: u64,
94 pub end_checkpoint: u64,
96 pub end_position: P,
98 pub exhaustion: RangeExhaustion,
100}
101
102pub trait ScanCursor<P> {
105 fn coordinate(&self) -> P;
106}
107
108impl IntraTxCoordinate {
109 pub fn start_of_tx(tx_seq: u64) -> Self {
112 Self { tx_seq, index: 0 }
113 }
114
115 pub fn tx_window(range: Range<u64>) -> Range<Self> {
117 Self::start_of_tx(range.start)..Self::start_of_tx(range.end)
118 }
119}
120
121impl RangeExhaustion {
122 pub fn reason(self) -> QueryEndReason {
123 match self {
124 Self::LedgerTip => QueryEndReason::LedgerTip,
125 Self::CheckpointBound => QueryEndReason::CheckpointBound,
126 Self::CursorBound { .. } => QueryEndReason::CursorBound,
127 }
128 }
129}
130
131impl QueryOptions {
132 pub fn checkpoints_from_proto(
133 request: Option<&ProtoQueryOptions>,
134 default_limit_items: u32,
135 max_limit_items: u32,
136 ) -> Result<Self, RpcError> {
137 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
138 matches!(position, Position::Checkpoints { .. })
139 })
140 }
141
142 pub fn transactions_from_proto(
143 request: Option<&ProtoQueryOptions>,
144 default_limit_items: u32,
145 max_limit_items: u32,
146 ) -> Result<Self, RpcError> {
147 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
148 matches!(position, Position::Transactions { .. })
149 })
150 }
151
152 pub fn events_from_proto(
153 request: Option<&ProtoQueryOptions>,
154 default_limit_items: u32,
155 max_limit_items: u32,
156 ) -> Result<Self, RpcError> {
157 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
158 matches!(position, Position::Events { .. })
159 })
160 }
161
162 fn from_proto_with_position(
163 request: Option<&ProtoQueryOptions>,
164 default_limit_items: u32,
165 max_limit_items: u32,
166 position_matches: fn(&Position) -> bool,
167 ) -> Result<Self, RpcError> {
168 let limit_items = request
169 .and_then(|options| options.limit)
170 .unwrap_or(default_limit_items)
171 .clamp(1, max_limit_items) as usize;
172
173 let ordering = match request.and_then(|options| options.ordering) {
174 None | Some(ORDERING_ASCENDING) => Ordering::Ascending,
175 Some(ORDERING_DESCENDING) => Ordering::Descending,
176 Some(_) => {
177 return Err(FieldViolation::new("options.ordering")
178 .with_description("invalid ordering")
179 .with_reason(ErrorReason::FieldInvalid)
180 .into());
181 }
182 };
183
184 let after = parse_cursor(
185 "options.after",
186 request.and_then(|options| options.after.as_ref()),
187 position_matches,
188 )?;
189 let before = parse_cursor(
190 "options.before",
191 request.and_then(|options| options.before.as_ref()),
192 position_matches,
193 )?;
194
195 Ok(Self {
196 limit_items,
197 ordering,
198 after,
199 before,
200 })
201 }
202
203 pub fn subscription() -> Self {
207 Self {
208 limit_items: usize::MAX,
209 ordering: Ordering::Ascending,
210 after: None,
211 before: None,
212 }
213 }
214
215 pub fn scan_direction(&self) -> ScanDirection {
216 match self.ordering {
217 Ordering::Ascending => ScanDirection::Ascending,
218 Ordering::Descending => ScanDirection::Descending,
219 }
220 }
221
222 pub fn is_ascending(&self) -> bool {
223 matches!(self.ordering, Ordering::Ascending)
224 }
225
226 pub fn has_after_cursor(&self) -> bool {
233 self.after.is_some()
234 }
235}
236
237impl ResolvedCheckpointRange {
238 pub fn empty_at(checkpoint: u64, exhaustion: RangeExhaustion) -> Self {
239 Self {
240 range: checkpoint..checkpoint,
241 exhaustion,
242 }
243 }
244
245 pub fn is_empty(&self) -> bool {
246 self.range.is_empty()
247 }
248
249 pub fn terminal_checkpoint(&self, ordering: Ordering) -> u64 {
250 match ordering {
251 Ordering::Ascending => self.range.end,
252 Ordering::Descending => self.range.start,
253 }
254 }
255}
256
257impl ResolvedScan<u64> {
258 pub fn range(&self) -> Range<u64> {
260 self.bounds.to_range()
261 }
262}
263
264impl<P: Copy + Ord> ScanBounds<P> {
265 pub fn from_range(range: Range<P>) -> Self {
266 Self {
267 lo: Bound::Included(range.start),
268 hi: Bound::Excluded(range.end),
269 }
270 }
271
272 pub fn empty_at(position: P) -> Self {
273 Self {
274 lo: Bound::Included(position),
275 hi: Bound::Excluded(position),
276 }
277 }
278
279 pub fn is_empty(&self) -> bool {
280 match (self.lo, self.hi) {
281 (Bound::Included(a), Bound::Excluded(b))
282 | (Bound::Excluded(a), Bound::Excluded(b))
283 | (Bound::Excluded(a), Bound::Included(b)) => a >= b,
284 (Bound::Included(a), Bound::Included(b)) => a > b,
285 (Bound::Unbounded, _) | (_, Bound::Unbounded) => false,
286 }
287 }
288
289 pub fn contains(&self, position: P) -> bool {
290 let above_lo = match self.lo {
291 Bound::Included(lo) => position >= lo,
292 Bound::Excluded(lo) => position > lo,
293 Bound::Unbounded => true,
294 };
295 let below_hi = match self.hi {
296 Bound::Included(hi) => position <= hi,
297 Bound::Excluded(hi) => position < hi,
298 Bound::Unbounded => true,
299 };
300 above_lo && below_hi
301 }
302}
303
304impl ScanBounds<u64> {
305 pub fn to_range(&self) -> Range<u64> {
306 let start = match self.lo {
307 Bound::Included(position) => position,
308 Bound::Excluded(position) => position.saturating_add(1),
309 Bound::Unbounded => 0,
310 };
311 let end = match self.hi {
312 Bound::Included(position) => position.saturating_add(1),
313 Bound::Excluded(position) => position,
314 Bound::Unbounded => u64::MAX,
315 };
316 start..end
317 }
318}
319
320impl ScanBounds<IntraTxCoordinate> {
321 pub fn tx_range(&self) -> Option<Range<u64>> {
326 let start_tx = match self.lo {
327 Bound::Included(position) | Bound::Excluded(position) => position.tx_seq,
328 Bound::Unbounded => 0,
329 };
330 let end_tx = match self.hi {
331 Bound::Excluded(position) if position.index == 0 => position.tx_seq,
332 Bound::Included(position) | Bound::Excluded(position) => {
333 position.tx_seq.saturating_add(1)
334 }
335 Bound::Unbounded => u64::MAX,
336 };
337 (start_tx < end_tx).then_some(start_tx..end_tx)
338 }
339}
340
341impl<P: Copy + Ord> ResolvedScan<P>
342where
343 CursorToken: ScanCursor<P>,
344{
345 pub fn resolve(
349 cp_range: ResolvedCheckpointRange,
350 range: Range<P>,
351 options: &QueryOptions,
352 ) -> Self {
353 let entry_checkpoint = if cp_range.is_empty() {
354 cp_range.range.end
356 } else if options.is_ascending() {
357 cp_range.range.start
358 } else {
359 cp_range.range.end.saturating_sub(1)
360 };
361
362 Self {
363 bounds: ScanBounds::from_range(range.start..range.end),
364 entry_checkpoint,
365 end_checkpoint: cp_range.terminal_checkpoint(options.ordering),
366 end_position: match options.ordering {
367 Ordering::Ascending => range.end,
368 Ordering::Descending => range.start,
369 },
370 exhaustion: cp_range.exhaustion,
371 }
372 .apply_cursor_bounds(options)
373 }
374
375 pub fn is_empty(&self) -> bool {
376 self.bounds.is_empty()
377 }
378
379 fn apply_cursor_bounds(mut self, options: &QueryOptions) -> Self {
380 if self.is_empty() {
381 return self;
382 }
383
384 let mut cursor_terminal = self.apply_after_cursor(options);
385
386 if let Some(recording) = self.apply_before_cursor(options) {
391 cursor_terminal = Some(recording);
392 }
393
394 if let Some((checkpoint, position, kind)) = cursor_terminal {
395 self.set_terminal_record(checkpoint, position, RangeExhaustion::CursorBound { kind });
396
397 self.bounds = ScanBounds::empty_at(self.end_position);
398 }
399
400 self
401 }
402
403 fn set_terminal_record(&mut self, checkpoint: u64, position: P, exhaustion: RangeExhaustion) {
404 self.end_checkpoint = checkpoint;
405 self.end_position = position;
406 self.exhaustion = exhaustion;
407 }
408
409 fn apply_after_cursor(
411 &mut self,
412 options: &QueryOptions,
413 ) -> Option<(u64, P, sui_rpc_cursor::CursorKind)> {
414 let cursor = options.after.as_ref()?;
415 let checkpoint = cursor.position.checkpoint();
416 let position: P = cursor.coordinate();
417
418 if options.is_ascending() {
419 self.entry_checkpoint = self.entry_checkpoint.max(checkpoint);
420 }
421
422 let candidate = match cursor.kind {
426 sui_rpc_cursor::CursorKind::Item => Bound::Excluded(position),
427 sui_rpc_cursor::CursorKind::Boundary => Bound::Included(position),
428 };
429
430 if !lower_bound_gte(candidate, self.bounds.lo) {
431 return None;
432 }
433
434 if !options.is_ascending() {
436 self.set_terminal_record(
437 checkpoint,
438 position,
439 RangeExhaustion::CursorBound {
440 kind: sui_rpc_cursor::CursorKind::Boundary,
441 },
442 );
443 }
444
445 self.bounds.lo = candidate;
446
447 if !self.bounds.is_empty() {
448 return None;
449 }
450
451 let kind = if options.is_ascending() {
455 cursor.kind
456 } else {
457 sui_rpc_cursor::CursorKind::Boundary
458 };
459 Some((checkpoint, position, kind))
460 }
461
462 fn apply_before_cursor(
464 &mut self,
465 options: &QueryOptions,
466 ) -> Option<(u64, P, sui_rpc_cursor::CursorKind)> {
467 let cursor = options.before.as_ref()?;
468 let checkpoint = cursor.position.checkpoint();
469 let position: P = cursor.coordinate();
470
471 if !options.is_ascending() {
472 self.entry_checkpoint = self.entry_checkpoint.min(checkpoint);
473 }
474
475 if !hi_admits_upper_bound(self.bounds.hi, position) {
476 return None;
477 }
478
479 if options.is_ascending() {
480 self.set_terminal_record(
481 checkpoint,
482 position,
483 RangeExhaustion::CursorBound {
484 kind: sui_rpc_cursor::CursorKind::Boundary,
485 },
486 );
487 }
488
489 self.bounds.hi = Bound::Excluded(position);
490
491 if !self.bounds.is_empty() {
492 return None;
493 }
494
495 Some((checkpoint, position, sui_rpc_cursor::CursorKind::Boundary))
496 }
497
498 pub fn apply_serving_floor(&mut self, floor: P, floor_checkpoint: u64, options: &QueryOptions) {
499 let floored_lo = Bound::Included(floor);
500 let floored = ScanBounds {
501 lo: floored_lo,
502 hi: self.bounds.hi,
503 };
504 if floored.is_empty() {
505 self.bounds = ScanBounds::empty_at(self.end_position);
508 return;
509 }
510 self.bounds.lo = floored_lo;
511 if options.is_ascending() {
512 self.entry_checkpoint = self.entry_checkpoint.max(floor_checkpoint);
513 } else {
514 self.end_checkpoint = floor_checkpoint;
515 self.end_position = floor;
516 }
517 }
518}
519
520pub fn validate_checkpoint_bounds(
525 start_checkpoint: Option<u64>,
526 end_checkpoint: Option<u64>,
527) -> Result<(), RpcError> {
528 let start = start_checkpoint.unwrap_or(0);
529 if let Some(end) = end_checkpoint
530 && end < start
531 {
532 return Err(FieldViolation::new("end_checkpoint")
533 .with_description("end_checkpoint must be greater than or equal to start_checkpoint")
534 .with_reason(ErrorReason::FieldInvalid)
535 .into());
536 }
537 Ok(())
538}
539
540impl ResolvedCheckpointRange {
541 pub fn from_request(
546 start_checkpoint: Option<u64>,
547 end_checkpoint: Option<u64>,
548 checkpoint_hi_exclusive: u64,
549 options: &QueryOptions,
550 ) -> Result<Self, RpcError> {
551 validate_checkpoint_bounds(start_checkpoint, end_checkpoint)?;
552 let start = start_checkpoint.unwrap_or(0);
553
554 let requested_end = end_checkpoint.unwrap_or(checkpoint_hi_exclusive);
555 let mut high_exhaustion =
556 if end_checkpoint.is_none() || requested_end > checkpoint_hi_exclusive {
557 RangeExhaustion::LedgerTip
558 } else {
559 RangeExhaustion::CheckpointBound
560 };
561 let mut start = start;
562 let mut end = requested_end.min(checkpoint_hi_exclusive);
563 let mut low_exhaustion = RangeExhaustion::CheckpointBound;
564 let mut cursor_bound = false;
565
566 if let Some(cursor) = &options.after
567 && cursor.position.checkpoint() >= start
568 {
569 start = cursor.position.checkpoint();
570 cursor_bound = true;
571 if matches!(options.ordering, Ordering::Descending) {
572 low_exhaustion = RangeExhaustion::CursorBound {
573 kind: sui_rpc_cursor::CursorKind::Boundary,
574 };
575 }
576 }
577
578 if let Some(cursor) = &options.before
579 && let Some(upper) = match cursor.kind {
580 sui_rpc_cursor::CursorKind::Item => cursor.position.checkpoint().checked_add(1),
581 sui_rpc_cursor::CursorKind::Boundary => Some(cursor.position.checkpoint()),
582 }
583 && upper <= end
584 {
585 end = upper;
586 cursor_bound = true;
587 if matches!(options.ordering, Ordering::Ascending) {
588 high_exhaustion = RangeExhaustion::CursorBound {
589 kind: sui_rpc_cursor::CursorKind::Boundary,
590 };
591 }
592 }
593
594 if start >= checkpoint_hi_exclusive {
595 return Ok(Self::empty_at(
596 checkpoint_hi_exclusive,
597 RangeExhaustion::LedgerTip,
598 ));
599 }
600
601 if start >= end {
602 let exhaustion = if cursor_bound {
603 RangeExhaustion::CursorBound {
604 kind: sui_rpc_cursor::CursorKind::Boundary,
605 }
606 } else {
607 match options.ordering {
608 Ordering::Ascending => high_exhaustion,
609 Ordering::Descending => low_exhaustion,
610 }
611 };
612 let checkpoint = match options.ordering {
613 Ordering::Ascending => end,
614 Ordering::Descending => start,
615 };
616 return Ok(Self::empty_at(checkpoint, exhaustion));
617 }
618
619 let exhaustion = match options.ordering {
620 Ordering::Ascending => high_exhaustion,
621 Ordering::Descending => low_exhaustion,
622 };
623 Ok(Self {
624 range: start..end,
625 exhaustion,
626 })
627 }
628}
629
630impl ScanCursor<u64> for CursorToken {
631 fn coordinate(&self) -> u64 {
632 match self.position {
633 Position::Checkpoints { checkpoint } => checkpoint,
634 Position::Transactions { tx_seq, .. } => tx_seq,
635 Position::Events { .. } => unreachable!("validated at decode"),
636 }
637 }
638}
639
640impl ScanCursor<IntraTxCoordinate> for CursorToken {
641 fn coordinate(&self) -> IntraTxCoordinate {
642 match self.position {
643 Position::Events {
644 tx_seq,
645 event_index,
646 ..
647 } => IntraTxCoordinate {
648 tx_seq,
649 index: event_index,
650 },
651 _ => unreachable!("validated at decode"),
652 }
653 }
654}
655
656impl From<IntraTxCoordinate> for (u64, u32) {
657 fn from(position: IntraTxCoordinate) -> Self {
658 (position.tx_seq, position.index)
659 }
660}
661
662impl From<(u64, u32)> for IntraTxCoordinate {
663 fn from((tx_seq, index): (u64, u32)) -> Self {
664 Self { tx_seq, index }
665 }
666}
667
668fn lower_bound_gte<P: Ord + Copy>(candidate: Bound<P>, current: Bound<P>) -> bool {
669 let Some(candidate) = lower_bound_key(candidate) else {
670 return false;
671 };
672 match lower_bound_key(current) {
673 Some(current) => candidate >= current,
674 None => true,
675 }
676}
677
678fn lower_bound_key<P: Ord + Copy>(bound: Bound<P>) -> Option<(P, u8)> {
679 match bound {
680 Bound::Included(position) => Some((position, 0)),
681 Bound::Excluded(position) => Some((position, 1)),
682 Bound::Unbounded => None,
683 }
684}
685
686fn hi_admits_upper_bound<P: Ord + Copy>(current: Bound<P>, candidate: P) -> bool {
687 match current {
688 Bound::Included(position) | Bound::Excluded(position) => candidate <= position,
689 Bound::Unbounded => true,
690 }
691}
692
693fn parse_cursor(
694 field: &'static str,
695 cursor: Option<&Bytes>,
696 position_matches: fn(&Position) -> bool,
697) -> Result<Option<CursorToken>, RpcError> {
698 cursor
699 .map(|cursor| {
700 CursorToken::decode(cursor).map_err(|_| invalid_cursor(field, "invalid cursor"))
701 })
702 .transpose()?
703 .map(|token| {
704 if position_matches(&token.position) {
705 Ok(token)
706 } else {
707 Err(invalid_cursor(field, "invalid cursor"))
708 }
709 })
710 .transpose()
711}
712
713fn invalid_cursor(field: &'static str, description: impl Into<String>) -> RpcError {
714 FieldViolation::new(field)
715 .with_description(description)
716 .with_reason(ErrorReason::FieldInvalid)
717 .into()
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 fn query_options_from_proto(
725 request: Option<&ProtoQueryOptions>,
726 ) -> Result<QueryOptions, RpcError> {
727 QueryOptions::transactions_from_proto(request, 100, 1_000)
728 }
729
730 fn resolved_range(range: Range<u64>) -> ResolvedScan<u64> {
731 ResolvedScan {
732 bounds: ScanBounds::from_range(range),
733 end_checkpoint: 20,
734 end_position: 20,
735 exhaustion: RangeExhaustion::CheckpointBound,
736 entry_checkpoint: 0,
737 }
738 }
739
740 fn empty_resolved_range(
741 end_checkpoint: u64,
742 end_position: u64,
743 exhaustion: RangeExhaustion,
744 ) -> ResolvedScan<u64> {
745 ResolvedScan {
746 bounds: ScanBounds::empty_at(end_position),
747 end_checkpoint,
748 end_position,
749 exhaustion,
750 entry_checkpoint: end_checkpoint,
751 }
752 }
753
754 fn tx_item(checkpoint: u64, tx_seq: u64) -> CursorToken {
755 CursorToken::item(Position::Transactions { checkpoint, tx_seq })
756 }
757
758 fn tx_boundary(checkpoint: u64, tx_seq: u64) -> CursorToken {
759 CursorToken::boundary(Position::Transactions { checkpoint, tx_seq })
760 }
761
762 fn cp_item(checkpoint: u64) -> CursorToken {
763 CursorToken::item(Position::Checkpoints { checkpoint })
764 }
765
766 fn ev_item(checkpoint: u64, tx_seq: u64, index: u32) -> CursorToken {
767 CursorToken::item(Position::Events {
768 checkpoint,
769 tx_seq,
770 event_index: index,
771 })
772 }
773
774 fn ev_boundary(checkpoint: u64, tx_seq: u64, index: u32) -> CursorToken {
775 CursorToken::boundary(Position::Events {
776 checkpoint,
777 tx_seq,
778 event_index: index,
779 })
780 }
781
782 fn resolved_intra_tx() -> ResolvedScan<IntraTxCoordinate> {
783 ResolvedScan {
784 bounds: IntraTxScanBounds::from_range(
785 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10),
786 ),
787 entry_checkpoint: 2,
788 end_checkpoint: 9,
789 end_position: IntraTxCoordinate::start_of_tx(10),
790 exhaustion: RangeExhaustion::CheckpointBound,
791 }
792 }
793
794 fn after_options(ordering: Ordering, cursor: CursorToken) -> QueryOptions {
795 QueryOptions {
796 limit_items: 100,
797 ordering,
798 after: Some(cursor),
799 before: None,
800 }
801 }
802
803 fn before_options(ordering: Ordering, cursor: CursorToken) -> QueryOptions {
804 QueryOptions {
805 limit_items: 100,
806 ordering,
807 after: None,
808 before: Some(cursor),
809 }
810 }
811
812 fn directional_options(ascending: bool) -> QueryOptions {
813 let mut request = ProtoQueryOptions::default();
814 if !ascending {
815 request.ordering = Some(ProtoOrdering::Descending as i32);
816 }
817 query_options_from_proto(Some(&request)).unwrap()
818 }
819
820 #[test]
826 fn tx_serving_floor_reconciles_or_canonicalizes_empty() {
827 let asc = directional_options(true);
828 let desc = directional_options(false);
829
830 let mut resolved = ResolvedScan {
833 bounds: ScanBounds::from_range(0..100),
834 end_checkpoint: 20,
835 end_position: 100,
836 exhaustion: RangeExhaustion::CheckpointBound,
837 entry_checkpoint: 0,
838 };
839 resolved.apply_serving_floor(50, 10, &asc);
840 assert_eq!(resolved.range(), 50..100);
841 assert_eq!(resolved.entry_checkpoint, 10);
842 assert_eq!(resolved.end_checkpoint, 20);
843 assert_eq!(resolved.end_position, 100);
844
845 let mut resolved = ResolvedScan {
848 bounds: ScanBounds::from_range(0..100),
849 end_checkpoint: 0,
850 end_position: 0,
851 exhaustion: RangeExhaustion::CheckpointBound,
852 entry_checkpoint: 20,
853 };
854 resolved.apply_serving_floor(50, 10, &desc);
855 assert_eq!(resolved.range(), 50..100);
856 assert_eq!(resolved.entry_checkpoint, 20);
857 assert_eq!(resolved.end_checkpoint, 10);
858 assert_eq!(resolved.end_position, 50);
859
860 for floor_tx in [40, 50] {
866 let mut resolved = ResolvedScan {
867 bounds: ScanBounds::from_range(0..40),
868 end_checkpoint: 8,
869 end_position: 40,
870 exhaustion: RangeExhaustion::CheckpointBound,
871 entry_checkpoint: 0,
872 };
873 resolved.apply_serving_floor(floor_tx, 10, &asc);
874 assert!(resolved.is_empty());
875 assert_eq!(resolved.range(), 40..40);
876 assert_eq!(resolved.entry_checkpoint, 0);
877 assert_eq!(resolved.end_checkpoint, 8);
878 assert_eq!(resolved.end_position, 40);
879
880 let mut resolved = ResolvedScan {
881 bounds: ScanBounds::from_range(0..40),
882 end_checkpoint: 0,
883 end_position: 0,
884 exhaustion: RangeExhaustion::CheckpointBound,
885 entry_checkpoint: 8,
886 };
887 resolved.apply_serving_floor(floor_tx, 10, &desc);
888 assert!(resolved.is_empty());
889 assert_eq!(resolved.range(), 0..0);
890 assert_eq!(resolved.entry_checkpoint, 8);
891 assert_eq!(resolved.end_checkpoint, 0);
892 assert_eq!(resolved.end_position, 0);
893 }
894 }
895
896 #[test]
899 fn event_serving_floor_reconciles_or_canonicalizes_empty() {
900 let asc = directional_options(true);
901 let desc = directional_options(false);
902
903 let mut resolved = ResolvedScan {
906 bounds: IntraTxScanBounds::from_range(
907 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(100),
908 ),
909 end_checkpoint: 20,
910 end_position: IntraTxCoordinate::start_of_tx(100),
911 exhaustion: RangeExhaustion::CheckpointBound,
912 entry_checkpoint: 0,
913 };
914 resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(50), 10, &asc);
915 assert_eq!(
916 resolved.bounds.lo,
917 Bound::Included(IntraTxCoordinate::start_of_tx(50))
918 );
919 assert_eq!(resolved.entry_checkpoint, 10);
920 assert_eq!(resolved.end_checkpoint, 20);
921 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(100));
922
923 let mut resolved = ResolvedScan {
925 bounds: IntraTxScanBounds::from_range(
926 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(100),
927 ),
928 end_checkpoint: 0,
929 end_position: IntraTxCoordinate::start_of_tx(0),
930 exhaustion: RangeExhaustion::CheckpointBound,
931 entry_checkpoint: 20,
932 };
933 resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(50), 10, &desc);
934 assert_eq!(
935 resolved.bounds.lo,
936 Bound::Included(IntraTxCoordinate::start_of_tx(50))
937 );
938 assert_eq!(resolved.entry_checkpoint, 20);
939 assert_eq!(resolved.end_checkpoint, 10);
940 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(50));
941
942 for floor_tx in [40, 50] {
946 let mut resolved = ResolvedScan {
947 bounds: IntraTxScanBounds::from_range(
948 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(40),
949 ),
950 end_checkpoint: 8,
951 end_position: IntraTxCoordinate::start_of_tx(40),
952 exhaustion: RangeExhaustion::CheckpointBound,
953 entry_checkpoint: 0,
954 };
955 resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(floor_tx), 10, &asc);
956 assert!(resolved.is_empty());
957 assert_eq!(
958 resolved.bounds,
959 IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(40))
960 );
961 assert_eq!(resolved.entry_checkpoint, 0);
962 assert_eq!(resolved.end_checkpoint, 8);
963 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(40));
964
965 let mut resolved = ResolvedScan {
966 bounds: IntraTxScanBounds::from_range(
967 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(40),
968 ),
969 end_checkpoint: 0,
970 end_position: IntraTxCoordinate::start_of_tx(0),
971 exhaustion: RangeExhaustion::CheckpointBound,
972 entry_checkpoint: 8,
973 };
974 resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(floor_tx), 10, &desc);
975 assert!(resolved.is_empty());
976 assert_eq!(
977 resolved.bounds,
978 IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(0))
979 );
980 assert_eq!(resolved.entry_checkpoint, 8);
981 assert_eq!(resolved.end_checkpoint, 0);
982 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(0));
983 }
984 }
985
986 #[test]
987 fn tx_range_covers_partial_endpoint_transactions() {
988 let bounds = IntraTxScanBounds {
989 lo: Bound::Included(IntraTxCoordinate {
990 tx_seq: 10,
991 index: 2,
992 }),
993 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(13)),
994 };
995
996 assert_eq!(bounds.tx_range(), Some(10..13));
997 }
998
999 #[test]
1000 fn tx_range_keeps_tx_of_nonzero_exclusive_hi() {
1001 let bounds = IntraTxScanBounds {
1002 lo: Bound::Unbounded,
1003 hi: Bound::Excluded(IntraTxCoordinate {
1004 tx_seq: 13,
1005 index: 1,
1006 }),
1007 };
1008
1009 assert_eq!(bounds.tx_range(), Some(0..14));
1010 }
1011
1012 #[test]
1013 fn tx_range_empty_bounds_yield_none() {
1014 let bounds = IntraTxScanBounds::from_range(
1015 IntraTxCoordinate::start_of_tx(10)..IntraTxCoordinate::start_of_tx(10),
1016 );
1017 assert_eq!(bounds.tx_range(), None);
1018 }
1019
1020 #[test]
1023 fn to_range_collapses_excluded_lo_at_successor() {
1024 let bounds = ScanBounds {
1025 lo: Bound::Excluded(14u64),
1026 hi: Bound::Excluded(20u64),
1027 };
1028 assert_eq!(bounds.to_range(), 15..20);
1029
1030 let bounds = ScanBounds {
1033 lo: Bound::Excluded(u64::MAX),
1034 hi: Bound::Unbounded,
1035 };
1036 assert!(bounds.to_range().is_empty());
1037 }
1038
1039 #[test]
1040 fn parses_cursors_and_ordering() {
1041 let after = tx_item(2, 20).encode();
1042 let before = tx_item(3, 30).encode();
1043 let mut request = ProtoQueryOptions::default();
1044 request.limit = Some(500);
1045 request.after = Some(after);
1046 request.before = Some(before);
1047 request.ordering = Some(ProtoOrdering::Descending as i32);
1048
1049 let options = query_options_from_proto(Some(&request)).unwrap();
1050
1051 assert_eq!(options.limit_items, 500);
1052 assert_eq!(options.ordering, Ordering::Descending);
1053 assert_eq!(options.scan_direction(), ScanDirection::Descending);
1054 assert_eq!(
1055 resolved_range(0..100).apply_cursor_bounds(&options).range(),
1056 21..30
1057 );
1058 }
1059
1060 #[test]
1061 fn has_after_cursor_reflects_only_the_after_field() {
1062 let options = query_options_from_proto(Some(&ProtoQueryOptions::default())).unwrap();
1064 assert!(!options.has_after_cursor());
1065
1066 let mut request = ProtoQueryOptions::default();
1068 request.before = Some(tx_item(3, 30).encode());
1069 let options = query_options_from_proto(Some(&request)).unwrap();
1070 assert!(!options.has_after_cursor());
1071
1072 let mut request = ProtoQueryOptions::default();
1074 request.after = Some(tx_item(2, 20).encode());
1075 let options = query_options_from_proto(Some(&request)).unwrap();
1076 assert!(options.has_after_cursor());
1077 }
1078
1079 #[test]
1080 fn clamps_limit_items_and_defaults_to_ascending() {
1081 let mut request = ProtoQueryOptions::default();
1082 request.limit = Some(5_000);
1083
1084 let options = query_options_from_proto(Some(&request)).unwrap();
1085
1086 assert_eq!(options.limit_items, 1_000);
1087 assert_eq!(options.ordering, Ordering::Ascending);
1088 assert_eq!(options.scan_direction(), ScanDirection::Ascending);
1089 }
1090
1091 #[test]
1092 fn rejects_malformed_cursors_and_unknown_ordering() {
1093 let mut request = ProtoQueryOptions::default();
1094 request.after = Some(Bytes::from_static(b"short"));
1095 assert!(query_options_from_proto(Some(&request)).is_err());
1096
1097 let mut request = ProtoQueryOptions::default();
1098 request.before = Some(Bytes::from_static(b"short"));
1099 assert!(query_options_from_proto(Some(&request)).is_err());
1100
1101 let mut request = ProtoQueryOptions::default();
1102 request.ordering = Some(99);
1103 assert!(query_options_from_proto(Some(&request)).is_err());
1104 }
1105
1106 #[test]
1107 fn rejects_cursor_for_different_position_variant() {
1108 let token = cp_item(9).encode();
1109 let mut request = ProtoQueryOptions::default();
1110 request.after = Some(token);
1111 assert!(query_options_from_proto(Some(&request)).is_err());
1112 }
1113
1114 #[test]
1115 fn accepts_cursor_regardless_of_filter_scope() {
1116 let after = tx_item(1, 9).encode();
1121 let before = tx_item(3, 30).encode();
1122 let mut request = ProtoQueryOptions::default();
1123 request.after = Some(after);
1124 request.before = Some(before);
1125 assert!(query_options_from_proto(Some(&request)).is_ok());
1126 }
1127
1128 #[test]
1129 fn accepts_cursors_for_different_checkpoint_range_and_ordering() {
1130 let token = tx_item(9, 9).encode();
1131 let mut request = ProtoQueryOptions::default();
1132 request.after = Some(token);
1133 request.ordering = Some(ProtoOrdering::Descending as i32);
1134
1135 let options = query_options_from_proto(Some(&request)).unwrap();
1136 let range =
1137 ResolvedCheckpointRange::from_request(Some(1_000), Some(1_100), 2_000, &options)
1138 .unwrap();
1139
1140 assert_eq!(range.range, 1_000..1_100);
1141 }
1142
1143 #[test]
1144 fn applies_canonical_cursor_bounds() {
1145 let options = QueryOptions {
1146 limit_items: 2,
1147 ordering: Ordering::Ascending,
1148 after: Some(tx_item(1, 11)),
1149 before: None,
1150 };
1151 assert_eq!(
1152 resolved_range(10..20).apply_cursor_bounds(&options).range(),
1153 12..20
1154 );
1155
1156 let options = QueryOptions {
1157 after: Some(tx_item(1, u64::MAX)),
1158 ..options
1159 };
1160 assert_eq!(
1161 resolved_range(10..20).apply_cursor_bounds(&options),
1162 empty_resolved_range(
1163 1,
1164 u64::MAX,
1165 RangeExhaustion::CursorBound {
1166 kind: sui_rpc_cursor::CursorKind::Item,
1167 },
1168 )
1169 );
1170
1171 let options = QueryOptions {
1172 ordering: Ordering::Descending,
1173 after: Some(tx_item(1, 11)),
1174 before: Some(tx_item(1, 19)),
1175 ..options
1176 };
1177 let bounded = resolved_range(10..20).apply_cursor_bounds(&options);
1178 assert_eq!(bounded.range(), 12..19);
1179 assert_eq!(
1180 bounded.exhaustion,
1181 RangeExhaustion::CursorBound {
1182 kind: sui_rpc_cursor::CursorKind::Boundary,
1183 }
1184 );
1185 assert_eq!(bounded.end_position, 11);
1186 }
1187
1188 #[test]
1192 fn descending_before_adjacent_to_after_empties_at_after_cursor() {
1193 let options = QueryOptions {
1194 limit_items: 2,
1195 ordering: Ordering::Descending,
1196 after: Some(tx_item(1, 11)),
1197 before: Some(tx_item(1, 12)),
1198 };
1199 let crossed = resolved_range(10..20).apply_cursor_bounds(&options);
1200 assert_eq!(
1201 crossed,
1202 ResolvedScan {
1203 bounds: ScanBounds {
1204 lo: Bound::Excluded(11),
1205 hi: Bound::Excluded(12),
1206 },
1207 entry_checkpoint: 0,
1208 end_checkpoint: 1,
1209 end_position: 11,
1210 exhaustion: RangeExhaustion::CursorBound {
1211 kind: sui_rpc_cursor::CursorKind::Boundary,
1212 },
1213 }
1214 );
1215 assert!(crossed.range().is_empty());
1216 }
1217
1218 #[test]
1219 fn applies_boundary_cursor_bounds_without_item_offset() {
1220 let options = QueryOptions {
1221 limit_items: 2,
1222 ordering: Ordering::Ascending,
1223 after: Some(tx_boundary(2, 20)),
1224 before: None,
1225 };
1226 assert_eq!(
1227 resolved_range(10..30).apply_cursor_bounds(&options).range(),
1228 20..30
1229 );
1230
1231 let options = QueryOptions {
1232 ordering: Ordering::Descending,
1233 after: None,
1234 before: Some(tx_boundary(2, 20)),
1235 ..options
1236 };
1237 assert_eq!(
1238 resolved_range(10..30).apply_cursor_bounds(&options).range(),
1239 10..20
1240 );
1241 }
1242
1243 #[test]
1244 fn resolves_checkpoint_range_with_terminal_reason() {
1245 let options = query_options_from_proto(None).unwrap();
1246 assert_eq!(
1247 ResolvedCheckpointRange::from_request(None, None, 20, &options)
1248 .unwrap()
1249 .exhaustion,
1250 RangeExhaustion::LedgerTip
1251 );
1252 assert!(ResolvedCheckpointRange::from_request(Some(10), Some(9), 20, &options).is_err());
1253
1254 let resolved = ResolvedCheckpointRange::from_request(Some(10), None, 20, &options).unwrap();
1255 assert_eq!(resolved.range, 10..20);
1256 assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip);
1257
1258 assert_eq!(
1259 ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap(),
1260 ResolvedCheckpointRange::empty_at(20, RangeExhaustion::LedgerTip)
1261 );
1262 }
1263
1264 #[test]
1268 fn resolves_checkpoint_range_no_longer_clamped_by_width() {
1269 let options = query_options_from_proto(None).unwrap();
1270 let resolved =
1271 ResolvedCheckpointRange::from_request(Some(10), Some(10_000_000), 10_000_000, &options)
1272 .unwrap();
1273 assert_eq!(resolved.range, 10..10_000_000);
1274 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
1275 }
1276
1277 #[test]
1280 fn resolve_window_past_tip_empties_with_ledger_tip() {
1281 for ascending in [true, false] {
1282 let mut request = ProtoQueryOptions::default();
1283 if !ascending {
1284 request.ordering = Some(ProtoOrdering::Descending as i32);
1285 }
1286 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1287 let cp_range =
1288 ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap();
1289 assert!(cp_range.is_empty());
1290 assert_eq!(
1291 ResolvedScan::<IntraTxCoordinate>::resolve(
1292 cp_range,
1293 IntraTxCoordinate::tx_window(100..100),
1294 &options
1295 ),
1296 ResolvedScan {
1297 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(100)),
1300 entry_checkpoint: 20,
1302 end_checkpoint: 20,
1303 end_position: IntraTxCoordinate::start_of_tx(100),
1304 exhaustion: RangeExhaustion::LedgerTip,
1305 }
1306 );
1307 }
1308 }
1309
1310 #[test]
1313 fn resolve_zero_width_window_empties_with_checkpoint_bound() {
1314 for ascending in [true, false] {
1315 let mut request = ProtoQueryOptions::default();
1316 if !ascending {
1317 request.ordering = Some(ProtoOrdering::Descending as i32);
1318 }
1319 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1320 let cp_range =
1321 ResolvedCheckpointRange::from_request(Some(10), Some(10), 20, &options).unwrap();
1322 assert!(cp_range.is_empty());
1323 assert_eq!(
1324 ResolvedScan::<IntraTxCoordinate>::resolve(
1325 cp_range,
1326 IntraTxCoordinate::tx_window(100..100),
1327 &options
1328 ),
1329 ResolvedScan {
1330 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(100)),
1331 entry_checkpoint: 10,
1332 end_checkpoint: 10,
1333 end_position: IntraTxCoordinate::start_of_tx(100),
1334 exhaustion: RangeExhaustion::CheckpointBound,
1335 }
1336 );
1337 }
1338 }
1339
1340 #[test]
1342 fn cursor_bounds_pass_empty_resolution_through_unchanged() {
1343 let position = Position::Events {
1344 checkpoint: 4,
1345 tx_seq: 50,
1346 event_index: 2,
1347 };
1348 for ascending in [true, false] {
1349 let mut request = ProtoQueryOptions::default();
1350 if !ascending {
1351 request.ordering = Some(ProtoOrdering::Descending as i32);
1352 }
1353 request.after = Some(CursorToken::item(position).encode());
1354 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1355 let cp_range =
1356 ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap();
1357 let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1358 cp_range,
1359 IntraTxCoordinate::tx_window(100..100),
1360 &options,
1361 );
1362 assert_eq!(resolved.clone().apply_cursor_bounds(&options), resolved);
1363 }
1364 }
1365
1366 #[test]
1369 fn resolve_orients_entry_and_terminal_by_ordering() {
1370 let options = directional_options(true);
1371 let cp_range =
1372 ResolvedCheckpointRange::from_request(Some(3), Some(10), 20, &options).unwrap();
1373 assert_eq!(cp_range.range, 3..10);
1374 let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1375 cp_range.clone(),
1376 IntraTxCoordinate::tx_window(100..200),
1377 &options,
1378 );
1379 assert_eq!(
1380 resolved.bounds,
1381 IntraTxScanBounds::from_range(
1382 IntraTxCoordinate::start_of_tx(100)..IntraTxCoordinate::start_of_tx(200)
1383 )
1384 );
1385 assert_eq!(resolved.entry_checkpoint, 3);
1386 assert_eq!(resolved.end_checkpoint, 10);
1387 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(200));
1388
1389 let options = directional_options(false);
1390 let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1391 cp_range,
1392 IntraTxCoordinate::tx_window(100..200),
1393 &options,
1394 );
1395 assert_eq!(resolved.entry_checkpoint, 9);
1396 assert_eq!(resolved.end_checkpoint, 3);
1397 assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(100));
1398 }
1399
1400 #[test]
1404 fn event_after_item_empty_interval_retains_item_kind() {
1405 let position = Position::Events {
1406 checkpoint: 1,
1407 tx_seq: 3,
1408 event_index: 0,
1409 };
1410 let resolved = ResolvedScan {
1411 bounds: IntraTxScanBounds::from_range(
1412 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(3),
1413 ),
1414 end_checkpoint: 1,
1415 end_position: IntraTxCoordinate::start_of_tx(3),
1416 exhaustion: RangeExhaustion::CheckpointBound,
1417 entry_checkpoint: 0,
1418 };
1419
1420 let mut request = ProtoQueryOptions::default();
1421 request.after = Some(CursorToken::item(position).encode());
1422 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1423 let item_bounded = resolved.clone().apply_cursor_bounds(&options);
1424
1425 assert!(item_bounded.is_empty());
1426 assert_eq!(
1427 item_bounded.end_position,
1428 IntraTxCoordinate {
1429 tx_seq: 3,
1430 index: 0,
1431 }
1432 );
1433 assert_eq!(
1434 item_bounded.exhaustion,
1435 RangeExhaustion::CursorBound {
1436 kind: sui_rpc_cursor::CursorKind::Item,
1437 }
1438 );
1439
1440 request.after = Some(CursorToken::boundary(position).encode());
1441 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1442 let boundary_bounded = resolved.apply_cursor_bounds(&options);
1443
1444 assert!(boundary_bounded.is_empty());
1445 assert_eq!(
1446 boundary_bounded.end_position,
1447 IntraTxCoordinate {
1448 tx_seq: 3,
1449 index: 0,
1450 }
1451 );
1452 assert_eq!(
1453 boundary_bounded.exhaustion,
1454 RangeExhaustion::CursorBound {
1455 kind: sui_rpc_cursor::CursorKind::Boundary,
1456 }
1457 );
1458 }
1459
1460 #[test]
1462 fn event_before_cursor_empties_descending_interval() {
1463 let resolved = ResolvedScan {
1465 bounds: IntraTxScanBounds::from_range(
1467 IntraTxCoordinate::start_of_tx(100)..IntraTxCoordinate::start_of_tx(200),
1468 ),
1469 entry_checkpoint: 9,
1471 end_checkpoint: 3,
1472 end_position: IntraTxCoordinate::start_of_tx(100),
1473 exhaustion: RangeExhaustion::CheckpointBound,
1474 };
1475
1476 let cursor = Position::Events {
1478 checkpoint: 2,
1479 tx_seq: 90,
1480 event_index: 0,
1481 };
1482
1483 for token in [CursorToken::item(cursor), CursorToken::boundary(cursor)] {
1484 let mut request = ProtoQueryOptions::default();
1485 request.ordering = Some(ProtoOrdering::Descending as i32);
1486 request.before = Some(token.encode());
1487 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1488
1489 let new_coordinate = IntraTxCoordinate::start_of_tx(90);
1490 assert_eq!(
1491 resolved.clone().apply_cursor_bounds(&options),
1492 ResolvedScan {
1493 bounds: IntraTxScanBounds::empty_at(new_coordinate),
1494 entry_checkpoint: 2,
1495 end_checkpoint: 2,
1496 end_position: new_coordinate,
1498 exhaustion: RangeExhaustion::CursorBound {
1499 kind: sui_rpc_cursor::CursorKind::Boundary,
1500 },
1501 }
1502 );
1503 }
1504 }
1505
1506 #[test]
1509 fn after_cursor_tightens_ascending_lower_bound() {
1510 let options = after_options(Ordering::Ascending, ev_item(3, 5, 1));
1511 assert_eq!(
1512 resolved_intra_tx().apply_cursor_bounds(&options),
1513 ResolvedScan {
1514 bounds: IntraTxScanBounds {
1515 lo: Bound::Excluded(IntraTxCoordinate {
1516 tx_seq: 5,
1517 index: 1,
1518 }),
1519 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1520 },
1521 entry_checkpoint: 3,
1522 ..resolved_intra_tx()
1523 }
1524 );
1525
1526 let options = after_options(Ordering::Ascending, ev_boundary(3, 5, 1));
1527 assert_eq!(
1528 resolved_intra_tx().apply_cursor_bounds(&options),
1529 ResolvedScan {
1530 bounds: IntraTxScanBounds {
1531 lo: Bound::Included(IntraTxCoordinate {
1532 tx_seq: 5,
1533 index: 1,
1534 }),
1535 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1536 },
1537 entry_checkpoint: 3,
1538 ..resolved_intra_tx()
1539 }
1540 );
1541 }
1542
1543 #[test]
1544 fn after_cursor_below_lower_bound_only_affects_entry_checkpoint() {
1545 let seed = ResolvedScan {
1546 bounds: IntraTxScanBounds {
1547 lo: Bound::Included(IntraTxCoordinate::start_of_tx(5)),
1548 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1549 },
1550 ..resolved_intra_tx()
1551 };
1552 let expected = ResolvedScan {
1553 entry_checkpoint: 4,
1554 ..seed.clone()
1555 };
1556 let options = after_options(Ordering::Ascending, ev_boundary(4, 2, 0));
1557 assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
1558 let options = after_options(Ordering::Ascending, ev_item(4, 2, 0));
1559 assert_eq!(seed.apply_cursor_bounds(&options), expected);
1560 }
1561
1562 #[test]
1565 fn descending_after_cursor_sets_terminal_and_tightens() {
1566 let options = after_options(Ordering::Descending, ev_item(3, 5, 1));
1567 assert_eq!(
1568 resolved_intra_tx().apply_cursor_bounds(&options),
1569 ResolvedScan {
1570 bounds: IntraTxScanBounds {
1571 lo: Bound::Excluded(IntraTxCoordinate {
1572 tx_seq: 5,
1573 index: 1,
1574 }),
1575 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1576 },
1577 end_checkpoint: 3,
1578 end_position: IntraTxCoordinate {
1579 tx_seq: 5,
1580 index: 1,
1581 },
1582 exhaustion: RangeExhaustion::CursorBound {
1583 kind: sui_rpc_cursor::CursorKind::Boundary,
1584 },
1585 ..resolved_intra_tx()
1586 }
1587 );
1588
1589 let options = after_options(Ordering::Descending, ev_boundary(3, 5, 1));
1590 assert_eq!(
1591 resolved_intra_tx().apply_cursor_bounds(&options),
1592 ResolvedScan {
1593 bounds: IntraTxScanBounds {
1594 lo: Bound::Included(IntraTxCoordinate {
1595 tx_seq: 5,
1596 index: 1,
1597 }),
1598 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1599 },
1600 end_checkpoint: 3,
1601 end_position: IntraTxCoordinate {
1602 tx_seq: 5,
1603 index: 1,
1604 },
1605 exhaustion: RangeExhaustion::CursorBound {
1606 kind: sui_rpc_cursor::CursorKind::Boundary,
1607 },
1608 ..resolved_intra_tx()
1609 }
1610 );
1611 }
1612
1613 #[test]
1617 fn after_cursor_empties_descending_interval() {
1618 let seed = ResolvedScan {
1619 bounds: IntraTxScanBounds::from_range(
1620 IntraTxCoordinate::start_of_tx(3)..IntraTxCoordinate::start_of_tx(9),
1621 ),
1622 entry_checkpoint: 9,
1623 end_checkpoint: 3,
1624 end_position: IntraTxCoordinate::start_of_tx(3),
1625 exhaustion: RangeExhaustion::CheckpointBound,
1626 };
1627
1628 let expected_coordinate = IntraTxCoordinate {
1631 tx_seq: 100,
1632 index: 10,
1633 };
1634 let expected = ResolvedScan {
1635 bounds: IntraTxScanBounds::empty_at(expected_coordinate),
1636 entry_checkpoint: 9,
1637 end_checkpoint: 100,
1638 end_position: expected_coordinate,
1639 exhaustion: RangeExhaustion::CursorBound {
1640 kind: sui_rpc_cursor::CursorKind::Boundary,
1641 },
1642 };
1643
1644 let options = after_options(Ordering::Descending, ev_item(100, 100, 10));
1645 assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
1646 let options = after_options(Ordering::Descending, ev_boundary(100, 100, 10));
1647 assert_eq!(seed.apply_cursor_bounds(&options), expected);
1648 }
1649
1650 #[test]
1654 fn before_cursor_tightens_descending_upper_bound() {
1655 let seed = ResolvedScan {
1656 entry_checkpoint: 8,
1657 ..resolved_intra_tx()
1658 };
1659 let expected = ResolvedScan {
1662 bounds: IntraTxScanBounds {
1663 lo: Bound::Included(IntraTxCoordinate::start_of_tx(0)),
1664 hi: Bound::Excluded(IntraTxCoordinate {
1665 tx_seq: 5,
1666 index: 1,
1667 }),
1668 },
1669 entry_checkpoint: 6,
1670 ..seed.clone()
1671 };
1672 let options = before_options(Ordering::Descending, ev_item(6, 5, 1));
1673 assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
1674 let options = before_options(Ordering::Descending, ev_boundary(6, 5, 1));
1675 assert_eq!(seed.apply_cursor_bounds(&options), expected);
1676 }
1677
1678 #[test]
1679 fn before_cursor_above_window_never_tightens() {
1680 let expected = ResolvedScan {
1681 entry_checkpoint: 1,
1682 ..resolved_intra_tx()
1683 };
1684 for cursor in [ev_boundary(1, 12, 0), ev_item(1, 12, 0)] {
1685 let options = before_options(Ordering::Descending, cursor.clone());
1686 assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
1687
1688 let options = before_options(Ordering::Ascending, cursor);
1689 assert_eq!(
1690 resolved_intra_tx().apply_cursor_bounds(&options),
1691 resolved_intra_tx()
1692 );
1693 }
1694 }
1695
1696 #[test]
1697 fn ascending_before_cursor_sets_terminal_and_tightens() {
1698 let expected = ResolvedScan {
1699 bounds: IntraTxScanBounds {
1700 lo: Bound::Included(IntraTxCoordinate::start_of_tx(0)),
1701 hi: Bound::Excluded(IntraTxCoordinate {
1702 tx_seq: 5,
1703 index: 1,
1704 }),
1705 },
1706 entry_checkpoint: 2,
1707 end_checkpoint: 6,
1708 end_position: IntraTxCoordinate {
1709 tx_seq: 5,
1710 index: 1,
1711 },
1712 exhaustion: RangeExhaustion::CursorBound {
1713 kind: sui_rpc_cursor::CursorKind::Boundary,
1714 },
1715 };
1716 let options = before_options(Ordering::Ascending, ev_boundary(6, 5, 1));
1717 assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
1718 let options = before_options(Ordering::Ascending, ev_item(6, 5, 1));
1719 assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
1720 }
1721
1722 #[test]
1725 fn before_cursor_empties_ascending_interval() {
1726 let seed = ResolvedScan {
1727 bounds: IntraTxScanBounds::from_range(
1728 IntraTxCoordinate::start_of_tx(3)..IntraTxCoordinate::start_of_tx(10),
1729 ),
1730 entry_checkpoint: 2,
1731 end_checkpoint: 9,
1732 ..resolved_intra_tx()
1733 };
1734 let expected = ResolvedScan {
1735 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(3)),
1736 end_checkpoint: 1,
1737 end_position: IntraTxCoordinate::start_of_tx(3),
1738 exhaustion: RangeExhaustion::CursorBound {
1739 kind: sui_rpc_cursor::CursorKind::Boundary,
1740 },
1741 ..seed.clone()
1742 };
1743 let options = before_options(Ordering::Ascending, ev_boundary(1, 3, 0));
1744 assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
1745 let options = before_options(Ordering::Ascending, ev_item(1, 3, 0));
1746 assert_eq!(seed.apply_cursor_bounds(&options), expected);
1747 }
1748
1749 #[test]
1753 fn ascending_with_both_cursors_picks_the_terminal_owner() {
1754 let options = QueryOptions {
1757 limit_items: 100,
1758 ordering: Ordering::Ascending,
1759 after: Some(ev_item(12, 12, 0)),
1760 before: Some(ev_boundary(14, 14, 0)),
1761 };
1762 assert_eq!(
1763 resolved_intra_tx().apply_cursor_bounds(&options),
1764 ResolvedScan {
1765 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(12)),
1766 entry_checkpoint: 12,
1767 end_checkpoint: 12,
1768 end_position: IntraTxCoordinate::start_of_tx(12),
1769 exhaustion: RangeExhaustion::CursorBound {
1770 kind: sui_rpc_cursor::CursorKind::Item,
1771 },
1772 }
1773 );
1774
1775 let options = QueryOptions {
1777 limit_items: 100,
1778 ordering: Ordering::Ascending,
1779 after: Some(ev_boundary(12, 12, 0)),
1780 before: Some(ev_boundary(3, 3, 0)),
1781 };
1782 assert_eq!(
1783 resolved_intra_tx().apply_cursor_bounds(&options),
1784 ResolvedScan {
1785 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(3)),
1786 entry_checkpoint: 12,
1787 end_checkpoint: 3,
1788 end_position: IntraTxCoordinate::start_of_tx(3),
1789 exhaustion: RangeExhaustion::CursorBound {
1790 kind: sui_rpc_cursor::CursorKind::Boundary,
1791 },
1792 }
1793 );
1794 }
1795
1796 #[test]
1800 fn descending_with_both_cursors_picks_the_terminal_owner() {
1801 let options = QueryOptions {
1804 limit_items: 100,
1805 ordering: Ordering::Descending,
1806 after: Some(ev_boundary(9, 12, 0)),
1807 before: Some(ev_boundary(1, 15, 0)),
1808 };
1809 assert_eq!(
1810 resolved_intra_tx().apply_cursor_bounds(&options),
1811 ResolvedScan {
1812 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(12)),
1813 entry_checkpoint: 1,
1814 end_checkpoint: 9,
1815 end_position: IntraTxCoordinate::start_of_tx(12),
1816 exhaustion: RangeExhaustion::CursorBound {
1817 kind: sui_rpc_cursor::CursorKind::Boundary,
1818 },
1819 }
1820 );
1821
1822 let options = QueryOptions {
1824 limit_items: 100,
1825 ordering: Ordering::Descending,
1826 after: Some(ev_boundary(9, 12, 0)),
1827 before: Some(ev_item(1, 5, 2)),
1828 };
1829 assert_eq!(
1830 resolved_intra_tx().apply_cursor_bounds(&options),
1831 ResolvedScan {
1832 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate {
1833 tx_seq: 5,
1834 index: 2,
1835 }),
1836 entry_checkpoint: 1,
1837 end_checkpoint: 1,
1838 end_position: IntraTxCoordinate {
1839 tx_seq: 5,
1840 index: 2,
1841 },
1842 exhaustion: RangeExhaustion::CursorBound {
1843 kind: sui_rpc_cursor::CursorKind::Boundary,
1844 },
1845 }
1846 );
1847
1848 let options = QueryOptions {
1851 limit_items: 100,
1852 ordering: Ordering::Descending,
1853 after: Some(ev_item(8, 5, 0)),
1854 before: Some(ev_item(1, 5, 0)),
1855 };
1856 assert_eq!(
1857 resolved_intra_tx().apply_cursor_bounds(&options),
1858 ResolvedScan {
1859 bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(5)),
1860 entry_checkpoint: 1,
1861 end_checkpoint: 1,
1862 end_position: IntraTxCoordinate::start_of_tx(5),
1863 exhaustion: RangeExhaustion::CursorBound {
1864 kind: sui_rpc_cursor::CursorKind::Boundary,
1865 },
1866 }
1867 );
1868 }
1869
1870 #[test]
1873 fn after_cursor_rejected_descending_is_noop() {
1874 let seed = ResolvedScan {
1875 bounds: IntraTxScanBounds {
1876 lo: Bound::Included(IntraTxCoordinate::start_of_tx(5)),
1877 hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1878 },
1879 ..resolved_intra_tx()
1880 };
1881 let options = after_options(Ordering::Descending, ev_boundary(4, 2, 0));
1882 assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
1883 }
1884
1885 #[test]
1889 fn cursor_at_the_window_bound_still_takes_the_terminal() {
1890 let options = before_options(Ordering::Ascending, ev_boundary(6, 10, 0));
1894 assert_eq!(
1895 resolved_intra_tx().apply_cursor_bounds(&options),
1896 ResolvedScan {
1897 bounds: IntraTxScanBounds::from_range(
1898 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10)
1899 ),
1900 entry_checkpoint: 2,
1901 end_checkpoint: 6,
1902 end_position: IntraTxCoordinate::start_of_tx(10),
1903 exhaustion: RangeExhaustion::CursorBound {
1904 kind: sui_rpc_cursor::CursorKind::Boundary,
1905 },
1906 }
1907 );
1908
1909 let options = after_options(Ordering::Descending, ev_boundary(6, 0, 0));
1910 assert_eq!(
1911 resolved_intra_tx().apply_cursor_bounds(&options),
1912 ResolvedScan {
1913 bounds: IntraTxScanBounds::from_range(
1914 IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10)
1915 ),
1916 entry_checkpoint: 2,
1917 end_checkpoint: 6,
1918 end_position: IntraTxCoordinate::start_of_tx(0),
1919 exhaustion: RangeExhaustion::CursorBound {
1920 kind: sui_rpc_cursor::CursorKind::Boundary,
1921 },
1922 }
1923 );
1924 }
1925
1926 #[test]
1929 fn tx_after_cursor_tightens_ascending_lower_bound() {
1930 let options = after_options(Ordering::Ascending, tx_item(3, 12));
1933 let tightened = resolved_range(10..20).apply_cursor_bounds(&options);
1935 assert_eq!(
1936 tightened,
1937 ResolvedScan {
1938 bounds: ScanBounds {
1939 lo: Bound::Excluded(12),
1940 hi: Bound::Excluded(20),
1941 },
1942 entry_checkpoint: 3,
1943 ..resolved_range(10..20)
1944 }
1945 );
1946 assert_eq!(tightened.range(), 13..20);
1948
1949 let options = after_options(Ordering::Ascending, tx_boundary(3, 12));
1950 assert_eq!(
1951 resolved_range(10..20).apply_cursor_bounds(&options),
1952 ResolvedScan {
1953 bounds: ScanBounds {
1954 lo: Bound::Included(12),
1955 hi: Bound::Excluded(20),
1956 },
1957 entry_checkpoint: 3,
1958 ..resolved_range(10..20)
1959 }
1960 );
1961 }
1962
1963 #[test]
1966 fn tx_descending_after_cursor_sets_terminal_and_tightens() {
1967 let options = after_options(Ordering::Descending, tx_item(3, 12));
1970 assert_eq!(
1971 resolved_range(10..20).apply_cursor_bounds(&options),
1972 ResolvedScan {
1973 bounds: ScanBounds {
1974 lo: Bound::Excluded(12),
1975 hi: Bound::Excluded(20),
1976 },
1977 entry_checkpoint: 0,
1978 end_checkpoint: 3,
1979 end_position: 12,
1980 exhaustion: RangeExhaustion::CursorBound {
1981 kind: sui_rpc_cursor::CursorKind::Boundary,
1982 },
1983 }
1984 );
1985 let options = after_options(Ordering::Descending, tx_boundary(3, 13));
1986 assert_eq!(
1987 resolved_range(10..20).apply_cursor_bounds(&options),
1988 ResolvedScan {
1989 bounds: ScanBounds::from_range(13..20),
1990 entry_checkpoint: 0,
1991 end_checkpoint: 3,
1992 end_position: 13,
1993 exhaustion: RangeExhaustion::CursorBound {
1994 kind: sui_rpc_cursor::CursorKind::Boundary,
1995 },
1996 }
1997 );
1998 }
1999
2000 #[test]
2004 fn tx_after_cursor_empties_descending_interval() {
2005 let options = after_options(Ordering::Descending, tx_boundary(9, 25));
2008 assert_eq!(
2009 resolved_range(10..20).apply_cursor_bounds(&options),
2010 ResolvedScan {
2011 bounds: ScanBounds::from_range(25..25),
2012 entry_checkpoint: 0,
2013 end_checkpoint: 9,
2014 end_position: 25,
2015 exhaustion: RangeExhaustion::CursorBound {
2016 kind: sui_rpc_cursor::CursorKind::Boundary,
2017 },
2018 }
2019 );
2020 let options = after_options(Ordering::Descending, tx_item(9, 24));
2021 assert_eq!(
2022 resolved_range(10..20).apply_cursor_bounds(&options),
2023 ResolvedScan {
2024 bounds: ScanBounds::from_range(24..24),
2025 entry_checkpoint: 0,
2026 end_checkpoint: 9,
2027 end_position: 24,
2028 exhaustion: RangeExhaustion::CursorBound {
2029 kind: sui_rpc_cursor::CursorKind::Boundary,
2030 },
2031 }
2032 );
2033 }
2034
2035 #[test]
2039 fn tx_after_item_and_successor_boundary_admit_the_same_interval() {
2040 let options = after_options(Ordering::Ascending, tx_item(5, 24));
2044 assert_eq!(
2045 resolved_range(10..20).apply_cursor_bounds(&options),
2046 ResolvedScan {
2047 bounds: ScanBounds::from_range(24..24),
2048 entry_checkpoint: 5,
2049 end_checkpoint: 5,
2050 end_position: 24,
2051 exhaustion: RangeExhaustion::CursorBound {
2052 kind: sui_rpc_cursor::CursorKind::Item,
2053 },
2054 }
2055 );
2056 let options = after_options(Ordering::Ascending, tx_boundary(5, 25));
2057 assert_eq!(
2058 resolved_range(10..20).apply_cursor_bounds(&options),
2059 ResolvedScan {
2060 bounds: ScanBounds::from_range(25..25),
2061 entry_checkpoint: 5,
2062 end_checkpoint: 5,
2063 end_position: 25,
2064 exhaustion: RangeExhaustion::CursorBound {
2065 kind: sui_rpc_cursor::CursorKind::Boundary,
2066 },
2067 }
2068 );
2069 }
2070
2071 #[test]
2074 fn tx_after_boundary_at_max_empties_interval() {
2075 let options = after_options(Ordering::Ascending, tx_boundary(5, u64::MAX));
2076 assert_eq!(
2077 resolved_range(10..20).apply_cursor_bounds(&options),
2078 empty_resolved_range(
2079 5,
2080 u64::MAX,
2081 RangeExhaustion::CursorBound {
2082 kind: sui_rpc_cursor::CursorKind::Boundary,
2083 },
2084 )
2085 );
2086 }
2087
2088 #[test]
2090 fn tx_after_item_at_max_lets_before_cursor_win() {
2091 let options = QueryOptions {
2095 limit_items: 100,
2096 ordering: Ordering::Ascending,
2097 after: Some(tx_item(5, u64::MAX)),
2098 before: Some(tx_boundary(3, 15)),
2099 };
2100 assert_eq!(
2101 resolved_range(10..20).apply_cursor_bounds(&options),
2102 ResolvedScan {
2103 bounds: ScanBounds::from_range(15..15),
2104 entry_checkpoint: 5,
2105 end_checkpoint: 3,
2106 end_position: 15,
2107 exhaustion: RangeExhaustion::CursorBound {
2108 kind: sui_rpc_cursor::CursorKind::Boundary,
2109 },
2110 }
2111 );
2112
2113 let seed = ResolvedScan {
2115 entry_checkpoint: 7,
2116 ..resolved_range(10..20)
2117 };
2118 let options = QueryOptions {
2119 limit_items: 100,
2120 ordering: Ordering::Descending,
2121 after: Some(tx_item(5, u64::MAX)),
2122 before: Some(tx_boundary(3, 15)),
2123 };
2124 assert_eq!(
2125 seed.apply_cursor_bounds(&options),
2126 ResolvedScan {
2127 bounds: ScanBounds::from_range(15..15),
2128 entry_checkpoint: 3,
2129 end_checkpoint: 3,
2130 end_position: 15,
2131 exhaustion: RangeExhaustion::CursorBound {
2132 kind: sui_rpc_cursor::CursorKind::Boundary,
2133 },
2134 }
2135 );
2136 }
2137
2138 #[test]
2145 fn raw_after_item_echo_resumes_like_successor_boundary() {
2146 let item = after_options(Ordering::Ascending, tx_item(3, 24));
2147 let successor = after_options(Ordering::Ascending, tx_boundary(3, 25));
2148
2149 assert_eq!(
2151 resolved_range(10..30).apply_cursor_bounds(&item).range(),
2152 resolved_range(10..30)
2153 .apply_cursor_bounds(&successor)
2154 .range(),
2155 );
2156
2157 assert!(resolved_range(10..20).apply_cursor_bounds(&item).is_empty());
2159 assert!(
2160 resolved_range(10..20)
2161 .apply_cursor_bounds(&successor)
2162 .is_empty()
2163 );
2164 }
2165
2166 #[test]
2171 fn tx_after_item_at_window_edge_defers_to_drain() {
2172 let options = after_options(Ordering::Ascending, tx_item(5, 19));
2173 let initial_range = resolved_range(10..20);
2174 let resolved = initial_range.clone().apply_cursor_bounds(&options);
2175 assert_eq!(
2176 resolved,
2177 ResolvedScan {
2178 bounds: ScanBounds {
2179 lo: Bound::Excluded(19),
2180 hi: Bound::Excluded(20),
2181 },
2182 entry_checkpoint: 5,
2183 ..initial_range
2184 }
2185 );
2186 assert!(!resolved.is_empty());
2187 assert!(resolved.range().is_empty());
2188 }
2189
2190 #[test]
2193 fn tx_after_item_at_window_edge_descending_keeps_stamped_terminal() {
2194 let options = after_options(Ordering::Descending, tx_item(5, 19));
2195 let initial_range = resolved_range(10..20);
2196 let resolved = initial_range.clone().apply_cursor_bounds(&options);
2197 assert_eq!(
2198 resolved,
2199 ResolvedScan {
2200 bounds: ScanBounds {
2201 lo: Bound::Excluded(19),
2202 hi: Bound::Excluded(20),
2203 },
2204 end_checkpoint: 5,
2205 end_position: 19,
2206 exhaustion: RangeExhaustion::CursorBound {
2207 kind: sui_rpc_cursor::CursorKind::Boundary,
2208 },
2209 ..initial_range
2210 }
2211 );
2212 assert!(resolved.range().is_empty());
2213 }
2214
2215 #[test]
2219 fn tx_after_item_below_window_start_descending_is_noop() {
2220 let options = after_options(Ordering::Descending, tx_item(5, 9));
2221 assert_eq!(
2222 resolved_range(10..20).apply_cursor_bounds(&options),
2223 resolved_range(10..20)
2224 );
2225 }
2226
2227 #[test]
2228 fn item_cursor_can_be_used_as_after_or_before() {
2229 let token = CursorToken::item(Position::Transactions {
2230 checkpoint: 1,
2231 tx_seq: 11,
2232 })
2233 .encode();
2234
2235 let mut request = ProtoQueryOptions::default();
2236 request.after = Some(token.clone());
2237 let options = query_options_from_proto(Some(&request)).unwrap();
2238 assert_eq!(
2239 resolved_range(10..20).apply_cursor_bounds(&options).range(),
2240 12..20
2241 );
2242
2243 request.after = None;
2244 request.before = Some(token);
2245 let options = query_options_from_proto(Some(&request)).unwrap();
2246 assert_eq!(
2247 resolved_range(10..20).apply_cursor_bounds(&options).range(),
2248 10..11
2249 );
2250 }
2251}