1use std::{collections::VecDeque, fmt::Debug};
29
30use crate::{
31 backend::{EventStore, IndexKind, ScanDirection},
32 entry::EventStoreEntry,
33 error::EventStoreError,
34 manifest::RunManifest,
35 snapshot::SnapshotAnchor,
36};
37
38pub const DEFAULT_SCAN_CHUNK_SIZE: u64 = 1_024;
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct SnapshotReplayPlan {
48 pub anchor: Option<SnapshotAnchor>,
50 pub from_seq: u64,
52 pub to_seq: u64,
54}
55
56impl SnapshotReplayPlan {
57 #[must_use]
59 pub const fn is_empty(&self) -> bool {
60 self.from_seq > self.to_seq
61 }
62}
63
64#[derive(Debug)]
70pub struct EventStoreReader<B> {
71 backend: B,
72}
73
74impl<B: EventStore> EventStoreReader<B> {
75 #[must_use]
77 pub const fn new(backend: B) -> Self {
78 Self { backend }
79 }
80
81 pub fn manifest(&self) -> Result<RunManifest, EventStoreError> {
87 self.backend.manifest()
88 }
89
90 pub fn high_watermark(&self) -> Result<u64, EventStoreError> {
96 self.backend.high_watermark()
97 }
98
99 pub fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
110 self.backend.scan_seq(seq)
111 }
112
113 pub fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
119 self.backend.lookup(kind, key)
120 }
121
122 pub fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
132 self.backend.latest_snapshot_anchor()
133 }
134
135 pub fn snapshot_replay_plan(&self) -> Result<SnapshotReplayPlan, EventStoreError> {
147 let anchor = self.latest_snapshot_anchor()?;
148 let to_seq = self.high_watermark()?;
149 let from_seq = match anchor.as_ref() {
150 Some(anchor) if anchor.high_watermark > to_seq => {
151 return Err(EventStoreError::Corrupted(format!(
152 "snapshot anchor high_watermark {} exceeds durable high_watermark {to_seq}",
153 anchor.high_watermark,
154 )));
155 }
156 Some(anchor) => anchor.high_watermark.saturating_add(1),
157 None => 1,
158 };
159
160 Ok(SnapshotReplayPlan {
161 anchor,
162 from_seq,
163 to_seq,
164 })
165 }
166
167 pub fn scan_snapshot_replay_tail(
177 &self,
178 ) -> Result<(SnapshotReplayPlan, RangeScan<'_>), EventStoreError> {
179 let plan = self.snapshot_replay_plan()?;
180 let scan = self.scan_range(plan.from_seq, plan.to_seq, ScanDirection::Forward);
181 Ok((plan, scan))
182 }
183
184 #[must_use]
191 pub fn scan_range(&self, from: u64, to: u64, direction: ScanDirection) -> RangeScan<'_> {
192 RangeScan::new(&self.backend, from, to, direction, DEFAULT_SCAN_CHUNK_SIZE)
193 }
194
195 #[must_use]
201 pub fn scan_range_chunked(
202 &self,
203 from: u64,
204 to: u64,
205 direction: ScanDirection,
206 chunk_size: u64,
207 ) -> RangeScan<'_> {
208 RangeScan::new(&self.backend, from, to, direction, chunk_size.max(1))
209 }
210
211 #[must_use]
213 pub fn into_inner(self) -> B {
214 self.backend
215 }
216
217 #[must_use]
219 pub const fn backend(&self) -> &B {
220 &self.backend
221 }
222}
223
224pub struct RangeScan<'a> {
231 backend: &'a dyn EventStore,
232 direction: ScanDirection,
233 chunk_size: u64,
234 cursor: u64,
235 end: u64,
236 buffer: VecDeque<EventStoreEntry>,
237 has_more: bool,
238 reverse_clamped: bool,
247}
248
249impl Debug for RangeScan<'_> {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.debug_struct(stringify!(RangeScan))
252 .field("direction", &self.direction)
253 .field("chunk_size", &self.chunk_size)
254 .field("cursor", &self.cursor)
255 .field("end", &self.end)
256 .field("buffered", &self.buffer.len())
257 .field("has_more", &self.has_more)
258 .field("reverse_clamped", &self.reverse_clamped)
259 .finish()
260 }
261}
262
263impl<'a> RangeScan<'a> {
264 fn new(
265 backend: &'a dyn EventStore,
266 from: u64,
267 to: u64,
268 direction: ScanDirection,
269 chunk_size: u64,
270 ) -> Self {
271 let valid = from != 0 && from <= to;
275 let (cursor, end) = if valid {
276 match direction {
277 ScanDirection::Forward => (from, to),
278 ScanDirection::Reverse => (to, from),
279 }
280 } else {
281 (0, 0)
282 };
283
284 Self {
285 backend,
286 direction,
287 chunk_size: chunk_size.max(1),
288 cursor,
289 end,
290 buffer: VecDeque::new(),
291 has_more: valid,
292 reverse_clamped: false,
293 }
294 }
295
296 fn fetch_chunk(&mut self) -> Option<Result<(), EventStoreError>> {
297 if !self.has_more {
298 return None;
299 }
300
301 if matches!(self.direction, ScanDirection::Reverse) && !self.reverse_clamped {
302 match self.backend.high_watermark() {
303 Ok(hwm) => {
304 if hwm == 0 || hwm < self.end {
305 self.has_more = false;
306 return Some(Ok(()));
307 }
308 self.cursor = self.cursor.min(hwm);
309 self.reverse_clamped = true;
310 }
311 Err(e) => {
312 self.has_more = false;
313 return Some(Err(e));
314 }
315 }
316 }
317 let (chunk_lo, chunk_hi) = match self.direction {
318 ScanDirection::Forward => {
319 let lo = self.cursor;
320 let hi = lo
321 .saturating_add(self.chunk_size)
322 .saturating_sub(1)
323 .min(self.end);
324 (lo, hi)
325 }
326 ScanDirection::Reverse => {
327 let hi = self.cursor;
328 let lo = hi
329 .saturating_sub(self.chunk_size.saturating_sub(1))
330 .max(self.end);
331 (lo, hi)
332 }
333 };
334
335 match self.backend.scan_range(chunk_lo, chunk_hi, self.direction) {
336 Ok(entries) => {
337 if entries.is_empty() {
338 self.has_more = false;
342 return Some(Ok(()));
343 }
344
345 match self.direction {
346 ScanDirection::Forward => {
347 let last_seq = entries.last().map_or(chunk_hi, |entry| entry.seq);
352
353 if last_seq >= self.end {
354 self.has_more = false;
355 } else {
356 self.cursor = last_seq + 1;
357 }
358 }
359 ScanDirection::Reverse => {
360 if chunk_lo <= self.end {
361 self.has_more = false;
362 } else {
363 self.cursor = chunk_lo - 1;
364 }
365 }
366 }
367 self.buffer.extend(entries);
368 Some(Ok(()))
369 }
370 Err(e) => {
371 self.has_more = false;
372 Some(Err(e))
373 }
374 }
375 }
376}
377
378impl Iterator for RangeScan<'_> {
379 type Item = Result<EventStoreEntry, EventStoreError>;
380
381 fn next(&mut self) -> Option<Self::Item> {
382 loop {
383 if let Some(entry) = self.buffer.pop_front() {
384 return Some(Ok(entry));
385 }
386
387 match self.fetch_chunk() {
388 Some(Ok(())) => {}
389 Some(Err(e)) => return Some(Err(e)),
390 None => return None,
391 }
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use bytes::Bytes;
399 use indexmap::IndexMap;
400 use nautilus_core::UnixNanos;
401 use rstest::{fixture, rstest};
402 use ustr::Ustr;
403
404 use super::*;
405 use crate::{
406 backend::{AppendEntry, IndexKey, MemoryBackend},
407 compute_entry_hash,
408 entry::Topic,
409 headers::Headers,
410 manifest::{RegisteredComponents, RunManifest, RunStatus},
411 };
412
413 fn manifest(run_id: &str) -> RunManifest {
414 RunManifest {
415 run_id: run_id.to_string(),
416 parent_run_id: None,
417 instance_id: "trader-001".to_string(),
418 binary_hash: "deadbeef".to_string(),
419 schema_version: 1,
420 crate_versions: "feedface".to_string(),
421 feature_flags: Vec::new(),
422 adapter_versions: IndexMap::new(),
423 config_hash: "cafebabe".to_string(),
424 registered_components: RegisteredComponents::default(),
425 seed: None,
426 start_ts_init: UnixNanos::from(0),
427 end_ts_init: None,
428 high_watermark: 0,
429 status: RunStatus::Running,
430 }
431 }
432
433 fn build_entry(seq: u64, ts_init: u64) -> EventStoreEntry {
434 let topic: Topic = "exec.command.SubmitOrder".into();
435 let payload_type = Ustr::from("SubmitOrder");
436 let payload = Bytes::from_static(b"\x01\x02\x03\x04");
437 let headers = Headers::empty();
438 let ts_publish = UnixNanos::from(ts_init + 1);
439 let ts_init = UnixNanos::from(ts_init);
440 let hash = compute_entry_hash(
441 seq,
442 ts_init,
443 ts_publish,
444 topic.as_ref(),
445 payload_type.as_str(),
446 &payload,
447 &headers,
448 );
449
450 EventStoreEntry::new(
451 hash,
452 seq,
453 headers,
454 topic,
455 payload_type,
456 payload,
457 ts_init,
458 ts_publish,
459 )
460 }
461
462 fn append_with(seq: u64, ts_init: u64, index_keys: Vec<IndexKey>) -> AppendEntry {
463 AppendEntry::new(build_entry(seq, ts_init), index_keys)
464 }
465
466 #[derive(Debug)]
469 struct GrowingBackend {
470 inner: std::cell::RefCell<MemoryBackend>,
471 grown: std::cell::Cell<bool>,
472 }
473
474 impl EventStore for GrowingBackend {
475 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
476 unreachable!("test wrapper does not forward open_run")
477 }
478
479 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
480 self.inner.borrow_mut().append_batch(entries)
481 }
482
483 fn scan_range(
484 &self,
485 from: u64,
486 to: u64,
487 direction: ScanDirection,
488 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
489 let result = self.inner.borrow().scan_range(from, to, direction);
490
491 if !self.grown.replace(true) {
492 self.inner
493 .borrow_mut()
494 .append_batch(&[
495 append_with(3, 103, Vec::new()),
496 append_with(4, 104, Vec::new()),
497 ])
498 .expect("grow run between chunks");
499 }
500 result
501 }
502
503 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
504 self.inner.borrow().scan_seq(seq)
505 }
506
507 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
508 self.inner.borrow().lookup(kind, key)
509 }
510
511 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
512 self.inner.borrow().iter_index_keys(kind)
513 }
514
515 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
516 self.inner.borrow_mut().seal(status)
517 }
518
519 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
520 self.inner.borrow().manifest()
521 }
522
523 fn high_watermark(&self) -> Result<u64, EventStoreError> {
524 self.inner.borrow().high_watermark()
525 }
526 }
527
528 fn populated(count: u64) -> EventStoreReader<MemoryBackend> {
529 let mut backend = MemoryBackend::new();
530 backend.open_run(manifest("run-reader")).expect("open run");
531 let batch: Vec<AppendEntry> = (1..=count)
532 .map(|seq| append_with(seq, 100 + seq, Vec::new()))
533 .collect();
534 backend.append_batch(&batch).expect("append");
535 EventStoreReader::new(backend)
536 }
537
538 #[derive(Debug)]
539 struct AnchorPastWatermarkBackend;
540
541 impl EventStore for AnchorPastWatermarkBackend {
542 fn open_run(&mut self, _manifest: RunManifest) -> Result<(), EventStoreError> {
543 Ok(())
544 }
545
546 fn append_batch(&mut self, _entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
547 Ok(1)
548 }
549
550 fn scan_range(
551 &self,
552 _from: u64,
553 _to: u64,
554 _direction: ScanDirection,
555 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
556 Ok(Vec::new())
557 }
558
559 fn scan_seq(&self, _seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
560 Ok(None)
561 }
562
563 fn lookup(&self, _kind: IndexKind, _key: &str) -> Result<Option<u64>, EventStoreError> {
564 Ok(None)
565 }
566
567 fn iter_index_keys(&self, _kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
568 Ok(Vec::new())
569 }
570
571 fn record_snapshot_anchor(
572 &mut self,
573 _anchor: SnapshotAnchor,
574 ) -> Result<(), EventStoreError> {
575 Ok(())
576 }
577
578 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
579 Ok(Some(SnapshotAnchor::new(
580 2,
581 "cache://snapshots/run-reader/2",
582 "blake3:abc",
583 )))
584 }
585
586 fn seal(&mut self, _status: RunStatus) -> Result<(), EventStoreError> {
587 Ok(())
588 }
589
590 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
591 Ok(manifest("run-anchor-past-watermark"))
592 }
593
594 fn high_watermark(&self) -> Result<u64, EventStoreError> {
595 Ok(1)
596 }
597 }
598
599 #[fixture]
600 fn reader_with_three() -> EventStoreReader<MemoryBackend> {
601 populated(3)
602 }
603
604 #[rstest]
605 fn manifest_delegates_to_backend(reader_with_three: EventStoreReader<MemoryBackend>) {
606 let m = reader_with_three.manifest().expect("manifest");
607
608 assert_eq!(m.run_id, "run-reader");
609 assert_eq!(m.high_watermark, 3);
610 }
611
612 #[rstest]
613 fn high_watermark_delegates_to_backend(reader_with_three: EventStoreReader<MemoryBackend>) {
614 assert_eq!(reader_with_three.high_watermark().expect("hwm"), 3);
615 }
616
617 #[rstest]
618 fn latest_snapshot_anchor_delegates_to_backend() {
619 let mut backend = MemoryBackend::new();
620 backend.open_run(manifest("run-anchor")).expect("open run");
621 backend
622 .append_batch(&[append_with(1, 101, Vec::new())])
623 .expect("append");
624 let anchor = SnapshotAnchor::new(1, "cache://snapshots/run-anchor/1", "blake3:abc");
625 backend
626 .record_snapshot_anchor(anchor.clone())
627 .expect("record anchor");
628 let reader = EventStoreReader::new(backend);
629
630 assert_eq!(
631 reader.latest_snapshot_anchor().expect("latest anchor"),
632 Some(anchor),
633 );
634 }
635
636 #[rstest]
637 fn snapshot_replay_plan_without_anchor_replays_from_start(
638 reader_with_three: EventStoreReader<MemoryBackend>,
639 ) {
640 let plan = reader_with_three
641 .snapshot_replay_plan()
642 .expect("snapshot replay plan");
643
644 assert_eq!(
645 plan,
646 SnapshotReplayPlan {
647 anchor: None,
648 from_seq: 1,
649 to_seq: 3,
650 },
651 );
652 assert!(!plan.is_empty());
653 }
654
655 #[rstest]
656 fn snapshot_replay_plan_with_anchor_starts_after_anchor_watermark() {
657 let mut backend = MemoryBackend::new();
658 backend.open_run(manifest("run-anchor")).expect("open run");
659 backend
660 .append_batch(&[
661 append_with(1, 101, Vec::new()),
662 append_with(2, 102, Vec::new()),
663 append_with(3, 103, Vec::new()),
664 ])
665 .expect("append");
666 let anchor = SnapshotAnchor::new(2, "cache://snapshots/run-anchor/2", "blake3:abc");
667 backend
668 .record_snapshot_anchor(anchor.clone())
669 .expect("record anchor");
670 let reader = EventStoreReader::new(backend);
671
672 let plan = reader.snapshot_replay_plan().expect("snapshot replay plan");
673
674 assert_eq!(
675 plan,
676 SnapshotReplayPlan {
677 anchor: Some(anchor),
678 from_seq: 3,
679 to_seq: 3,
680 },
681 );
682 assert!(!plan.is_empty());
683 }
684
685 #[rstest]
686 fn snapshot_replay_plan_rejects_anchor_past_watermark() {
687 let reader = EventStoreReader::new(AnchorPastWatermarkBackend);
688 let err = reader
689 .snapshot_replay_plan()
690 .expect_err("anchor past watermark must fail");
691
692 match err {
693 EventStoreError::Corrupted(msg) => {
694 assert!(
695 msg.contains("exceeds durable high_watermark"),
696 "msg was: {msg}",
697 );
698 }
699 other => panic!("expected Corrupted, was {other:?}"),
700 }
701 }
702
703 #[rstest]
704 fn scan_snapshot_replay_tail_yields_entries_after_anchor() {
705 let mut backend = MemoryBackend::new();
706 backend.open_run(manifest("run-anchor")).expect("open run");
707 backend
708 .append_batch(&[
709 append_with(1, 101, Vec::new()),
710 append_with(2, 102, Vec::new()),
711 append_with(3, 103, Vec::new()),
712 ])
713 .expect("append");
714 backend
715 .record_snapshot_anchor(SnapshotAnchor::new(
716 2,
717 "cache://snapshots/run-anchor/2",
718 "blake3:abc",
719 ))
720 .expect("record anchor");
721 let reader = EventStoreReader::new(backend);
722
723 let (plan, scan) = reader
724 .scan_snapshot_replay_tail()
725 .expect("snapshot replay tail");
726 let seqs: Vec<_> = scan.map(|entry| entry.expect("entry").seq).collect();
727
728 assert_eq!(plan.from_seq, 3);
729 assert_eq!(seqs, vec![3]);
730 }
731
732 #[rstest]
733 fn scan_snapshot_replay_tail_is_empty_when_anchor_matches_watermark() {
734 let mut backend = MemoryBackend::new();
735 backend.open_run(manifest("run-anchor")).expect("open run");
736 backend
737 .append_batch(&[
738 append_with(1, 101, Vec::new()),
739 append_with(2, 102, Vec::new()),
740 ])
741 .expect("append");
742 backend
743 .record_snapshot_anchor(SnapshotAnchor::new(
744 2,
745 "cache://snapshots/run-anchor/2",
746 "blake3:abc",
747 ))
748 .expect("record anchor");
749 let reader = EventStoreReader::new(backend);
750
751 let (plan, scan) = reader
752 .scan_snapshot_replay_tail()
753 .expect("snapshot replay tail");
754 let seqs: Vec<_> = scan.map(|entry| entry.expect("entry").seq).collect();
755
756 assert_eq!(plan.from_seq, 3);
757 assert_eq!(plan.to_seq, 2);
758 assert!(plan.is_empty());
759 assert!(seqs.is_empty());
760 }
761
762 #[rstest]
763 fn scan_seq_returns_committed_entry(reader_with_three: EventStoreReader<MemoryBackend>) {
764 let entry = reader_with_three
765 .scan_seq(2)
766 .expect("scan")
767 .expect("present");
768
769 assert_eq!(entry.seq, 2);
770 assert_eq!(entry.ts_init, UnixNanos::from(102));
771 }
772
773 #[rstest]
774 fn scan_seq_returns_none_outside_watermark(reader_with_three: EventStoreReader<MemoryBackend>) {
775 assert!(reader_with_three.scan_seq(0).expect("scan").is_none());
776 assert!(reader_with_three.scan_seq(99).expect("scan").is_none());
777 }
778
779 #[rstest]
780 fn lookup_finds_recorded_index_key() {
781 let mut backend = MemoryBackend::new();
782 backend.open_run(manifest("run-lookup")).expect("open run");
783 backend
784 .append_batch(&[
785 AppendEntry::new(
786 build_entry(1, 100),
787 vec![IndexKey::new(IndexKind::ClientOrderId, "O-1".to_string())],
788 ),
789 AppendEntry::new(
790 build_entry(2, 101),
791 vec![IndexKey::new(IndexKind::VenueOrderId, "V-1".to_string())],
792 ),
793 ])
794 .expect("append");
795 let reader = EventStoreReader::new(backend);
796
797 assert_eq!(
798 reader
799 .lookup(IndexKind::ClientOrderId, "O-1")
800 .expect("lookup"),
801 Some(1),
802 );
803 assert_eq!(
804 reader
805 .lookup(IndexKind::VenueOrderId, "V-1")
806 .expect("lookup"),
807 Some(2),
808 );
809 assert!(
810 reader
811 .lookup(IndexKind::ClientOrderId, "missing")
812 .expect("lookup")
813 .is_none(),
814 );
815 }
816
817 #[rstest]
818 fn scan_range_forward_yields_entries_in_order(
819 reader_with_three: EventStoreReader<MemoryBackend>,
820 ) {
821 let seqs: Vec<u64> = reader_with_three
822 .scan_range(1, 3, ScanDirection::Forward)
823 .map(|r| r.expect("entry").seq)
824 .collect();
825
826 assert_eq!(seqs, vec![1, 2, 3]);
827 }
828
829 #[rstest]
830 fn scan_range_reverse_yields_entries_in_reverse(
831 reader_with_three: EventStoreReader<MemoryBackend>,
832 ) {
833 let seqs: Vec<u64> = reader_with_three
834 .scan_range(1, 3, ScanDirection::Reverse)
835 .map(|r| r.expect("entry").seq)
836 .collect();
837
838 assert_eq!(seqs, vec![3, 2, 1]);
839 }
840
841 #[rstest]
842 fn scan_range_window_clips_to_request() {
843 let reader = populated(10);
844
845 let seqs: Vec<u64> = reader
846 .scan_range(4, 7, ScanDirection::Forward)
847 .map(|r| r.expect("entry").seq)
848 .collect();
849
850 assert_eq!(seqs, vec![4, 5, 6, 7]);
851 }
852
853 #[rstest]
854 fn scan_range_chunked_forward_walks_full_range() {
855 let reader = populated(7);
859
860 let seqs: Vec<u64> = reader
861 .scan_range_chunked(1, 7, ScanDirection::Forward, 2)
862 .map(|r| r.expect("entry").seq)
863 .collect();
864
865 assert_eq!(seqs, vec![1, 2, 3, 4, 5, 6, 7]);
866 }
867
868 #[rstest]
869 fn forward_chunked_scan_includes_entries_committed_between_chunks() {
870 let mut inner = MemoryBackend::new();
874 inner.open_run(manifest("run-growing")).expect("open run");
875 inner
876 .append_batch(&[
877 append_with(1, 101, Vec::new()),
878 append_with(2, 102, Vec::new()),
879 ])
880 .expect("seed");
881 let reader = EventStoreReader::new(GrowingBackend {
882 inner: std::cell::RefCell::new(inner),
883 grown: std::cell::Cell::new(false),
884 });
885
886 let seqs: Vec<u64> = reader
887 .scan_range_chunked(1, u64::MAX, ScanDirection::Forward, 10)
888 .map(|r| r.expect("entry").seq)
889 .collect();
890
891 assert_eq!(seqs, vec![1, 2, 3, 4]);
892 }
893
894 #[rstest]
895 fn scan_range_chunked_reverse_walks_full_range() {
896 let reader = populated(7);
899
900 let seqs: Vec<u64> = reader
901 .scan_range_chunked(1, 7, ScanDirection::Reverse, 2)
902 .map(|r| r.expect("entry").seq)
903 .collect();
904
905 assert_eq!(seqs, vec![7, 6, 5, 4, 3, 2, 1]);
906 }
907
908 #[rstest]
909 fn scan_range_clips_to_high_watermark() {
910 let reader = populated(3);
914
915 let seqs: Vec<u64> = reader
916 .scan_range_chunked(1, 99, ScanDirection::Forward, 2)
917 .map(|r| r.expect("entry").seq)
918 .collect();
919
920 assert_eq!(seqs, vec![1, 2, 3]);
921 }
922
923 #[rstest]
924 fn scan_range_reverse_clips_to_high_watermark() {
925 let reader = populated(3);
931
932 let seqs: Vec<u64> = reader
933 .scan_range_chunked(1, 99, ScanDirection::Reverse, 2)
934 .map(|r| r.expect("entry").seq)
935 .collect();
936
937 assert_eq!(seqs, vec![3, 2, 1]);
938 }
939
940 #[rstest]
941 fn scan_range_reverse_with_to_at_u64_max() {
942 let reader = populated(3);
946
947 let seqs: Vec<u64> = reader
948 .scan_range_chunked(1, u64::MAX, ScanDirection::Reverse, 1)
949 .map(|r| r.expect("entry").seq)
950 .collect();
951
952 assert_eq!(seqs, vec![3, 2, 1]);
953 }
954
955 #[rstest]
956 fn scan_range_reverse_above_watermark_yields_nothing() {
957 let reader = populated(3);
960
961 let seqs: Vec<u64> = reader
962 .scan_range(10, 20, ScanDirection::Reverse)
963 .map(|r| r.expect("entry").seq)
964 .collect();
965
966 assert!(seqs.is_empty(), "seqs was: {seqs:?}");
967 }
968
969 #[rstest]
970 #[case::inverted(5, 1, ScanDirection::Forward)]
971 #[case::zero_from(0, 5, ScanDirection::Forward)]
972 #[case::inverted_reverse(5, 1, ScanDirection::Reverse)]
973 fn scan_range_empty_bounds_yield_no_entries(
974 #[case] from: u64,
975 #[case] to: u64,
976 #[case] direction: ScanDirection,
977 ) {
978 let reader = populated(3);
979
980 let seqs: Vec<u64> = reader
981 .scan_range(from, to, direction)
982 .map(|r| r.expect("entry").seq)
983 .collect();
984
985 assert!(seqs.is_empty(), "seqs was: {seqs:?}");
986 }
987
988 #[rstest]
989 fn scan_range_propagates_hash_mismatch_error() {
990 let mut backend = MemoryBackend::new();
994 backend.open_run(manifest("run-tamper")).expect("open run");
995 let mut tampered = build_entry(1, 100);
996 tampered.payload = Bytes::from_static(b"\xFF\xFF");
997 backend
998 .append_batch(&[AppendEntry::without_indices(tampered)])
999 .expect("append");
1000 let reader = EventStoreReader::new(backend);
1001
1002 let mut iter = reader.scan_range(1, 1, ScanDirection::Forward);
1003 let first = iter.next().expect("first item");
1004
1005 match first {
1006 Err(EventStoreError::HashMismatch { seq: 1 }) => {}
1007 other => panic!("expected HashMismatch, was {other:?}"),
1008 }
1009 assert!(
1010 iter.next().is_none(),
1011 "iterator must terminate after surfacing the error",
1012 );
1013 }
1014
1015 #[rstest]
1016 fn scan_range_chunk_size_zero_normalizes_to_one() {
1017 let reader = populated(3);
1020
1021 let seqs: Vec<u64> = reader
1022 .scan_range_chunked(1, 3, ScanDirection::Forward, 0)
1023 .map(|r| r.expect("entry").seq)
1024 .collect();
1025
1026 assert_eq!(seqs, vec![1, 2, 3]);
1027 }
1028
1029 #[rstest]
1030 fn into_inner_returns_backend(reader_with_three: EventStoreReader<MemoryBackend>) {
1031 let backend = reader_with_three.into_inner();
1032
1033 assert_eq!(backend.high_watermark().expect("hwm"), 3);
1034 }
1035
1036 #[rstest]
1037 fn lookup_uses_distinct_kinds() {
1038 let mut backend = MemoryBackend::new();
1041 backend.open_run(manifest("run-kinds")).expect("open run");
1042 let shared_key = "shared-key".to_string();
1043 backend
1044 .append_batch(&[AppendEntry::new(
1045 build_entry(1, 100),
1046 vec![IndexKey::new(IndexKind::ClientOrderId, shared_key.clone())],
1047 )])
1048 .expect("append");
1049 let reader = EventStoreReader::new(backend);
1050
1051 assert_eq!(
1052 reader
1053 .lookup(IndexKind::ClientOrderId, &shared_key)
1054 .expect("lookup"),
1055 Some(1),
1056 );
1057 assert!(
1058 reader
1059 .lookup(IndexKind::VenueOrderId, &shared_key)
1060 .expect("lookup")
1061 .is_none(),
1062 );
1063 }
1064}