Skip to main content

mito2/read/
last_row.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utilities to read the last row of each time series.
16
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use datatypes::arrow::array::{Array, BinaryArray};
21use datatypes::arrow::compute::concat_batches;
22use datatypes::arrow::record_batch::RecordBatch;
23use futures::{Stream, TryStreamExt};
24use snafu::ResultExt;
25use store_api::storage::{FileId, TimeSeriesRowSelector};
26
27use crate::cache::{
28    CacheStrategy, SelectorResult, SelectorResultKey, SelectorResultValue,
29    selector_result_cache_hit, selector_result_cache_miss,
30};
31use crate::error::{ComputeArrowSnafu, Result};
32use crate::read::read_columns::JsonTargetTypes;
33use crate::read::{
34    Batch, BatchReader, BoxedBatchReader, BoxedRecordBatchStream, timestamp_array_to_i64_slice,
35};
36use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
37use crate::sst::parquet::flat_format::{primary_key_column_index, time_index_column_index};
38use crate::sst::parquet::format::{PrimaryKeyArray, primary_key_offsets};
39use crate::sst::parquet::read_columns::ParquetReadColumns;
40use crate::sst::parquet::reader::FlatRowGroupReader;
41
42/// Reader to keep the last row for each time series.
43/// It assumes that batches from the input reader are
44/// - sorted
45/// - all deleted rows has been filtered.
46/// - not empty
47///
48/// This reader is different from the [MergeMode](crate::region::options::MergeMode) as
49/// it focus on time series (the same key).
50#[allow(dead_code)]
51pub(crate) struct LastRowReader {
52    /// Inner reader.
53    reader: BoxedBatchReader,
54    /// The last batch pending to return.
55    selector: LastRowSelector,
56}
57
58#[allow(dead_code)]
59impl LastRowReader {
60    /// Creates a new `LastRowReader`.
61    pub(crate) fn new(reader: BoxedBatchReader) -> Self {
62        Self {
63            reader,
64            selector: LastRowSelector::default(),
65        }
66    }
67
68    /// Returns the last row of the next key.
69    pub(crate) async fn next_last_row(&mut self) -> Result<Option<Batch>> {
70        while let Some(batch) = self.reader.next_batch().await? {
71            if let Some(yielded) = self.selector.on_next(batch) {
72                return Ok(Some(yielded));
73            }
74        }
75        Ok(self.selector.finish())
76    }
77}
78
79#[async_trait]
80impl BatchReader for LastRowReader {
81    async fn next_batch(&mut self) -> Result<Option<Batch>> {
82        self.next_last_row().await
83    }
84}
85
86/// Common struct that selects only the last row of each time series.
87#[derive(Default)]
88pub struct LastRowSelector {
89    last_batch: Option<Batch>,
90}
91
92impl LastRowSelector {
93    /// Handles next batch. Return the yielding batch if present.
94    pub fn on_next(&mut self, batch: Batch) -> Option<Batch> {
95        if let Some(last) = &self.last_batch {
96            if last.primary_key() == batch.primary_key() {
97                // Same key, update last batch.
98                self.last_batch = Some(batch);
99                None
100            } else {
101                // Different key, return the last row in `last` and update `last_batch` by
102                // current batch.
103                debug_assert!(!last.is_empty());
104                let last_row = last.slice(last.num_rows() - 1, 1);
105                self.last_batch = Some(batch);
106                Some(last_row)
107            }
108        } else {
109            self.last_batch = Some(batch);
110            None
111        }
112    }
113
114    /// Finishes the selector and returns the pending batch if any.
115    pub fn finish(&mut self) -> Option<Batch> {
116        if let Some(last) = self.last_batch.take() {
117            // This is the last key.
118            let last_row = last.slice(last.num_rows() - 1, 1);
119            return Some(last_row);
120        }
121        None
122    }
123}
124
125/// Cached last row reader for flat format row group.
126/// If the last rows are already cached (as flat `RecordBatch`), returns cached values.
127/// Otherwise, reads from the row group, selects last rows, and updates the cache.
128pub(crate) enum FlatRowGroupLastRowCachedReader {
129    /// Cache hit, reads last rows from cached value.
130    Hit(FlatLastRowCacheReader),
131    /// Cache miss, reads from row group reader and updates cache.
132    Miss(FlatRowGroupLastRowReader),
133}
134
135impl FlatRowGroupLastRowCachedReader {
136    pub(crate) fn new(
137        file_id: FileId,
138        row_group_idx: usize,
139        cache_strategy: CacheStrategy,
140        read_cols: &ParquetReadColumns,
141        json_target_types: JsonTargetTypes,
142        reader: FlatRowGroupReader,
143    ) -> Self {
144        let key = SelectorResultKey {
145            file_id,
146            row_group_idx,
147            selector: TimeSeriesRowSelector::LastRow,
148        };
149
150        if let Some(value) = cache_strategy.get_selector_result(&key) {
151            let is_flat = matches!(&value.result, SelectorResult::Flat(_));
152            let schema_matches = value.read_cols == *read_cols;
153            let json_target_types_matches = value.json_target_types == json_target_types;
154            if is_flat && schema_matches && json_target_types_matches {
155                Self::new_hit(value)
156            } else {
157                Self::new_miss(key, read_cols, json_target_types, reader, cache_strategy)
158            }
159        } else {
160            Self::new_miss(key, read_cols, json_target_types, reader, cache_strategy)
161        }
162    }
163
164    /// Returns the next RecordBatch.
165    pub(crate) async fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
166        match self {
167            FlatRowGroupLastRowCachedReader::Hit(r) => r.next_batch(),
168            FlatRowGroupLastRowCachedReader::Miss(r) => r.next_batch().await,
169        }
170    }
171
172    fn new_hit(value: Arc<SelectorResultValue>) -> Self {
173        selector_result_cache_hit();
174        Self::Hit(FlatLastRowCacheReader { value, idx: 0 })
175    }
176
177    fn new_miss(
178        key: SelectorResultKey,
179        read_cols: &ParquetReadColumns,
180        json_target_types: JsonTargetTypes,
181        reader: FlatRowGroupReader,
182        cache_strategy: CacheStrategy,
183    ) -> Self {
184        selector_result_cache_miss();
185        Self::Miss(FlatRowGroupLastRowReader::new(
186            key,
187            read_cols.clone(),
188            json_target_types,
189            reader,
190            cache_strategy,
191        ))
192    }
193}
194
195/// Iterates over cached flat last rows.
196pub(crate) struct FlatLastRowCacheReader {
197    value: Arc<SelectorResultValue>,
198    idx: usize,
199}
200
201impl FlatLastRowCacheReader {
202    fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
203        let batches = match &self.value.result {
204            SelectorResult::Flat(batches) => batches,
205            SelectorResult::PrimaryKey(_) => unreachable!(),
206        };
207        if self.idx < batches.len() {
208            let res = Ok(Some(batches[self.idx].clone()));
209            self.idx += 1;
210            res
211        } else {
212            Ok(None)
213        }
214    }
215}
216
217/// Buffer that accumulates small `RecordBatch`es and tracks total row count.
218pub(crate) struct BatchBuffer {
219    batches: Vec<RecordBatch>,
220    num_rows: usize,
221}
222
223impl BatchBuffer {
224    fn new() -> Self {
225        Self {
226            batches: Vec::new(),
227            num_rows: 0,
228        }
229    }
230
231    /// Returns true if total buffered rows reaches `DEFAULT_READ_BATCH_SIZE`.
232    fn is_full(&self) -> bool {
233        self.num_rows >= DEFAULT_READ_BATCH_SIZE
234    }
235
236    /// Extends the buffer from a slice of batches.
237    fn extend_from_slice(&mut self, batches: &[RecordBatch]) {
238        for batch in batches {
239            self.num_rows += batch.num_rows();
240        }
241        self.batches.extend_from_slice(batches);
242    }
243
244    /// Returns true if the buffer has no batches.
245    fn is_empty(&self) -> bool {
246        self.batches.is_empty()
247    }
248
249    /// Concatenates all buffered batches into one, resets the buffer, and returns the result.
250    fn concat(&mut self) -> Result<RecordBatch> {
251        debug_assert!(!self.batches.is_empty());
252        let schema = self.batches[0].schema();
253        let merged = concat_batches(&schema, &self.batches).context(ComputeArrowSnafu)?;
254        self.batches.clear();
255        self.num_rows = 0;
256        Ok(merged)
257    }
258}
259
260/// Reads last rows from a flat format row group and caches the results.
261pub(crate) struct FlatRowGroupLastRowReader {
262    key: SelectorResultKey,
263    reader: FlatRowGroupReader,
264    selector: FlatLastTimestampSelector,
265    yielded_batches: Vec<RecordBatch>,
266    cache_strategy: CacheStrategy,
267    read_cols: ParquetReadColumns,
268    json_target_types: JsonTargetTypes,
269    /// Accumulates small selector-output batches before concatenating.
270    pending: BatchBuffer,
271}
272
273impl FlatRowGroupLastRowReader {
274    fn new(
275        key: SelectorResultKey,
276        read_cols: ParquetReadColumns,
277        json_target_types: JsonTargetTypes,
278        reader: FlatRowGroupReader,
279        cache_strategy: CacheStrategy,
280    ) -> Self {
281        Self {
282            key,
283            reader,
284            selector: FlatLastTimestampSelector::default(),
285            yielded_batches: vec![],
286            cache_strategy,
287            read_cols,
288            json_target_types,
289            pending: BatchBuffer::new(),
290        }
291    }
292
293    /// Concatenates pending batches and records the result in `yielded_batches`.
294    fn flush_pending(&mut self) -> Result<Option<RecordBatch>> {
295        if self.pending.is_empty() {
296            return Ok(None);
297        }
298        let merged = self.pending.concat()?;
299        self.yielded_batches.push(merged.clone());
300        Ok(Some(merged))
301    }
302
303    async fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
304        if self.pending.is_full() {
305            return self.flush_pending();
306        }
307
308        while let Some(batch) = self.reader.next_batch().await? {
309            self.selector.on_next(batch, &mut self.pending)?;
310            if self.pending.is_full() {
311                return self.flush_pending();
312            }
313        }
314
315        // Reader exhausted — flush remaining selector state.
316        self.selector.finish(&mut self.pending)?;
317        if !self.pending.is_empty() {
318            let result = self.flush_pending();
319            // All last rows in row group are yielded, update cache.
320            self.maybe_update_cache();
321            return result;
322        }
323
324        // All last rows in row group are yielded, update cache.
325        self.maybe_update_cache();
326        Ok(None)
327    }
328
329    fn maybe_update_cache(&mut self) {
330        if self.yielded_batches.is_empty() {
331            return;
332        }
333        let batches = std::mem::take(&mut self.yielded_batches);
334        let value = Arc::new(SelectorResultValue::new_flat(
335            batches,
336            self.read_cols.clone(),
337            self.json_target_types.clone(),
338        ));
339        self.cache_strategy.put_selector_result(self.key, value);
340    }
341}
342
343/// Selects the last-timestamp rows per primary key from flat `RecordBatch`.
344///
345/// Assumes that input batches are sorted by primary key then by timestamp,
346/// and contain only PUT operations (no DELETE).
347#[derive(Default)]
348pub(crate) struct FlatLastTimestampSelector {
349    /// State for the currently in-progress primary key.
350    current_key: Option<LastKeyState>,
351}
352
353#[derive(Debug)]
354struct LastKeyState {
355    key: Vec<u8>,
356    last_timestamp: i64,
357    slices: Vec<RecordBatch>,
358}
359
360impl LastKeyState {
361    fn new(key: Vec<u8>, last_timestamp: i64, first_slice: RecordBatch) -> Self {
362        Self {
363            key,
364            last_timestamp,
365            slices: vec![first_slice],
366        }
367    }
368}
369
370impl FlatLastTimestampSelector {
371    /// Processes the next batch and appends completed-key results into `output_buffer`.
372    pub(crate) fn on_next(
373        &mut self,
374        batch: RecordBatch,
375        output_buffer: &mut BatchBuffer,
376    ) -> Result<()> {
377        if batch.num_rows() == 0 {
378            return Ok(());
379        }
380
381        let num_columns = batch.num_columns();
382        let pk_col_idx = primary_key_column_index(num_columns);
383        let ts_col_idx = time_index_column_index(num_columns);
384
385        let pk_array = batch
386            .column(pk_col_idx)
387            .as_any()
388            .downcast_ref::<PrimaryKeyArray>()
389            .unwrap();
390        let offsets = primary_key_offsets(pk_array)?;
391        if offsets.is_empty() {
392            return Ok(());
393        }
394
395        let ts_values = timestamp_array_to_i64_slice(batch.column(ts_col_idx));
396        for i in 0..offsets.len() - 1 {
397            let range_start = offsets[i];
398            let range_end = offsets[i + 1];
399            let range_key = primary_key_bytes_at(&batch, pk_col_idx, range_start);
400            let range_last_ts = ts_values[range_end - 1];
401            let range_last_ts_start = last_timestamp_start(ts_values, range_start, range_end);
402            let range_slice = batch.slice(range_last_ts_start, range_end - range_last_ts_start);
403
404            match self.current_key.as_mut() {
405                Some(state) if state.key.as_slice() == range_key => {
406                    if range_last_ts > state.last_timestamp {
407                        state.last_timestamp = range_last_ts;
408                        state.slices.clear();
409                        state.slices.push(range_slice);
410                    } else if range_last_ts == state.last_timestamp {
411                        state.slices.push(range_slice);
412                    }
413                }
414                Some(_) => {
415                    self.flush_current_key(output_buffer);
416                    self.current_key = Some(LastKeyState::new(
417                        range_key.to_vec(),
418                        range_last_ts,
419                        range_slice,
420                    ));
421                }
422                None => {
423                    self.current_key = Some(LastKeyState::new(
424                        range_key.to_vec(),
425                        range_last_ts,
426                        range_slice,
427                    ));
428                }
429            }
430        }
431
432        Ok(())
433    }
434
435    /// Finishes the selector and appends remaining results into `output_buffer`.
436    pub(crate) fn finish(&mut self, output_buffer: &mut BatchBuffer) -> Result<()> {
437        self.flush_current_key(output_buffer);
438        Ok(())
439    }
440
441    fn flush_current_key(&mut self, output_buffer: &mut BatchBuffer) {
442        let Some(state) = self.current_key.take() else {
443            return;
444        };
445        output_buffer.extend_from_slice(&state.slices);
446    }
447}
448
449/// Reader that keeps only the last row of each time series from a flat RecordBatch stream.
450/// Assumes input is sorted, deduped, and contains no delete operations.
451pub(crate) struct FlatLastRowReader {
452    stream: BoxedRecordBatchStream,
453    selector: FlatLastTimestampSelector,
454    pending: BatchBuffer,
455}
456
457impl FlatLastRowReader {
458    /// Creates a new `FlatLastRowReader`.
459    pub(crate) fn new(stream: BoxedRecordBatchStream) -> Self {
460        Self {
461            stream,
462            selector: FlatLastTimestampSelector::default(),
463            pending: BatchBuffer::new(),
464        }
465    }
466
467    /// Converts the reader into a stream of RecordBatches.
468    pub(crate) fn into_stream(mut self) -> impl Stream<Item = Result<RecordBatch>> {
469        async_stream::try_stream! {
470            while let Some(batch) = self.stream.try_next().await? {
471                self.selector.on_next(batch, &mut self.pending)?;
472                if self.pending.is_full() {
473                    yield self.pending.concat()?;
474                }
475            }
476            self.selector.finish(&mut self.pending)?;
477            if !self.pending.is_empty() {
478                yield self.pending.concat()?;
479            }
480        }
481    }
482}
483
484/// Gets the primary key bytes at `index` from the primary key dictionary column.
485fn primary_key_bytes_at(batch: &RecordBatch, pk_col_idx: usize, index: usize) -> &[u8] {
486    let pk_dict = batch
487        .column(pk_col_idx)
488        .as_any()
489        .downcast_ref::<PrimaryKeyArray>()
490        .unwrap();
491    let key = pk_dict.keys().value(index);
492    let binary_values = pk_dict
493        .values()
494        .as_any()
495        .downcast_ref::<BinaryArray>()
496        .unwrap();
497    binary_values.value(key as usize)
498}
499
500/// Finds the start index of rows sharing the last (maximum) timestamp
501/// within the range `[range_start, range_end)`.
502fn last_timestamp_start(ts_values: &[i64], range_start: usize, range_end: usize) -> usize {
503    debug_assert!(range_start < range_end);
504
505    let last_ts = ts_values[range_end - 1];
506    let mut start = range_end - 1;
507    while start > range_start && ts_values[start - 1] == last_ts {
508        start -= 1;
509    }
510    start
511}
512
513#[cfg(test)]
514mod tests {
515    use std::sync::Arc;
516
517    use api::v1::OpType;
518    use datatypes::arrow::array::{
519        ArrayRef, BinaryDictionaryBuilder, Int64Array, TimestampMillisecondArray, UInt8Array,
520        UInt64Array,
521    };
522    use datatypes::arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit, UInt32Type};
523    use datatypes::arrow::record_batch::RecordBatch;
524
525    use super::*;
526    use crate::test_util::{VecBatchReader, check_reader_result, new_batch};
527
528    #[tokio::test]
529    async fn test_last_row_one_batch() {
530        let input = [new_batch(
531            b"k1",
532            &[1, 2],
533            &[11, 11],
534            &[OpType::Put, OpType::Put],
535            &[21, 22],
536        )];
537        let reader = VecBatchReader::new(&input);
538        let mut reader = LastRowReader::new(Box::new(reader));
539        check_reader_result(
540            &mut reader,
541            &[new_batch(b"k1", &[2], &[11], &[OpType::Put], &[22])],
542        )
543        .await;
544
545        // Only one row.
546        let input = [new_batch(b"k1", &[1], &[11], &[OpType::Put], &[21])];
547        let reader = VecBatchReader::new(&input);
548        let mut reader = LastRowReader::new(Box::new(reader));
549        check_reader_result(
550            &mut reader,
551            &[new_batch(b"k1", &[1], &[11], &[OpType::Put], &[21])],
552        )
553        .await;
554    }
555
556    #[tokio::test]
557    async fn test_last_row_multi_batch() {
558        let input = [
559            new_batch(
560                b"k1",
561                &[1, 2],
562                &[11, 11],
563                &[OpType::Put, OpType::Put],
564                &[21, 22],
565            ),
566            new_batch(
567                b"k1",
568                &[3, 4],
569                &[11, 11],
570                &[OpType::Put, OpType::Put],
571                &[23, 24],
572            ),
573            new_batch(
574                b"k2",
575                &[1, 2],
576                &[11, 11],
577                &[OpType::Put, OpType::Put],
578                &[31, 32],
579            ),
580        ];
581        let reader = VecBatchReader::new(&input);
582        let mut reader = LastRowReader::new(Box::new(reader));
583        check_reader_result(
584            &mut reader,
585            &[
586                new_batch(b"k1", &[4], &[11], &[OpType::Put], &[24]),
587                new_batch(b"k2", &[2], &[11], &[OpType::Put], &[32]),
588            ],
589        )
590        .await;
591    }
592
593    /// Helper to build a flat format RecordBatch for testing.
594    fn new_flat_batch(primary_keys: &[&[u8]], timestamps: &[i64], fields: &[i64]) -> RecordBatch {
595        let num_rows = timestamps.len();
596        assert_eq!(primary_keys.len(), num_rows);
597        assert_eq!(fields.len(), num_rows);
598
599        let columns: Vec<ArrayRef> = vec![
600            // field0 column
601            Arc::new(Int64Array::from_iter_values(fields.iter().copied())),
602            // ts column (time index)
603            Arc::new(TimestampMillisecondArray::from_iter_values(
604                timestamps.iter().copied(),
605            )),
606            // __primary_key column (dictionary(uint32, binary))
607            {
608                let mut builder = BinaryDictionaryBuilder::<UInt32Type>::new();
609                for &pk in primary_keys {
610                    builder.append(pk).unwrap();
611                }
612                Arc::new(builder.finish())
613            },
614            // __sequence column
615            Arc::new(UInt64Array::from_iter_values(vec![1u64; num_rows])),
616            // __op_type column
617            Arc::new(UInt8Array::from_iter_values(vec![1u8; num_rows])),
618        ];
619
620        RecordBatch::try_new(test_flat_schema(), columns).unwrap()
621    }
622
623    fn test_flat_schema() -> SchemaRef {
624        let fields = vec![
625            Field::new("field0", DataType::Int64, false),
626            Field::new(
627                "ts",
628                DataType::Timestamp(TimeUnit::Millisecond, None),
629                false,
630            ),
631            Field::new(
632                "__primary_key",
633                DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Binary)),
634                false,
635            ),
636            Field::new("__sequence", DataType::UInt64, false),
637            Field::new("__op_type", DataType::UInt8, false),
638        ];
639        Arc::new(Schema::new(fields))
640    }
641
642    /// Collects all rows from the selector across all result batches.
643    fn collect_flat_results(
644        selector: &mut FlatLastTimestampSelector,
645        batches: Vec<RecordBatch>,
646    ) -> Vec<(Vec<u8>, i64)> {
647        let mut output_buffer = BatchBuffer::new();
648        let mut results = Vec::new();
649        for batch in batches {
650            selector.on_next(batch, &mut output_buffer).unwrap();
651            for r in output_buffer.batches.drain(..) {
652                extract_flat_rows(&r, &mut results);
653            }
654            output_buffer.num_rows = 0;
655        }
656        selector.finish(&mut output_buffer).unwrap();
657        for r in output_buffer.batches.drain(..) {
658            extract_flat_rows(&r, &mut results);
659        }
660        results
661    }
662
663    /// Extracts (primary_key, timestamp) pairs from a result batch.
664    fn extract_flat_rows(batch: &RecordBatch, out: &mut Vec<(Vec<u8>, i64)>) {
665        let ts_col = batch
666            .column(1)
667            .as_any()
668            .downcast_ref::<TimestampMillisecondArray>()
669            .unwrap();
670        let pk_col = batch
671            .column(2)
672            .as_any()
673            .downcast_ref::<PrimaryKeyArray>()
674            .unwrap();
675        let binary_values = pk_col
676            .values()
677            .as_any()
678            .downcast_ref::<BinaryArray>()
679            .unwrap();
680
681        for i in 0..batch.num_rows() {
682            let key_idx = pk_col.keys().value(i);
683            let pk = binary_values.value(key_idx as usize).to_vec();
684            let ts = ts_col.value(i);
685            out.push((pk, ts));
686        }
687    }
688
689    #[test]
690    fn test_flat_single_batch_one_key() {
691        let mut selector = FlatLastTimestampSelector::default();
692        let batch = new_flat_batch(&[b"k1", b"k1", b"k1"], &[1, 2, 3], &[10, 20, 30]);
693        let results = collect_flat_results(&mut selector, vec![batch]);
694        assert_eq!(vec![(b"k1".to_vec(), 3)], results);
695    }
696
697    #[test]
698    fn test_flat_single_batch_multiple_keys() {
699        let mut selector = FlatLastTimestampSelector::default();
700        let batch = new_flat_batch(
701            &[b"k1", b"k1", b"k2", b"k2", b"k3"],
702            &[1, 2, 3, 4, 5],
703            &[10, 20, 30, 40, 50],
704        );
705        let results = collect_flat_results(&mut selector, vec![batch]);
706        assert_eq!(
707            vec![
708                (b"k1".to_vec(), 2),
709                (b"k2".to_vec(), 4),
710                (b"k3".to_vec(), 5),
711            ],
712            results
713        );
714    }
715
716    #[test]
717    fn test_flat_key_spans_batches() {
718        let mut selector = FlatLastTimestampSelector::default();
719        let batches = vec![
720            new_flat_batch(&[b"k1", b"k1"], &[1, 2], &[10, 20]),
721            new_flat_batch(&[b"k1", b"k2"], &[3, 4], &[30, 40]),
722            new_flat_batch(&[b"k2", b"k3"], &[5, 6], &[50, 60]),
723        ];
724        let results = collect_flat_results(&mut selector, batches);
725        assert_eq!(
726            vec![
727                (b"k1".to_vec(), 3),
728                (b"k2".to_vec(), 5),
729                (b"k3".to_vec(), 6),
730            ],
731            results
732        );
733    }
734
735    #[test]
736    fn test_flat_duplicate_last_timestamps() {
737        let mut selector = FlatLastTimestampSelector::default();
738        // k1 has two rows with the same last timestamp (3).
739        let batch = new_flat_batch(
740            &[b"k1", b"k1", b"k1", b"k2"],
741            &[1, 3, 3, 5],
742            &[10, 20, 30, 40],
743        );
744        let results = collect_flat_results(&mut selector, vec![batch]);
745        assert_eq!(
746            vec![
747                (b"k1".to_vec(), 3),
748                (b"k1".to_vec(), 3),
749                (b"k2".to_vec(), 5),
750            ],
751            results
752        );
753    }
754
755    #[test]
756    fn test_flat_duplicate_last_timestamps_across_batches() {
757        let mut selector = FlatLastTimestampSelector::default();
758        // k1's last timestamp (3) spans two batches.
759        let batches = vec![
760            new_flat_batch(&[b"k1", b"k1"], &[1, 3], &[10, 20]),
761            new_flat_batch(&[b"k1", b"k2"], &[3, 5], &[30, 40]),
762        ];
763        let results = collect_flat_results(&mut selector, batches);
764        assert_eq!(
765            vec![
766                (b"k1".to_vec(), 3),
767                (b"k1".to_vec(), 3),
768                (b"k2".to_vec(), 5),
769            ],
770            results
771        );
772    }
773
774    #[test]
775    fn test_flat_pending_chain_dropped_by_higher_timestamp() {
776        let mut selector = FlatLastTimestampSelector::default();
777        let batches = vec![
778            new_flat_batch(&[b"k1", b"k1"], &[1, 3], &[10, 20]),
779            new_flat_batch(&[b"k1", b"k1"], &[3, 3], &[21, 22]),
780            new_flat_batch(&[b"k1", b"k1"], &[4, 4], &[23, 24]),
781        ];
782        let results = collect_flat_results(&mut selector, batches);
783        assert_eq!(vec![(b"k1".to_vec(), 4), (b"k1".to_vec(), 4)], results);
784    }
785
786    #[test]
787    fn test_flat_finish_is_one_shot() {
788        let mut selector = FlatLastTimestampSelector::default();
789        let batch = new_flat_batch(&[b"k1", b"k1", b"k2"], &[1, 2, 3], &[10, 20, 30]);
790        let mut output_buffer = BatchBuffer::new();
791
792        // Feed one batch: completed keys can be emitted before EOF.
793        selector.on_next(batch, &mut output_buffer).unwrap();
794        let mut pre_finish = Vec::new();
795        for r in output_buffer.batches.drain(..) {
796            extract_flat_rows(&r, &mut pre_finish);
797        }
798        output_buffer.num_rows = 0;
799        assert_eq!(vec![(b"k1".to_vec(), 2)], pre_finish);
800
801        // Simulate EOF by calling finish().
802        selector.finish(&mut output_buffer).unwrap();
803        assert!(!output_buffer.is_empty());
804        output_buffer.batches.clear();
805        output_buffer.num_rows = 0;
806
807        // A second finish after EOF should not yield any more rows.
808        selector.finish(&mut output_buffer).unwrap();
809        assert!(output_buffer.is_empty());
810    }
811}