@@ -430,13 +430,15 @@ pub struct IndexRecord {
430430 pub data_offset : u64 ,
431431}
432432
433- /// mmap-backed reader for a single part. Cheap to construct (three
434- /// mmaps + one header parse), safe to share across threads via `Arc`.
433+ /// mmap-backed reader with a sorted output/window index built once on open.
434+ /// Safe to share across threads via `Arc`.
435435pub struct PartReader {
436436 pub meta : PartMeta ,
437437 pub part_dir : PathBuf ,
438438 data_mmap : Arc < Mmap > ,
439439 index_mmap : Arc < Mmap > ,
440+ // Older parts preserve flush order, so keep a separate sorted lookup.
441+ window_index : Vec < usize > ,
440442}
441443
442444impl std:: fmt:: Debug for PartReader {
@@ -479,12 +481,25 @@ impl PartReader {
479481 ) ) ) ;
480482 }
481483
482- Ok ( Self {
484+ let mut reader = Self {
483485 meta,
484486 part_dir : part_dir. to_path_buf ( ) ,
485487 data_mmap : Arc :: new ( data_mmap) ,
486488 index_mmap : Arc :: new ( index_mmap) ,
487- } )
489+ window_index : Vec :: new ( ) ,
490+ } ;
491+ let mut positions: Vec < _ > = ( 0 ..reader. meta . num_entries as usize ) . collect ( ) ;
492+ positions. sort_unstable_by_key ( |& position| {
493+ let record = reader. index_record ( position) ;
494+ (
495+ record. agg_id ,
496+ record. start_ts ,
497+ record. end_ts ,
498+ record. data_offset ,
499+ )
500+ } ) ;
501+ reader. window_index = positions;
502+ Ok ( reader)
488503 }
489504
490505 /// Read and verify just the meta.bin header (cheap — 64 bytes).
@@ -537,28 +552,51 @@ impl PartReader {
537552 } )
538553 }
539554
540- /// Return all index records. Small (32 B × num_entries) — cheap to
541- /// materialize.
542- pub fn index_records ( & self ) -> Vec < IndexRecord > {
543- let n = self . meta . num_entries as usize ;
544- let mut out = Vec :: with_capacity ( n) ;
545- for i in 0 ..n {
546- let off = i * INDEX_ENTRY_SIZE ;
547- let agg_id = u64:: from_le_bytes ( self . index_mmap [ off..off + 8 ] . try_into ( ) . unwrap ( ) ) ;
548- let start_ts =
549- u64:: from_le_bytes ( self . index_mmap [ off + 8 ..off + 16 ] . try_into ( ) . unwrap ( ) ) ;
550- let end_ts =
551- u64:: from_le_bytes ( self . index_mmap [ off + 16 ..off + 24 ] . try_into ( ) . unwrap ( ) ) ;
552- let data_offset =
553- u64:: from_le_bytes ( self . index_mmap [ off + 24 ..off + 32 ] . try_into ( ) . unwrap ( ) ) ;
554- out. push ( IndexRecord {
555- agg_id,
556- start_ts,
557- end_ts,
558- data_offset,
559- } ) ;
555+ fn index_record ( & self , position : usize ) -> IndexRecord {
556+ let off = position * INDEX_ENTRY_SIZE ;
557+ let read =
558+ |offset| u64:: from_le_bytes ( self . index_mmap [ offset..offset + 8 ] . try_into ( ) . unwrap ( ) ) ;
559+ IndexRecord {
560+ agg_id : read ( off) ,
561+ start_ts : read ( off + 8 ) ,
562+ end_ts : read ( off + 16 ) ,
563+ data_offset : read ( off + 24 ) ,
560564 }
561- out
565+ }
566+
567+ /// Preserve disk order for callers that need to inspect the entire part.
568+ pub fn index_records ( & self ) -> Vec < IndexRecord > {
569+ ( 0 ..self . meta . num_entries as usize )
570+ . map ( |position| self . index_record ( position) )
571+ . collect ( )
572+ }
573+
574+ /// Find records contained in the requested window under one stored-output
575+ /// prefix. Duplicate windows remain visible so callers can reject ambiguity.
576+ pub fn window_records (
577+ & self ,
578+ agg_id : u64 ,
579+ start_ts : u64 ,
580+ end_ts : u64 ,
581+ ) -> impl Iterator < Item = IndexRecord > + ' _ {
582+ let first = self . window_index . partition_point ( |& position| {
583+ let record = self . index_record ( position) ;
584+ ( record. agg_id , record. start_ts ) < ( agg_id, start_ts)
585+ } ) ;
586+ self . window_index [ first..]
587+ . iter ( )
588+ . map ( |& position| self . index_record ( position) )
589+ . take_while ( move |record| record. agg_id == agg_id && record. start_ts <= end_ts)
590+ . filter ( move |record| record. end_ts <= end_ts)
591+ }
592+
593+ pub fn resident_bytes ( & self ) -> u64 {
594+ ( self . data_mmap . len ( ) as u64 )
595+ . saturating_add ( self . index_mmap . len ( ) as u64 )
596+ . saturating_add (
597+ ( self . window_index . capacity ( ) as u64 )
598+ . saturating_mul ( std:: mem:: size_of :: < usize > ( ) as u64 ) ,
599+ )
562600 }
563601
564602 /// Resolve a single index record into a [`SnapshotEntry`] by reading
@@ -658,6 +696,40 @@ mod tests {
658696 }
659697 }
660698
699+ // Old parts can be unsorted; lookup must isolate the prefix/range while
700+ // preserving duplicate records for the immutable reader's ambiguity check.
701+ #[ test]
702+ fn window_lookup_handles_unsorted_parts_duplicates_and_recovery ( ) {
703+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
704+ let part_dir = tmp. path ( ) . join ( "lookup" ) ;
705+ let mut snapshots = Vec :: new ( ) ;
706+ for id in [ 99 , 42 , 1 ] {
707+ let mut snapshot = make_snapshot ( ) ;
708+ snapshot. agg_id = id;
709+ snapshot. entries . reverse ( ) ;
710+ if id == 42 {
711+ snapshot. entries . push ( snapshot. entries [ 1 ] . clone ( ) ) ;
712+ }
713+ snapshots. push ( snapshot) ;
714+ }
715+ PartWriter :: write_part ( & part_dir, 1 , & snapshots) . unwrap ( ) ;
716+ for _ in 0 ..2 {
717+ let reader = PartReader :: open ( & part_dir) . unwrap ( ) ;
718+ let records = reader. window_records ( 42 , 1_000 , 1_500 ) . collect :: < Vec < _ > > ( ) ;
719+ assert_eq ! ( records. len( ) , 2 ) ;
720+ assert ! ( records. iter( ) . all( |record| record. agg_id == 42
721+ && record. start_ts == 1_000
722+ && record. end_ts == 1_500 ) ) ;
723+ assert_ne ! ( records[ 0 ] . data_offset, records[ 1 ] . data_offset) ;
724+ assert_eq ! ( reader. window_records( 42 , 1_500 , 2_000 ) . count( ) , 1 ) ;
725+ assert_eq ! ( reader. window_records( 43 , 0 , u64 :: MAX ) . count( ) , 0 ) ;
726+ assert_eq ! ( reader. window_records( 42 , 1_001 , 1_999 ) . count( ) , 0 ) ;
727+ assert_eq ! ( reader. window_records( 42 , 2_000 , 1_000 ) . count( ) , 0 ) ;
728+ assert_eq ! ( reader. index_records( ) [ 0 ] . agg_id, 99 ) ;
729+ assert ! ( reader. resident_bytes( ) > reader. meta. data_len + reader. meta. index_len) ;
730+ }
731+ }
732+
661733 #[ test]
662734 fn part_round_trip_writes_and_reads_back ( ) {
663735 let tmp = TempDir :: new ( ) . unwrap ( ) ;
0 commit comments