Skip to main content

lance_table/utils/
stream.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{
5    fmt,
6    sync::{Arc, OnceLock},
7};
8
9use arrow_array::{
10    ArrayRef, BooleanArray, RecordBatch, RecordBatchOptions, UInt64Array, make_array,
11};
12use arrow_buffer::NullBuffer;
13use arrow_schema::{Field, Schema, SchemaRef};
14use futures::{
15    FutureExt, Stream, StreamExt,
16    future::{BoxFuture, Shared},
17    stream::{BoxStream, FuturesOrdered},
18};
19use lance_arrow::RecordBatchExt;
20use lance_core::{
21    Error, ROW_ADDR, ROW_ADDR_FIELD, ROW_CREATED_AT_VERSION_FIELD, ROW_ID, ROW_ID_FIELD,
22    ROW_LAST_UPDATED_AT_VERSION_FIELD, Result,
23    utils::{address::RowAddress, deletion::DeletionVector},
24};
25use lance_io::ReadBatchParams;
26use tracing::instrument;
27
28use crate::rowids::{RowIdSequence, RowIdSequenceCursor};
29
30pub type ReadBatchFut = BoxFuture<'static, Result<RecordBatch>>;
31/// A task, emitted by a file reader, that will produce a batch (of the
32/// given size)
33pub struct ReadBatchTask {
34    pub task: ReadBatchFut,
35    pub num_rows: u32,
36}
37pub type ReadBatchTaskStream = BoxStream<'static, ReadBatchTask>;
38pub type ReadBatchFutStream = BoxStream<'static, ReadBatchFut>;
39
40type SharedReadBatchFut = Shared<BoxFuture<'static, std::result::Result<RecordBatch, Arc<Error>>>>;
41
42#[derive(Debug)]
43struct SharedReadError(Arc<Error>);
44
45impl fmt::Display for SharedReadError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        self.0.fmt(f)
48    }
49}
50
51impl std::error::Error for SharedReadError {
52    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
53        Some(self.0.as_ref())
54    }
55}
56
57struct PendingReadBatch {
58    task: Option<ReadBatchFut>,
59    shared_task: Option<SharedReadBatchFut>,
60    offset: u32,
61    num_rows: u32,
62}
63
64impl PendingReadBatch {
65    fn new(task: ReadBatchTask) -> Self {
66        Self {
67            task: Some(task.task),
68            shared_task: None,
69            offset: 0,
70            num_rows: task.num_rows,
71        }
72    }
73
74    fn take(&mut self, num_rows: u32) -> ReadBatchFut {
75        debug_assert!(num_rows <= self.num_rows);
76
77        if self.offset == 0 && num_rows == self.num_rows && self.shared_task.is_none() {
78            self.num_rows = 0;
79            let Some(task) = self.task.take() else {
80                return async {
81                    Err(Error::internal(
82                        "missing read task while merging aligned streams".to_string(),
83                    ))
84                }
85                .boxed();
86            };
87            return task;
88        }
89
90        let shared_task = self
91            .shared_task
92            .get_or_insert_with(|| {
93                let task = self.task.take();
94                async move {
95                    let Some(task) = task else {
96                        return Err(Arc::new(Error::internal(
97                            "missing read task while splitting a merged stream".to_string(),
98                        )));
99                    };
100                    task.await.map_err(Arc::new)
101                }
102                .boxed()
103                .shared()
104            })
105            .clone();
106        let offset = self.offset;
107        self.offset += num_rows;
108        self.num_rows -= num_rows;
109
110        async move {
111            match shared_task.await {
112                Ok(batch) => Ok(batch.slice(offset as usize, num_rows as usize)),
113                Err(error) => Err(Error::wrapped(Box::new(SharedReadError(error)))),
114            }
115        }
116        .boxed()
117    }
118}
119
120struct MergeStream {
121    streams: Vec<ReadBatchTaskStream>,
122    pending: Vec<Option<PendingReadBatch>>,
123}
124
125impl MergeStream {
126    fn emit(&mut self) -> ReadBatchTask {
127        let num_rows = self
128            .pending
129            .iter()
130            .filter_map(|pending| pending.as_ref().map(|pending| pending.num_rows))
131            .min()
132            .unwrap_or_default();
133        let mut batches = FuturesOrdered::new();
134        for pending in &mut self.pending {
135            let Some(pending_batch) = pending.as_mut() else {
136                continue;
137            };
138            batches.push_back(pending_batch.take(num_rows));
139            if pending_batch.num_rows == 0 {
140                *pending = None;
141            }
142        }
143        let task = async move {
144            let Some(first) = batches.next().await else {
145                return Err(Error::internal(
146                    "cannot merge an empty set of read batches".to_string(),
147                ));
148            };
149            let mut batch = first?;
150            while let Some(next) = batches.next().await {
151                let next = next?;
152                batch = batch.merge(&next)?;
153            }
154            Ok(batch)
155        }
156        .boxed();
157        ReadBatchTask { task, num_rows }
158    }
159}
160
161impl Stream for MergeStream {
162    type Item = ReadBatchTask;
163
164    fn poll_next(
165        mut self: std::pin::Pin<&mut Self>,
166        cx: &mut std::task::Context<'_>,
167    ) -> std::task::Poll<Option<Self::Item>> {
168        loop {
169            if self.pending.iter().all(Option::is_some) {
170                return std::task::Poll::Ready(Some(self.emit()));
171            }
172
173            // Poll every stream that doesn't have a batch ready yet, instead
174            // of stopping at the first one that isn't. All files in a
175            // fragment share a base read priority (only row offset tells
176            // their reads apart), so stopping early can leave a later
177            // sibling's already-completed read unpolled. That read's
178            // response would then never be consumed, permanently pinning
179            // the I/O scheduler's byte budget at that priority and
180            // deadlocking the scan. See
181            // https://github.com/lance-format/lance/pull/9576#issuecomment-6265846131.
182            //
183            // Each stream here is paced by the caller's own batch size (see
184            // `next_batch_task`), so none of them can race arbitrarily far
185            // ahead of the others while parked in `pending`: a stream only
186            // ever holds rows it has already fully loaded, never a dangling
187            // claim on bytes it hasn't consumed yet.
188            let this = &mut *self;
189            let mut progressed = false;
190            for (index, stream) in this.streams.iter_mut().enumerate() {
191                if this.pending[index].is_some() {
192                    continue;
193                }
194                match stream.poll_next_unpin(cx) {
195                    std::task::Poll::Ready(Some(batch_task)) => {
196                        this.pending[index] = Some(PendingReadBatch::new(batch_task));
197                        progressed = true;
198                    }
199                    std::task::Poll::Ready(None) => {
200                        return std::task::Poll::Ready(None);
201                    }
202                    std::task::Poll::Pending => {}
203                }
204            }
205            if !progressed {
206                return std::task::Poll::Pending;
207            }
208        }
209    }
210}
211
212/// Given multiple streams of batch tasks, merge them into a single stream
213///
214/// This pulls one batch from each stream and then combines the columns from
215/// all of the batches into a single batch.  The order of the batches in the
216/// streams is maintained and the merged batch columns will be in order from first
217/// to last stream. If the streams use different batch boundaries then batches are
218/// sliced so each merged output remains row-aligned.
219///
220/// This stream ends as soon as any of the input streams ends (we do not
221/// verify that the other input streams are finished as well)
222pub fn merge_streams(streams: Vec<ReadBatchTaskStream>) -> ReadBatchTaskStream {
223    if streams.is_empty() {
224        return futures::stream::empty().boxed();
225    }
226    let pending = (0..streams.len()).map(|_| None).collect();
227    MergeStream { streams, pending }.boxed()
228}
229
230/// Apply a mask to the batch, where rows are "deleted" by the _rowid column null.
231///
232/// This is used partly as a performance optimization (cheaper to null than to filter)
233/// but also because there are cases where we want to load the physical rows.  For example,
234/// we may be replacing a column based on some UDF and we want to provide a value for the
235/// deleted rows to ensure the fragments are aligned.
236fn apply_deletions_as_nulls(batch: RecordBatch, mask: &BooleanArray) -> Result<RecordBatch> {
237    // Transform mask into null buffer. Null means deleted, though note that
238    // null buffers are actually validity buffers, so True means not null
239    // and thus not deleted.
240    let mask_buffer = NullBuffer::new(mask.values().clone());
241
242    if mask_buffer.null_count() == 0 {
243        // No rows are deleted
244        return Ok(batch);
245    }
246
247    // For each column convert to data
248    let new_columns = batch
249        .schema()
250        .fields()
251        .iter()
252        .zip(batch.columns())
253        .map(|(field, col)| {
254            if field.name() == ROW_ID || field.name() == ROW_ADDR {
255                let col_data = col.to_data();
256                // If it already has a validity bitmap, then AND it with the mask.
257                // Otherwise, use the boolean buffer as the mask.
258                let null_buffer = NullBuffer::union(col_data.nulls(), Some(&mask_buffer));
259
260                Ok(col_data
261                    .into_builder()
262                    .null_bit_buffer(null_buffer.map(|b| b.buffer().clone()))
263                    .build()
264                    .map(make_array)?)
265            } else {
266                Ok(col.clone())
267            }
268        })
269        .collect::<Result<Vec<_>>>()?;
270
271    Ok(RecordBatch::try_new_with_options(
272        batch.schema(),
273        new_columns,
274        &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
275    )?)
276}
277
278/// Extract version values for a batch selection with a reusable RLE cursor.
279/// Single-run fragments (the common case) take the O(1) fast path.
280fn version_values_for_selection_with_cursor(
281    sequence: &crate::rowids::version::RowDatasetVersionSequence,
282    cursor: &mut crate::rowids::version::RowDatasetVersionCursor,
283    params: &ReadBatchParams,
284    batch_offset: u32,
285    num_rows: u32,
286) -> Result<Vec<u64>> {
287    let selection = params
288        .slice(batch_offset as usize, num_rows as usize)
289        .unwrap()
290        .to_ranges()
291        .unwrap();
292
293    if sequence.runs.len() == 1 {
294        return Ok(vec![sequence.runs[0].version(); num_rows as usize]);
295    }
296
297    let mut versions = Vec::with_capacity(num_rows as usize);
298    for r in &selection {
299        cursor.extend_range(sequence, r.start as usize..r.end as usize, &mut versions)?;
300    }
301    Ok(versions)
302}
303
304fn version_values_for_selection(
305    sequence: &crate::rowids::version::RowDatasetVersionSequence,
306    params: &ReadBatchParams,
307    batch_offset: u32,
308    num_rows: u32,
309) -> Result<Vec<u64>> {
310    // Preserve the common direct-call path without constructing a cursor.
311    // Keep the selection validation in the same order as the general path.
312    let _selection = params
313        .slice(batch_offset as usize, num_rows as usize)
314        .unwrap()
315        .to_ranges()
316        .unwrap();
317    if sequence.runs.len() == 1 {
318        return Ok(vec![sequence.runs[0].version(); num_rows as usize]);
319    }
320    version_values_for_selection_with_cursor(
321        sequence,
322        &mut sequence.cursor(),
323        params,
324        batch_offset,
325        num_rows,
326    )
327}
328
329/// Configuration needed to apply row ids and deletions to a batch
330#[derive(Debug)]
331pub struct RowIdAndDeletesConfig {
332    /// The row ids that were requested
333    pub params: ReadBatchParams,
334    /// Whether to include the row id column in the final batch
335    pub with_row_id: bool,
336    /// Whether to include the row address column in the final batch
337    pub with_row_addr: bool,
338    /// Whether to include the last updated at version column in the final batch
339    pub with_row_last_updated_at_version: bool,
340    /// Whether to include the created at version column in the final batch
341    pub with_row_created_at_version: bool,
342    /// An optional deletion vector to apply to the batch
343    pub deletion_vector: Option<Arc<DeletionVector>>,
344    /// An optional row id sequence to use for the row id column.
345    pub row_id_sequence: Option<Arc<RowIdSequence>>,
346    /// The last_updated_at version sequence
347    pub last_updated_at_sequence: Option<Arc<crate::rowids::version::RowDatasetVersionSequence>>,
348    /// The created_at version sequence
349    pub created_at_sequence: Option<Arc<crate::rowids::version::RowDatasetVersionSequence>>,
350    /// Whether to make deleted rows null instead of filtering them out
351    pub make_deletions_null: bool,
352    /// The total number of rows that will be loaded
353    ///
354    /// This is needed to convert ReadbatchParams::RangeTo into a valid range
355    pub total_num_rows: u32,
356}
357
358impl RowIdAndDeletesConfig {
359    fn has_system_cols(&self) -> bool {
360        self.with_row_id
361            || self.with_row_addr
362            || self.with_row_last_updated_at_version
363            || self.with_row_created_at_version
364    }
365}
366
367pub fn apply_row_id_and_deletes(
368    batch: RecordBatch,
369    batch_offset: u32,
370    fragment_id: u32,
371    config: &RowIdAndDeletesConfig,
372) -> Result<RecordBatch> {
373    apply_row_id_and_deletes_with_system_columns(
374        batch,
375        batch_offset,
376        fragment_id,
377        config,
378        PrecomputedSystemColumns::default(),
379        None,
380    )
381}
382
383#[derive(Default)]
384struct PrecomputedSystemColumns {
385    row_ids: Option<Result<Arc<UInt64Array>>>,
386    last_updated_versions: Option<Result<Arc<UInt64Array>>>,
387    created_versions: Option<Result<Arc<UInt64Array>>>,
388}
389
390const ROW_ID_READ_AHEAD_ROWS: usize = 64 * 1024;
391
392struct PrecomputedRowIdChunk {
393    logical_offset: usize,
394    values: Arc<UInt64Array>,
395}
396
397struct CachedOutputSchema {
398    input: SchemaRef,
399    output: SchemaRef,
400}
401
402impl PrecomputedRowIdChunk {
403    fn end_offset(&self) -> usize {
404        self.logical_offset + self.values.len()
405    }
406
407    fn slice(&self, logical_offset: usize, num_rows: usize) -> Option<Arc<UInt64Array>> {
408        let offset_in_chunk = logical_offset.checked_sub(self.logical_offset)?;
409        if offset_in_chunk + num_rows > self.values.len() {
410            return None;
411        }
412        if offset_in_chunk == 0 && num_rows == self.values.len() {
413            return Some(self.values.clone());
414        }
415        Some(Arc::new(self.values.slice(offset_in_chunk, num_rows)))
416    }
417}
418
419fn decode_row_id_chunk<const USE_DENSE_ROW_ID_EXPANSION: bool>(
420    sequence: &RowIdSequence,
421    cursor: &mut RowIdSequenceCursor,
422    params: &ReadBatchParams,
423    logical_offset: usize,
424    chunk_len: usize,
425) -> Result<PrecomputedRowIdChunk> {
426    let selection = params
427        .slice(logical_offset, chunk_len)
428        .unwrap()
429        .to_ranges()
430        .unwrap();
431    let values = match selection.as_slice() {
432        [range] if USE_DENSE_ROW_ID_EXPANSION => UInt64Array::from(
433            sequence
434                .select_dense_range_with_cursor(cursor, range.start as usize..range.end as usize),
435        ),
436        [range] => UInt64Array::from(
437            sequence.select_range_with_cursor(cursor, range.start as usize..range.end as usize),
438        ),
439        _ => sequence
440            .select_with_cursor(
441                cursor,
442                selection
443                    .iter()
444                    .flat_map(|range| range.start as usize..range.end as usize),
445            )
446            .collect::<UInt64Array>(),
447    };
448    if values.len() != chunk_len {
449        return Err(Error::corrupt_file_named(
450            "row ID metadata",
451            format!(
452                "decoded row ID chunk at selected offset {logical_offset} contains {} rows, but the selection requires {chunk_len} rows",
453                values.len()
454            ),
455        ));
456    }
457    Ok(PrecomputedRowIdChunk {
458        logical_offset,
459        values: Arc::new(values),
460    })
461}
462
463fn selected_row_count(params: &ReadBatchParams, total_num_rows: usize) -> usize {
464    match params {
465        ReadBatchParams::Range(range) => range.len(),
466        ReadBatchParams::Ranges(ranges) => ranges
467            .iter()
468            .map(|range| (range.end - range.start) as usize)
469            .sum(),
470        ReadBatchParams::RangeFull => total_num_rows,
471        ReadBatchParams::RangeTo(range) => range.end,
472        ReadBatchParams::RangeFrom(range) => total_num_rows.saturating_sub(range.start),
473        ReadBatchParams::Indices(indices) => indices.len(),
474    }
475}
476
477#[instrument(name = "apply_row_id_and_deletes", level = "debug", skip_all)]
478fn apply_row_id_and_deletes_with_system_columns(
479    batch: RecordBatch,
480    batch_offset: u32,
481    fragment_id: u32,
482    config: &RowIdAndDeletesConfig,
483    precomputed: PrecomputedSystemColumns,
484    output_schema_cache: Option<&OnceLock<CachedOutputSchema>>,
485) -> Result<RecordBatch> {
486    let PrecomputedSystemColumns {
487        row_ids: precomputed_row_ids,
488        last_updated_versions,
489        created_versions,
490    } = precomputed;
491    let mut deletion_vector = config.deletion_vector.as_ref();
492    // Convert Some(NoDeletions) into None to simplify logic below
493    if let Some(deletion_vector_inner) = deletion_vector
494        && matches!(deletion_vector_inner.as_ref(), DeletionVector::NoDeletions)
495    {
496        deletion_vector = None;
497    }
498    let has_deletions = deletion_vector.is_some();
499    debug_assert!(batch.num_columns() > 0 || config.has_system_cols() || has_deletions);
500
501    // If row id sequence is None, then row id IS row address.
502    let should_fetch_row_addr = config.with_row_addr
503        || (config.with_row_id && config.row_id_sequence.is_none())
504        || has_deletions;
505
506    let num_rows = batch.num_rows() as u32;
507
508    let row_addrs =
509        if should_fetch_row_addr {
510            let _rowaddrs = tracing::span!(tracing::Level::DEBUG, "fetch_row_addrs").entered();
511            let mut row_addrs = Vec::with_capacity(num_rows as usize);
512            for offset_range in config
513                .params
514                .slice(batch_offset as usize, num_rows as usize)
515                .unwrap()
516                .iter_offset_ranges()?
517            {
518                row_addrs.extend(offset_range.map(|row_offset| {
519                    u64::from(RowAddress::new_from_parts(fragment_id, row_offset))
520                }));
521            }
522
523            Some(Arc::new(UInt64Array::from(row_addrs)))
524        } else {
525            None
526        };
527
528    let row_ids = if config.with_row_id {
529        let _rowids = tracing::span!(tracing::Level::DEBUG, "fetch_row_ids").entered();
530        if let Some(row_ids) = precomputed_row_ids {
531            let row_ids = row_ids?;
532            debug_assert_eq!(row_ids.len(), num_rows as usize);
533            Some(row_ids)
534        } else if let Some(row_id_sequence) = &config.row_id_sequence {
535            let selection = config
536                .params
537                .slice(batch_offset as usize, num_rows as usize)
538                .unwrap()
539                .to_ranges()
540                .unwrap();
541            let row_ids = row_id_sequence
542                .select(
543                    selection
544                        .iter()
545                        .flat_map(|r| r.start as usize..r.end as usize),
546                )
547                .collect::<UInt64Array>();
548            Some(Arc::new(row_ids))
549        } else {
550            // If we don't have a row id sequence, can assume the row ids are
551            // the same as the row addresses.
552            row_addrs.clone()
553        }
554    } else {
555        None
556    };
557
558    let span = tracing::span!(tracing::Level::DEBUG, "apply_deletions");
559    let _enter = span.enter();
560    let deletion_mask = deletion_vector.and_then(|v| {
561        let row_addrs: &[u64] = row_addrs.as_ref().unwrap().values();
562        v.build_predicate(row_addrs.iter())
563    });
564
565    let mut system_columns: Vec<(Field, ArrayRef)> = Vec::with_capacity(4);
566    if config.with_row_id {
567        system_columns.push((ROW_ID_FIELD.clone(), row_ids.unwrap()));
568    }
569    if config.with_row_addr {
570        system_columns.push((ROW_ADDR_FIELD.clone(), row_addrs.unwrap()));
571    }
572    if config.with_row_last_updated_at_version {
573        let version_arr = if let Some(version_arr) = last_updated_versions {
574            version_arr?
575        } else if let Some(sequence) = &config.last_updated_at_sequence {
576            Arc::new(UInt64Array::from(version_values_for_selection(
577                sequence,
578                &config.params,
579                batch_offset,
580                num_rows,
581            )?))
582        } else {
583            // Default to version 1 if sequence not provided
584            Arc::new(UInt64Array::from(vec![1u64; num_rows as usize]))
585        };
586        system_columns.push((ROW_LAST_UPDATED_AT_VERSION_FIELD.clone(), version_arr));
587    }
588    if config.with_row_created_at_version {
589        let version_arr = if let Some(version_arr) = created_versions {
590            version_arr?
591        } else if let Some(sequence) = &config.created_at_sequence {
592            Arc::new(UInt64Array::from(version_values_for_selection(
593                sequence,
594                &config.params,
595                batch_offset,
596                num_rows,
597            )?))
598        } else {
599            // Default to version 1 if sequence not provided
600            Arc::new(UInt64Array::from(vec![1u64; num_rows as usize]))
601        };
602        system_columns.push((ROW_CREATED_AT_VERSION_FIELD.clone(), version_arr));
603    }
604
605    let batch = if system_columns.is_empty() {
606        batch
607    } else if let Some(output_schema_cache) = output_schema_cache {
608        let input_schema = batch.schema();
609        let make_output_schema = || {
610            let mut fields = input_schema
611                .fields()
612                .iter()
613                .map(|field| field.as_ref().clone())
614                .collect::<Vec<_>>();
615            fields.extend(system_columns.iter().map(|(field, _)| field.clone()));
616            Arc::new(Schema::new_with_metadata(
617                fields,
618                input_schema.metadata().clone(),
619            ))
620        };
621        let cached = output_schema_cache.get_or_init(|| CachedOutputSchema {
622            input: input_schema.clone(),
623            output: make_output_schema(),
624        });
625        let output_schema = if Arc::ptr_eq(&cached.input, &input_schema)
626            || cached.input.as_ref() == input_schema.as_ref()
627        {
628            cached.output.clone()
629        } else {
630            make_output_schema()
631        };
632        let mut columns = Vec::with_capacity(batch.num_columns() + system_columns.len());
633        columns.extend_from_slice(batch.columns());
634        columns.extend(system_columns.into_iter().map(|(_, array)| array));
635        RecordBatch::try_new_with_options(
636            output_schema,
637            columns,
638            &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
639        )?
640    } else {
641        system_columns
642            .into_iter()
643            .try_fold(batch, |batch, (field, array)| {
644                batch.try_with_column(field, array)
645            })?
646    };
647
648    match (deletion_mask, config.make_deletions_null) {
649        (None, _) => Ok(batch),
650        (Some(mask), false) => Ok(arrow::compute::filter_record_batch(&batch, &mask)?),
651        (Some(mask), true) => Ok(apply_deletions_as_nulls(batch, &mask)?),
652    }
653}
654
655/// Given a stream of batch tasks this function will add a row ids column (if requested)
656/// and also apply a deletions vector to the batch.
657///
658/// This converts from BatchTaskStream to BatchFutStream because, if we are applying a
659/// deletion vector, it is impossible to know how many output rows we will have.
660pub fn wrap_with_row_id_and_delete(
661    stream: ReadBatchTaskStream,
662    fragment_id: u32,
663    config: RowIdAndDeletesConfig,
664) -> ReadBatchFutStream {
665    let (row_id_cursor, use_dense_row_id_expansion) = config
666        .row_id_sequence
667        .as_ref()
668        .filter(|_| config.with_row_id)
669        .map(|sequence| {
670            let (cursor, use_dense_range_expansion) = sequence.cursor_with_dense_range_expansion();
671            (Some(cursor), use_dense_range_expansion)
672        })
673        .unwrap_or((None, false));
674    if use_dense_row_id_expansion {
675        wrap_with_row_id_and_delete_impl::<true>(stream, fragment_id, config, row_id_cursor)
676    } else {
677        wrap_with_row_id_and_delete_impl::<false>(stream, fragment_id, config, row_id_cursor)
678    }
679}
680
681fn wrap_with_row_id_and_delete_impl<const USE_DENSE_ROW_ID_EXPANSION: bool>(
682    stream: ReadBatchTaskStream,
683    fragment_id: u32,
684    config: RowIdAndDeletesConfig,
685    mut row_id_cursor: Option<RowIdSequenceCursor>,
686) -> ReadBatchFutStream {
687    let config = Arc::new(config);
688    let output_schema_cache = Arc::new(OnceLock::new());
689    let mut row_id_chunk: Option<PrecomputedRowIdChunk> = None;
690    let mut uniform_batch_size = None;
691    let mut use_uniform_batch_fast_paths = true;
692    let selected_rows = selected_row_count(&config.params, config.total_num_rows as usize);
693    let mut last_updated_cursor = config
694        .last_updated_at_sequence
695        .as_ref()
696        .filter(|sequence| config.with_row_last_updated_at_version && sequence.runs.len() > 1)
697        .map(|sequence| sequence.cursor());
698    let mut created_cursor = config
699        .created_at_sequence
700        .as_ref()
701        .filter(|sequence| config.with_row_created_at_version && sequence.runs.len() > 1)
702        .map(|sequence| sequence.cursor());
703    let mut offset = 0;
704    stream
705        .map(move |batch_task| {
706            let config = config.clone();
707            let this_offset = offset;
708            let num_rows = batch_task.num_rows;
709            offset += num_rows;
710            let logical_offset = this_offset as usize;
711            let num_rows_usize = num_rows as usize;
712            if num_rows_usize != 0 && use_uniform_batch_fast_paths {
713                if let Some(batch_size) = uniform_batch_size {
714                    // A shorter final batch is expected. Any other task-size change can
715                    // repeatedly straddle read-ahead boundaries. Drain the current row-ID
716                    // cache, then use exact per-task decoding and the original incremental
717                    // system-column assembly from this point forward.
718                    if num_rows_usize != batch_size
719                        && logical_offset + num_rows_usize < selected_rows
720                    {
721                        use_uniform_batch_fast_paths = false;
722                    }
723                } else {
724                    uniform_batch_size = Some(num_rows_usize);
725                }
726            }
727            let output_schema_cache = use_uniform_batch_fast_paths
728                .then(|| output_schema_cache.clone());
729            // Build row ids while pulling the ordered task stream, before the
730            // batch futures can run concurrently. Adjacent batches share a
731            // bounded chunk and take zero-copy Arrow slices from it.
732            let row_ids = config.row_id_sequence.as_ref().and_then(|sequence| {
733                row_id_cursor.as_mut().map(|cursor| {
734                    if num_rows_usize == 0 {
735                        return Ok(Arc::new(UInt64Array::from(Vec::<u64>::new())));
736                    }
737                    if !use_uniform_batch_fast_paths
738                        && row_id_chunk
739                            .as_ref()
740                            .is_none_or(|chunk| chunk.end_offset() <= logical_offset)
741                    {
742                        row_id_chunk = None;
743                        return decode_row_id_chunk::<USE_DENSE_ROW_ID_EXPANSION>(
744                            sequence,
745                            cursor,
746                            &config.params,
747                            logical_offset,
748                            num_rows_usize,
749                        )
750                        .map(|chunk| chunk.values);
751                    }
752                    if let Some(row_ids) = row_id_chunk
753                        .as_ref()
754                        .and_then(|chunk| chunk.slice(logical_offset, num_rows_usize))
755                    {
756                        return Ok(row_ids);
757                    }
758
759                    let prefix = row_id_chunk.as_ref().and_then(|chunk| {
760                        let prefix_len = chunk.end_offset().checked_sub(logical_offset)?;
761                        if prefix_len == 0 {
762                            None
763                        } else {
764                            chunk.slice(logical_offset, prefix_len)
765                        }
766                    });
767                    let decode_offset = logical_offset
768                        + prefix
769                            .as_ref()
770                            .map(|row_ids| row_ids.len())
771                            .unwrap_or_default();
772                    let required_end = logical_offset + num_rows_usize;
773                    let missing_rows = required_end.saturating_sub(decode_offset);
774                    let chunk_len = if use_uniform_batch_fast_paths {
775                        let batch_size = uniform_batch_size.unwrap_or(num_rows_usize);
776                        let batches_per_chunk = (ROW_ID_READ_AHEAD_ROWS / batch_size).max(1);
777                        let chunk_rows = batch_size.saturating_mul(batches_per_chunk);
778                        missing_rows.max(chunk_rows)
779                    } else {
780                        missing_rows
781                    }
782                    .min(selected_rows.saturating_sub(decode_offset));
783                    let chunk = decode_row_id_chunk::<USE_DENSE_ROW_ID_EXPANSION>(
784                        sequence,
785                        cursor,
786                        &config.params,
787                        decode_offset,
788                        chunk_len,
789                    )?;
790                    let suffix = chunk.slice(decode_offset, missing_rows).ok_or_else(|| {
791                        Error::corrupt_file_named(
792                            "row ID metadata",
793                            format!(
794                                "decoded row ID chunk at selected offset {decode_offset} contains {} rows, but the current batch requires {missing_rows} more rows",
795                                chunk.values.len()
796                            ),
797                        )
798                    })?;
799                    let row_ids = if let Some(prefix) = prefix {
800                        let mut values = Vec::with_capacity(num_rows_usize);
801                        values.extend_from_slice(prefix.values());
802                        values.extend_from_slice(suffix.values());
803                        Arc::new(UInt64Array::from(values))
804                    } else {
805                        suffix
806                    };
807                    row_id_chunk = Some(chunk);
808                    Ok(row_ids)
809                })
810            });
811            let last_updated_versions =
812                config
813                    .last_updated_at_sequence
814                    .as_ref()
815                    .and_then(|sequence| {
816                        last_updated_cursor.as_mut().map(|cursor| {
817                            version_values_for_selection_with_cursor(
818                                sequence,
819                                cursor,
820                                &config.params,
821                                this_offset,
822                                num_rows,
823                            )
824                            .map(UInt64Array::from)
825                            .map(Arc::new)
826                        })
827                    });
828            let created_versions = config.created_at_sequence.as_ref().and_then(|sequence| {
829                created_cursor.as_mut().map(|cursor| {
830                    version_values_for_selection_with_cursor(
831                        sequence,
832                        cursor,
833                        &config.params,
834                        this_offset,
835                        num_rows,
836                    )
837                    .map(UInt64Array::from)
838                    .map(Arc::new)
839                })
840            });
841            batch_task
842                .task
843                .map(move |batch| {
844                    apply_row_id_and_deletes_with_system_columns(
845                        batch?,
846                        this_offset,
847                        fragment_id,
848                        config.as_ref(),
849                        PrecomputedSystemColumns {
850                            row_ids,
851                            last_updated_versions,
852                            created_versions,
853                        },
854                        output_schema_cache.as_deref(),
855                    )
856                })
857                .boxed()
858        })
859        .boxed()
860}
861
862#[cfg(test)]
863mod tests {
864    use std::sync::Arc;
865
866    use arrow::{array::AsArray, datatypes::UInt64Type};
867    use arrow_array::{RecordBatch, UInt32Array, types::Int32Type};
868    use arrow_schema::ArrowError;
869    use futures::{
870        FutureExt, StreamExt, TryStreamExt,
871        stream::{self, BoxStream},
872    };
873    use lance_core::{
874        ROW_ID,
875        utils::{address::RowAddress, deletion::DeletionVector},
876    };
877    use lance_datagen::{BatchCount, RowCount};
878    use lance_io::{ReadBatchParams, stream::arrow_stream_to_lance_stream};
879    use roaring::RoaringBitmap;
880
881    use crate::{rowids::RowIdSequence, utils::stream::ReadBatchTask};
882
883    use super::RowIdAndDeletesConfig;
884
885    fn batch_task_stream(
886        datagen_stream: BoxStream<'static, std::result::Result<RecordBatch, ArrowError>>,
887    ) -> super::ReadBatchTaskStream {
888        arrow_stream_to_lance_stream(datagen_stream)
889            .map(|batch| ReadBatchTask {
890                num_rows: batch.as_ref().unwrap().num_rows() as u32,
891                task: std::future::ready(batch).boxed(),
892            })
893            .boxed()
894    }
895
896    #[tokio::test]
897    async fn test_basic_zip() {
898        let left = batch_task_stream(
899            lance_datagen::gen_batch()
900                .col("x", lance_datagen::array::step::<Int32Type>())
901                .into_reader_stream(RowCount::from(100), BatchCount::from(10))
902                .0,
903        );
904        let right = batch_task_stream(
905            lance_datagen::gen_batch()
906                .col("y", lance_datagen::array::step::<Int32Type>())
907                .into_reader_stream(RowCount::from(100), BatchCount::from(10))
908                .0,
909        );
910
911        let merged = super::merge_streams(vec![left, right])
912            .map(|batch_task| batch_task.task)
913            .buffered(1)
914            .try_collect::<Vec<_>>()
915            .await
916            .unwrap();
917
918        let expected = lance_datagen::gen_batch()
919            .col("x", lance_datagen::array::step::<Int32Type>())
920            .col("y", lance_datagen::array::step::<Int32Type>())
921            .into_reader_rows(RowCount::from(100), BatchCount::from(10))
922            .collect::<Result<Vec<_>, ArrowError>>()
923            .unwrap();
924        assert_eq!(merged, expected);
925    }
926
927    #[tokio::test]
928    async fn test_stable_row_ids_across_concurrent_batches_and_deletes() {
929        let expected = (10_000..120_000)
930            .filter(|row_id| row_id % 13 != 0)
931            .collect::<Vec<u64>>();
932        let row_id_sequence = Arc::new(RowIdSequence::try_from_iter(expected.clone()).unwrap());
933        let deletion_offsets = (0..expected.len() as u32).step_by(997).collect::<Vec<_>>();
934        let deletion_vector = Some(Arc::new(DeletionVector::Bitmap(
935            deletion_offsets.iter().copied().collect(),
936        )));
937
938        let batches = expected
939            .chunks(257)
940            .map(|chunk| arrow_array::record_batch!(("x", Int32, vec![0; chunk.len()])).unwrap())
941            .map(Ok)
942            .collect::<Vec<std::result::Result<RecordBatch, ArrowError>>>();
943        let data = batch_task_stream(stream::iter(batches).boxed());
944        let config = RowIdAndDeletesConfig {
945            params: ReadBatchParams::RangeFull,
946            with_row_id: true,
947            with_row_addr: true,
948            with_row_last_updated_at_version: false,
949            with_row_created_at_version: false,
950            deletion_vector,
951            row_id_sequence: Some(row_id_sequence),
952            last_updated_at_sequence: None,
953            created_at_sequence: None,
954            make_deletions_null: false,
955            total_num_rows: expected.len() as u32,
956        };
957
958        let batches = super::wrap_with_row_id_and_delete(data, 7, config)
959            .buffered(8)
960            .try_collect::<Vec<_>>()
961            .await
962            .unwrap();
963        let actual_row_ids = batches
964            .iter()
965            .flat_map(|batch| batch[ROW_ID].as_primitive::<UInt64Type>().values())
966            .copied()
967            .collect::<Vec<_>>();
968        let actual_row_addrs = batches
969            .iter()
970            .flat_map(|batch| {
971                batch[lance_core::ROW_ADDR]
972                    .as_primitive::<UInt64Type>()
973                    .values()
974            })
975            .copied()
976            .collect::<Vec<_>>();
977        let expected_survivors = expected
978            .iter()
979            .enumerate()
980            .filter(|(offset, _)| deletion_offsets.binary_search(&(*offset as u32)).is_err())
981            .map(|(offset, row_id)| {
982                (
983                    *row_id,
984                    u64::from(RowAddress::new_from_parts(7, offset as u32)),
985                )
986            })
987            .collect::<Vec<_>>();
988
989        assert_eq!(
990            actual_row_ids,
991            expected_survivors
992                .iter()
993                .map(|(row_id, _)| *row_id)
994                .collect::<Vec<_>>()
995        );
996        assert_eq!(
997            actual_row_addrs,
998            expected_survivors
999                .iter()
1000                .map(|(_, row_addr)| *row_addr)
1001                .collect::<Vec<_>>()
1002        );
1003    }
1004
1005    #[tokio::test]
1006    async fn test_stable_row_ids_with_unsorted_indices() {
1007        let expected = (100..140)
1008            .filter(|row_id| row_id % 3 != 0)
1009            .collect::<Vec<u64>>();
1010        let indices = UInt32Array::from(vec![8, 2, 9, 1, 6]);
1011        let batches = [2, 2, 1].into_iter().map(|num_rows| ReadBatchTask {
1012            num_rows,
1013            task: std::future::ready(Ok(arrow_array::record_batch!((
1014                "x",
1015                Int32,
1016                vec![0; num_rows as usize]
1017            ))
1018            .unwrap()))
1019            .boxed(),
1020        });
1021        let config = RowIdAndDeletesConfig {
1022            params: ReadBatchParams::Indices(indices.clone()),
1023            with_row_id: true,
1024            with_row_addr: false,
1025            with_row_last_updated_at_version: false,
1026            with_row_created_at_version: false,
1027            deletion_vector: None,
1028            row_id_sequence: Some(Arc::new(
1029                RowIdSequence::try_from_iter(expected.clone()).unwrap(),
1030            )),
1031            last_updated_at_sequence: None,
1032            created_at_sequence: None,
1033            make_deletions_null: false,
1034            total_num_rows: expected.len() as u32,
1035        };
1036
1037        let actual = super::wrap_with_row_id_and_delete(stream::iter(batches).boxed(), 7, config)
1038            .buffered(3)
1039            .try_collect::<Vec<_>>()
1040            .await
1041            .unwrap()
1042            .iter()
1043            .flat_map(|batch| batch[ROW_ID].as_primitive::<UInt64Type>().values())
1044            .copied()
1045            .collect::<Vec<_>>();
1046        let expected = indices
1047            .values()
1048            .iter()
1049            .map(|index| expected[*index as usize])
1050            .collect::<Vec<_>>();
1051        assert_eq!(actual, expected);
1052    }
1053
1054    #[tokio::test]
1055    async fn test_repeated_row_id_after_bulk_segment_boundary() {
1056        let mut row_ids = RowIdSequence::from(0..5);
1057        row_ids.extend(RowIdSequence::from(10..20));
1058        let batches = [1_u32, 2].into_iter().map(|num_rows| ReadBatchTask {
1059            num_rows,
1060            task: std::future::ready(Ok(arrow_array::record_batch!((
1061                "x",
1062                Int32,
1063                vec![0; num_rows as usize]
1064            ))
1065            .unwrap()))
1066            .boxed(),
1067        });
1068        let config = RowIdAndDeletesConfig {
1069            params: ReadBatchParams::Indices(UInt32Array::from(vec![4, 4, 5])),
1070            with_row_id: true,
1071            with_row_addr: false,
1072            with_row_last_updated_at_version: false,
1073            with_row_created_at_version: false,
1074            deletion_vector: None,
1075            row_id_sequence: Some(Arc::new(row_ids)),
1076            last_updated_at_sequence: None,
1077            created_at_sequence: None,
1078            make_deletions_null: false,
1079            total_num_rows: 15,
1080        };
1081
1082        let actual = super::wrap_with_row_id_and_delete(stream::iter(batches).boxed(), 0, config)
1083            .buffered(1)
1084            .try_collect::<Vec<_>>()
1085            .await
1086            .unwrap()
1087            .iter()
1088            .flat_map(|batch| batch[ROW_ID].as_primitive::<UInt64Type>().values())
1089            .copied()
1090            .collect::<Vec<_>>();
1091        assert_eq!(actual, vec![4, 4, 10]);
1092    }
1093
1094    #[tokio::test]
1095    async fn test_stable_row_id_read_ahead_range_boundary_and_tail() {
1096        let all_row_ids = (10_000..120_000)
1097            .filter(|row_id| row_id % 11 != 0)
1098            .collect::<Vec<u64>>();
1099        let selection = 1_234..71_237;
1100        let selected_len = selection.len();
1101        let mut remaining = selected_len;
1102        let tasks = std::iter::from_fn(move || {
1103            if remaining == 0 {
1104                return None;
1105            }
1106            let num_rows = remaining.min(1_025);
1107            remaining -= num_rows;
1108            Some(ReadBatchTask {
1109                num_rows: num_rows as u32,
1110                task: std::future::ready(Ok(arrow_array::record_batch!((
1111                    "x",
1112                    Int32,
1113                    vec![0; num_rows]
1114                ))
1115                .unwrap()))
1116                .boxed(),
1117            })
1118        });
1119        let config = RowIdAndDeletesConfig {
1120            params: ReadBatchParams::Range(selection.clone()),
1121            with_row_id: true,
1122            with_row_addr: false,
1123            with_row_last_updated_at_version: false,
1124            with_row_created_at_version: false,
1125            deletion_vector: None,
1126            row_id_sequence: Some(Arc::new(
1127                RowIdSequence::try_from_iter(all_row_ids.clone()).unwrap(),
1128            )),
1129            last_updated_at_sequence: None,
1130            created_at_sequence: None,
1131            make_deletions_null: false,
1132            total_num_rows: all_row_ids.len() as u32,
1133        };
1134
1135        let batches = super::wrap_with_row_id_and_delete(stream::iter(tasks).boxed(), 3, config)
1136            .buffered(8)
1137            .try_collect::<Vec<_>>()
1138            .await
1139            .unwrap();
1140        assert_eq!(batches.len(), 69);
1141        assert_eq!(batches.last().unwrap().num_rows(), 303);
1142
1143        fn row_ids(batch: &RecordBatch) -> &arrow_array::UInt64Array {
1144            batch[ROW_ID].as_primitive::<UInt64Type>()
1145        }
1146        assert_eq!(
1147            row_ids(&batches[62]).values().as_ptr(),
1148            row_ids(&batches[0])
1149                .values()
1150                .as_ptr()
1151                .wrapping_add(62 * 1_025)
1152        );
1153        assert_eq!(
1154            row_ids(&batches[68]).values().as_ptr(),
1155            row_ids(&batches[64])
1156                .values()
1157                .as_ptr()
1158                .wrapping_add(4 * 1_025)
1159        );
1160
1161        let actual = batches
1162            .iter()
1163            .flat_map(|batch| row_ids(batch).values())
1164            .copied()
1165            .collect::<Vec<_>>();
1166        assert_eq!(actual, all_row_ids[selection]);
1167    }
1168
1169    #[tokio::test]
1170    async fn test_stable_row_id_read_ahead_empty_task() {
1171        let tasks = [0_u32, 1].into_iter().map(|num_rows| ReadBatchTask {
1172            num_rows,
1173            task: std::future::ready(Ok(arrow_array::record_batch!((
1174                "x",
1175                Int32,
1176                vec![0; num_rows as usize]
1177            ))
1178            .unwrap()))
1179            .boxed(),
1180        });
1181        let config = RowIdAndDeletesConfig {
1182            params: ReadBatchParams::RangeFull,
1183            with_row_id: true,
1184            with_row_addr: false,
1185            with_row_last_updated_at_version: false,
1186            with_row_created_at_version: false,
1187            deletion_vector: None,
1188            row_id_sequence: Some(Arc::new(RowIdSequence::try_from_iter([42]).unwrap())),
1189            last_updated_at_sequence: None,
1190            created_at_sequence: None,
1191            make_deletions_null: false,
1192            total_num_rows: 1,
1193        };
1194
1195        let batches = super::wrap_with_row_id_and_delete(stream::iter(tasks).boxed(), 0, config)
1196            .buffered(2)
1197            .try_collect::<Vec<_>>()
1198            .await
1199            .unwrap();
1200        assert_eq!(batches[0].num_rows(), 0);
1201        assert_eq!(
1202            batches[1][ROW_ID].as_primitive::<UInt64Type>().values(),
1203            &[42]
1204        );
1205    }
1206
1207    #[tokio::test]
1208    async fn test_truncated_stable_row_ids_returns_error() {
1209        let task = ReadBatchTask {
1210            num_rows: 10,
1211            task: std::future::ready(Ok(
1212                arrow_array::record_batch!(("x", Int32, vec![0; 10])).unwrap()
1213            ))
1214            .boxed(),
1215        };
1216        let config = RowIdAndDeletesConfig {
1217            params: ReadBatchParams::RangeFull,
1218            with_row_id: true,
1219            with_row_addr: false,
1220            with_row_last_updated_at_version: false,
1221            with_row_created_at_version: false,
1222            deletion_vector: None,
1223            row_id_sequence: Some(Arc::new(RowIdSequence::try_from_iter(0_u64..5).unwrap())),
1224            last_updated_at_sequence: None,
1225            created_at_sequence: None,
1226            make_deletions_null: false,
1227            total_num_rows: 10,
1228        };
1229
1230        let error = super::wrap_with_row_id_and_delete(stream::iter([task]).boxed(), 0, config)
1231            .buffered(1)
1232            .try_collect::<Vec<_>>()
1233            .await
1234            .unwrap_err();
1235        assert!(matches!(error, lance_core::Error::CorruptFile { .. }));
1236        assert!(error.to_string().contains(
1237            "decoded row ID chunk at selected offset 0 contains 5 rows, but the selection requires 10 rows"
1238        ));
1239    }
1240
1241    #[tokio::test]
1242    async fn test_truncated_stable_row_ids_with_unsorted_indices_returns_error() {
1243        let tasks = (0..4).map(|_| ReadBatchTask {
1244            num_rows: 1,
1245            task: std::future::ready(Ok(
1246                arrow_array::record_batch!(("x", Int32, vec![0])).unwrap()
1247            ))
1248            .boxed(),
1249        });
1250        let config = RowIdAndDeletesConfig {
1251            params: ReadBatchParams::Indices(UInt32Array::from(vec![0, 5, 1, 2])),
1252            with_row_id: true,
1253            with_row_addr: false,
1254            with_row_last_updated_at_version: false,
1255            with_row_created_at_version: false,
1256            deletion_vector: None,
1257            row_id_sequence: Some(Arc::new(RowIdSequence::try_from_iter(0_u64..5).unwrap())),
1258            last_updated_at_sequence: None,
1259            created_at_sequence: None,
1260            make_deletions_null: false,
1261            total_num_rows: 6,
1262        };
1263
1264        let error = super::wrap_with_row_id_and_delete(stream::iter(tasks).boxed(), 0, config)
1265            .buffered(1)
1266            .try_collect::<Vec<_>>()
1267            .await
1268            .unwrap_err();
1269        assert!(matches!(error, lance_core::Error::CorruptFile { .. }));
1270    }
1271
1272    #[tokio::test]
1273    async fn test_stable_row_id_read_ahead_with_variable_task_boundaries() {
1274        let total_rows = super::ROW_ID_READ_AHEAD_ROWS * 3 + 41;
1275        let expected = (0_u64..)
1276            .filter(|row_id| row_id % 17 != 0)
1277            .take(total_rows)
1278            .collect::<Vec<_>>();
1279        let mut remaining = total_rows;
1280        let mut use_short_task = true;
1281        let tasks = std::iter::from_fn(move || {
1282            if remaining == 0 {
1283                return None;
1284            }
1285            let requested = if use_short_task { 32_768 } else { 32_769 };
1286            use_short_task = !use_short_task;
1287            let num_rows = remaining.min(requested);
1288            remaining -= num_rows;
1289            Some(ReadBatchTask {
1290                num_rows: num_rows as u32,
1291                task: std::future::ready(Ok(arrow_array::record_batch!((
1292                    "x",
1293                    Int32,
1294                    vec![0; num_rows]
1295                ))
1296                .unwrap()))
1297                .boxed(),
1298            })
1299        });
1300        let config = RowIdAndDeletesConfig {
1301            params: ReadBatchParams::RangeFull,
1302            with_row_id: true,
1303            with_row_addr: false,
1304            with_row_last_updated_at_version: false,
1305            with_row_created_at_version: false,
1306            deletion_vector: None,
1307            row_id_sequence: Some(Arc::new(
1308                RowIdSequence::try_from_iter(expected.clone()).unwrap(),
1309            )),
1310            last_updated_at_sequence: None,
1311            created_at_sequence: None,
1312            make_deletions_null: false,
1313            total_num_rows: total_rows as u32,
1314        };
1315
1316        let batches = super::wrap_with_row_id_and_delete(stream::iter(tasks).boxed(), 0, config)
1317            .buffered(3)
1318            .try_collect::<Vec<_>>()
1319            .await
1320            .unwrap();
1321        let second_schema = batches[1].schema();
1322        let third_schema = batches[2].schema();
1323        assert!(!Arc::ptr_eq(&second_schema, &third_schema));
1324
1325        let actual = batches
1326            .iter()
1327            .flat_map(|batch| batch[ROW_ID].as_primitive::<UInt64Type>().values())
1328            .copied()
1329            .collect::<Vec<_>>();
1330        assert_eq!(actual, expected);
1331    }
1332
1333    #[tokio::test]
1334    async fn test_system_columns_share_schema_for_equivalent_payload_batches() {
1335        let batches = (0..3)
1336            .map(|batch_index| {
1337                arrow_array::record_batch!((
1338                    "payload",
1339                    Int32,
1340                    (batch_index * 10..(batch_index + 1) * 10).collect::<Vec<_>>()
1341                ))
1342                .unwrap()
1343            })
1344            .collect::<Vec<_>>();
1345        assert!(!Arc::ptr_eq(&batches[0].schema(), &batches[1].schema()));
1346        let tasks = batches.into_iter().map(|batch| ReadBatchTask {
1347            num_rows: batch.num_rows() as u32,
1348            task: std::future::ready(Ok(batch)).boxed(),
1349        });
1350        let config = RowIdAndDeletesConfig {
1351            params: ReadBatchParams::RangeFull,
1352            with_row_id: true,
1353            with_row_addr: true,
1354            with_row_last_updated_at_version: true,
1355            with_row_created_at_version: true,
1356            deletion_vector: None,
1357            row_id_sequence: Some(Arc::new(
1358                RowIdSequence::try_from_iter((0..30).map(|row_id| 100 + row_id + row_id / 7))
1359                    .unwrap(),
1360            )),
1361            last_updated_at_sequence: None,
1362            created_at_sequence: None,
1363            make_deletions_null: false,
1364            total_num_rows: 30,
1365        };
1366
1367        let batches = super::wrap_with_row_id_and_delete(stream::iter(tasks).boxed(), 7, config)
1368            .buffered(3)
1369            .try_collect::<Vec<_>>()
1370            .await
1371            .unwrap();
1372        let expected_fields = [
1373            "payload",
1374            lance_core::ROW_ID,
1375            lance_core::ROW_ADDR,
1376            lance_core::ROW_LAST_UPDATED_AT_VERSION,
1377            lance_core::ROW_CREATED_AT_VERSION,
1378        ];
1379        assert_eq!(
1380            batches[0]
1381                .schema()
1382                .fields()
1383                .iter()
1384                .map(|field| field.name().as_str())
1385                .collect::<Vec<_>>(),
1386            expected_fields
1387        );
1388        assert!(
1389            batches
1390                .windows(2)
1391                .all(|pair| Arc::ptr_eq(&pair[0].schema(), &pair[1].schema()))
1392        );
1393        assert!(batches.iter().all(|batch| batch.num_columns() == 5));
1394    }
1395
1396    #[tokio::test]
1397    async fn test_zip_with_different_batch_boundaries() {
1398        let left_batch =
1399            arrow_array::record_batch!(("x", Int32, (0..10).collect::<Vec<_>>())).unwrap();
1400        let right_batch =
1401            arrow_array::record_batch!(("y", Int32, (10..20).collect::<Vec<_>>())).unwrap();
1402        let left = batch_task_stream(
1403            stream::iter([Ok(left_batch.slice(0, 6)), Ok(left_batch.slice(6, 4))]).boxed(),
1404        );
1405        let right = batch_task_stream(
1406            stream::iter([Ok(right_batch.slice(0, 4)), Ok(right_batch.slice(4, 6))]).boxed(),
1407        );
1408
1409        let merged = super::merge_streams(vec![left, right])
1410            .map(|batch_task| batch_task.task)
1411            .buffered(3)
1412            .try_collect::<Vec<_>>()
1413            .await
1414            .unwrap();
1415
1416        let expected = vec![
1417            arrow_array::record_batch!(
1418                ("x", Int32, (0..4).collect::<Vec<_>>()),
1419                ("y", Int32, (10..14).collect::<Vec<_>>())
1420            )
1421            .unwrap(),
1422            arrow_array::record_batch!(
1423                ("x", Int32, (4..6).collect::<Vec<_>>()),
1424                ("y", Int32, (14..16).collect::<Vec<_>>())
1425            )
1426            .unwrap(),
1427            arrow_array::record_batch!(
1428                ("x", Int32, (6..10).collect::<Vec<_>>()),
1429                ("y", Int32, (16..20).collect::<Vec<_>>())
1430            )
1431            .unwrap(),
1432        ];
1433        assert_eq!(merged, expected);
1434    }
1435
1436    async fn check_row_id(params: ReadBatchParams, expected: impl IntoIterator<Item = u32>) {
1437        let expected = Vec::from_iter(expected);
1438
1439        for has_columns in [false, true] {
1440            for fragment_id in [0, 10] {
1441                // 100 rows across 10 batches of 10 rows
1442                let mut datagen = lance_datagen::gen_batch();
1443                if has_columns {
1444                    datagen = datagen.col("x", lance_datagen::array::rand::<Int32Type>());
1445                }
1446                let data = batch_task_stream(
1447                    datagen
1448                        .into_reader_stream(RowCount::from(10), BatchCount::from(10))
1449                        .0,
1450                );
1451
1452                let config = RowIdAndDeletesConfig {
1453                    params: params.clone(),
1454                    with_row_id: true,
1455                    with_row_addr: false,
1456                    with_row_last_updated_at_version: false,
1457                    with_row_created_at_version: false,
1458                    deletion_vector: None,
1459                    row_id_sequence: None,
1460                    last_updated_at_sequence: None,
1461                    created_at_sequence: None,
1462                    make_deletions_null: false,
1463                    total_num_rows: 100,
1464                };
1465                let stream = super::wrap_with_row_id_and_delete(data, fragment_id, config);
1466                let batches = stream.buffered(1).try_collect::<Vec<_>>().await.unwrap();
1467
1468                let mut offset = 0;
1469                let expected = expected.clone();
1470                for batch in batches {
1471                    let actual_row_ids =
1472                        batch[ROW_ID].as_primitive::<UInt64Type>().values().to_vec();
1473                    let expected_row_ids = expected[offset..offset + 10]
1474                        .iter()
1475                        .map(|row_offset| {
1476                            RowAddress::new_from_parts(fragment_id, *row_offset).into()
1477                        })
1478                        .collect::<Vec<u64>>();
1479                    assert_eq!(actual_row_ids, expected_row_ids);
1480                    offset += batch.num_rows();
1481                }
1482            }
1483        }
1484    }
1485
1486    #[tokio::test]
1487    async fn test_row_id() {
1488        let some_indices = (0..100).rev().collect::<Vec<u32>>();
1489        let some_indices_arr = UInt32Array::from(some_indices.clone());
1490        check_row_id(ReadBatchParams::RangeFull, 0..100).await;
1491        check_row_id(ReadBatchParams::Indices(some_indices_arr), some_indices).await;
1492        check_row_id(ReadBatchParams::Range(1000..1100), 1000..1100).await;
1493        check_row_id(
1494            ReadBatchParams::RangeFrom(std::ops::RangeFrom { start: 1000 }),
1495            1000..1100,
1496        )
1497        .await;
1498        check_row_id(
1499            ReadBatchParams::RangeTo(std::ops::RangeTo { end: 1000 }),
1500            0..100,
1501        )
1502        .await;
1503    }
1504
1505    #[tokio::test]
1506    async fn test_deletes() {
1507        let no_deletes: Option<Arc<DeletionVector>> = None;
1508        let no_deletes_2 = Some(Arc::new(DeletionVector::NoDeletions));
1509        let delete_some_bitmap = Some(Arc::new(DeletionVector::Bitmap(RoaringBitmap::from_iter(
1510            0..35,
1511        ))));
1512        let delete_some_set = Some(Arc::new(DeletionVector::Set((0..35).collect())));
1513
1514        for deletion_vector in [
1515            no_deletes,
1516            no_deletes_2,
1517            delete_some_bitmap,
1518            delete_some_set,
1519        ] {
1520            for has_columns in [false, true] {
1521                for with_row_id in [false, true] {
1522                    for make_deletions_null in [false, true] {
1523                        for frag_id in [0, 1] {
1524                            let has_deletions = if let Some(dv) = &deletion_vector {
1525                                !matches!(dv.as_ref(), DeletionVector::NoDeletions)
1526                            } else {
1527                                false
1528                            };
1529                            if !has_columns && !has_deletions && !with_row_id {
1530                                // This is an invalid case and should be prevented upstream,
1531                                // no meaningful work is being done!
1532                                continue;
1533                            }
1534                            if make_deletions_null && !with_row_id {
1535                                // This is an invalid case and should be prevented upstream
1536                                // we cannot make the row_id column null if it isn't present
1537                                continue;
1538                            }
1539
1540                            let mut datagen = lance_datagen::gen_batch();
1541                            if has_columns {
1542                                datagen =
1543                                    datagen.col("x", lance_datagen::array::rand::<Int32Type>());
1544                            }
1545                            // 100 rows across 10 batches of 10 rows
1546                            let data = batch_task_stream(
1547                                datagen
1548                                    .into_reader_stream(RowCount::from(10), BatchCount::from(10))
1549                                    .0,
1550                            );
1551
1552                            let config = RowIdAndDeletesConfig {
1553                                params: ReadBatchParams::RangeFull,
1554                                with_row_id,
1555                                with_row_addr: false,
1556                                with_row_last_updated_at_version: false,
1557                                with_row_created_at_version: false,
1558                                deletion_vector: deletion_vector.clone(),
1559                                row_id_sequence: None,
1560                                last_updated_at_sequence: None,
1561                                created_at_sequence: None,
1562                                make_deletions_null,
1563                                total_num_rows: 100,
1564                            };
1565                            let stream = super::wrap_with_row_id_and_delete(data, frag_id, config);
1566                            let batches = stream
1567                                .buffered(1)
1568                                .filter_map(|batch| {
1569                                    std::future::ready(
1570                                        batch
1571                                            .map(|batch| {
1572                                                if batch.num_rows() == 0 {
1573                                                    None
1574                                                } else {
1575                                                    Some(batch)
1576                                                }
1577                                            })
1578                                            .transpose(),
1579                                    )
1580                                })
1581                                .try_collect::<Vec<_>>()
1582                                .await
1583                                .unwrap();
1584
1585                            let total_num_rows =
1586                                batches.iter().map(|b| b.num_rows()).sum::<usize>();
1587                            let total_num_nulls = if make_deletions_null {
1588                                batches
1589                                    .iter()
1590                                    .map(|b| b[ROW_ID].null_count())
1591                                    .sum::<usize>()
1592                            } else {
1593                                0
1594                            };
1595                            let total_actually_deleted = total_num_nulls + (100 - total_num_rows);
1596
1597                            let expected_deletions = match &deletion_vector {
1598                                None => 0,
1599                                Some(deletion_vector) => match deletion_vector.as_ref() {
1600                                    DeletionVector::NoDeletions => 0,
1601                                    DeletionVector::Bitmap(b) => b.len() as usize,
1602                                    DeletionVector::Set(s) => s.len(),
1603                                },
1604                            };
1605                            assert_eq!(total_actually_deleted, expected_deletions);
1606                            if expected_deletions > 0 && with_row_id {
1607                                if make_deletions_null {
1608                                    // If we make deletions null we get 3 batches of all-null and then
1609                                    // a batch of half-null
1610                                    assert_eq!(
1611                                        batches[3][ROW_ID].as_primitive::<UInt64Type>().value(0),
1612                                        u64::from(RowAddress::new_from_parts(frag_id, 30))
1613                                    );
1614                                    assert_eq!(batches[3][ROW_ID].null_count(), 5);
1615                                } else {
1616                                    // If we materialize deletions the first row will be 35
1617                                    assert_eq!(
1618                                        batches[0][ROW_ID].as_primitive::<UInt64Type>().value(0),
1619                                        u64::from(RowAddress::new_from_parts(frag_id, 35))
1620                                    );
1621                                }
1622                            }
1623                            if !with_row_id {
1624                                assert!(batches[0].column_by_name(ROW_ID).is_none());
1625                            }
1626                        }
1627                    }
1628                }
1629            }
1630        }
1631    }
1632
1633    #[tokio::test]
1634    async fn test_version_column_with_deletions() {
1635        use crate::rowids::segment::U64Segment;
1636        use crate::rowids::version::{RowDatasetVersionRun, RowDatasetVersionSequence};
1637
1638        let seq = Arc::new(RowDatasetVersionSequence {
1639            runs: vec![RowDatasetVersionRun {
1640                span: U64Segment::Range(0..100),
1641                version: 42,
1642            }],
1643        });
1644
1645        let data = batch_task_stream(
1646            lance_datagen::gen_batch()
1647                .col("x", lance_datagen::array::rand::<Int32Type>())
1648                .into_reader_stream(RowCount::from(10), BatchCount::from(10))
1649                .0,
1650        );
1651
1652        let config = RowIdAndDeletesConfig {
1653            params: ReadBatchParams::RangeFull,
1654            with_row_id: true,
1655            with_row_addr: false,
1656            with_row_last_updated_at_version: false,
1657            with_row_created_at_version: true,
1658            deletion_vector: Some(Arc::new(DeletionVector::Bitmap(RoaringBitmap::from_iter(
1659                0..35,
1660            )))),
1661            row_id_sequence: None,
1662            last_updated_at_sequence: None,
1663            created_at_sequence: Some(seq),
1664            make_deletions_null: false,
1665            total_num_rows: 100,
1666        };
1667        let stream = super::wrap_with_row_id_and_delete(data, 0, config);
1668        let batches: Vec<_> = stream
1669            .buffered(1)
1670            .try_filter(|b| std::future::ready(b.num_rows() > 0))
1671            .try_collect()
1672            .await
1673            .unwrap();
1674
1675        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1676        assert_eq!(total_rows, 65);
1677
1678        for batch in &batches {
1679            let versions = batch
1680                .column_by_name("_row_created_at_version")
1681                .unwrap()
1682                .as_primitive::<UInt64Type>()
1683                .values();
1684            assert!(versions.iter().all(|&v| v == 42));
1685        }
1686    }
1687
1688    #[tokio::test]
1689    async fn test_version_column_multi_run() {
1690        use crate::rowids::segment::U64Segment;
1691        use crate::rowids::version::{RowDatasetVersionRun, RowDatasetVersionSequence};
1692
1693        // Exercise the worst-case created-at shape: one run per row.
1694        let created_seq = Arc::new(RowDatasetVersionSequence {
1695            runs: (0..100)
1696                .map(|position| RowDatasetVersionRun {
1697                    span: U64Segment::Range(position..position + 1),
1698                    version: 1_000 + position,
1699                })
1700                .collect(),
1701        });
1702        // Also exercise irregular boundaries for last-updated-at.
1703        let last_updated_seq = Arc::new(RowDatasetVersionSequence {
1704            runs: vec![
1705                RowDatasetVersionRun {
1706                    span: U64Segment::Range(0..7),
1707                    version: 11,
1708                },
1709                RowDatasetVersionRun {
1710                    span: U64Segment::Range(7..20),
1711                    version: 22,
1712                },
1713                RowDatasetVersionRun {
1714                    span: U64Segment::Range(20..21),
1715                    version: 33,
1716                },
1717                RowDatasetVersionRun {
1718                    span: U64Segment::Range(21..50),
1719                    version: 44,
1720                },
1721                RowDatasetVersionRun {
1722                    span: U64Segment::Range(50..100),
1723                    version: 55,
1724                },
1725            ],
1726        });
1727
1728        // Delete 0..20 and 60..80 (spans run boundary).
1729        // Survivors: 20..40 (v1), 40..60 (v2), 80..100 (v3) = 60 rows
1730        let mut deletions = RoaringBitmap::from_iter(0..20);
1731        deletions.extend(60..80);
1732
1733        let data = batch_task_stream(
1734            lance_datagen::gen_batch()
1735                .col("x", lance_datagen::array::rand::<Int32Type>())
1736                .into_reader_stream(RowCount::from(10), BatchCount::from(10))
1737                .0,
1738        );
1739
1740        let config = RowIdAndDeletesConfig {
1741            params: ReadBatchParams::RangeFull,
1742            with_row_id: true,
1743            with_row_addr: false,
1744            with_row_last_updated_at_version: true,
1745            with_row_created_at_version: true,
1746            deletion_vector: Some(Arc::new(DeletionVector::Bitmap(deletions))),
1747            row_id_sequence: None,
1748            last_updated_at_sequence: Some(last_updated_seq),
1749            created_at_sequence: Some(created_seq),
1750            make_deletions_null: false,
1751            total_num_rows: 100,
1752        };
1753        let stream = super::wrap_with_row_id_and_delete(data, 0, config);
1754        let batches: Vec<_> = stream
1755            .buffered(8)
1756            .try_filter(|b| std::future::ready(b.num_rows() > 0))
1757            .try_collect()
1758            .await
1759            .unwrap();
1760
1761        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1762        assert_eq!(total_rows, 60);
1763
1764        let created_versions: Vec<u64> = batches
1765            .iter()
1766            .flat_map(|b| {
1767                b.column_by_name("_row_created_at_version")
1768                    .unwrap()
1769                    .as_primitive::<UInt64Type>()
1770                    .values()
1771                    .to_vec()
1772            })
1773            .collect();
1774        let last_updated_versions: Vec<u64> = batches
1775            .iter()
1776            .flat_map(|b| {
1777                b.column_by_name("_row_last_updated_at_version")
1778                    .unwrap()
1779                    .as_primitive::<UInt64Type>()
1780                    .values()
1781                    .to_vec()
1782            })
1783            .collect();
1784        let surviving_positions: Vec<u64> = (20..60).chain(80..100).collect();
1785        let expected_created: Vec<u64> = surviving_positions
1786            .iter()
1787            .map(|position| 1_000 + position)
1788            .collect();
1789        let expected_last_updated: Vec<u64> = surviving_positions
1790            .iter()
1791            .map(|position| match position {
1792                0..=6 => 11,
1793                7..=19 => 22,
1794                20 => 33,
1795                21..=49 => 44,
1796                _ => 55,
1797            })
1798            .collect();
1799
1800        assert_eq!(created_versions, expected_created);
1801        assert_eq!(last_updated_versions, expected_last_updated);
1802    }
1803
1804    #[tokio::test]
1805    async fn test_version_column_with_unsorted_indices_across_batches() {
1806        use crate::rowids::segment::U64Segment;
1807        use crate::rowids::version::{RowDatasetVersionRun, RowDatasetVersionSequence};
1808
1809        let sequence = Arc::new(RowDatasetVersionSequence {
1810            runs: (0..10)
1811                .map(|position| RowDatasetVersionRun {
1812                    span: U64Segment::Range(position..position + 1),
1813                    version: 100 + position,
1814                })
1815                .collect(),
1816        });
1817        let indices = UInt32Array::from(vec![8, 2, 9, 1, 6]);
1818        let batches = [2, 2, 1].into_iter().map(|num_rows| ReadBatchTask {
1819            num_rows,
1820            task: std::future::ready(Ok(arrow_array::record_batch!((
1821                "x",
1822                Int32,
1823                vec![0; num_rows as usize]
1824            ))
1825            .unwrap()))
1826            .boxed(),
1827        });
1828        let config = RowIdAndDeletesConfig {
1829            params: ReadBatchParams::Indices(indices.clone()),
1830            with_row_id: false,
1831            with_row_addr: false,
1832            with_row_last_updated_at_version: true,
1833            with_row_created_at_version: false,
1834            deletion_vector: None,
1835            row_id_sequence: None,
1836            last_updated_at_sequence: Some(sequence),
1837            created_at_sequence: None,
1838            make_deletions_null: false,
1839            total_num_rows: 10,
1840        };
1841
1842        let actual = super::wrap_with_row_id_and_delete(stream::iter(batches).boxed(), 0, config)
1843            .buffered(3)
1844            .try_collect::<Vec<_>>()
1845            .await
1846            .unwrap()
1847            .iter()
1848            .flat_map(|batch| {
1849                batch["_row_last_updated_at_version"]
1850                    .as_primitive::<UInt64Type>()
1851                    .values()
1852            })
1853            .copied()
1854            .collect::<Vec<_>>();
1855        let expected = indices
1856            .values()
1857            .iter()
1858            .map(|position| 100 + u64::from(*position))
1859            .collect::<Vec<_>>();
1860        assert_eq!(actual, expected);
1861    }
1862
1863    #[test]
1864    fn test_apply_version_column_direct_call_fallback() {
1865        use crate::rowids::segment::U64Segment;
1866        use crate::rowids::version::{RowDatasetVersionRun, RowDatasetVersionSequence};
1867
1868        let sequence = Arc::new(RowDatasetVersionSequence {
1869            runs: (0..5)
1870                .map(|position| RowDatasetVersionRun {
1871                    span: U64Segment::Range(position..position + 1),
1872                    version: 10 + position,
1873                })
1874                .collect(),
1875        });
1876        let config = RowIdAndDeletesConfig {
1877            params: ReadBatchParams::Indices(UInt32Array::from(vec![4, 1, 3])),
1878            with_row_id: false,
1879            with_row_addr: false,
1880            with_row_last_updated_at_version: true,
1881            with_row_created_at_version: false,
1882            deletion_vector: None,
1883            row_id_sequence: None,
1884            last_updated_at_sequence: Some(sequence),
1885            created_at_sequence: None,
1886            make_deletions_null: false,
1887            total_num_rows: 5,
1888        };
1889        let batch = arrow_array::record_batch!(("x", Int32, vec![0; 3])).unwrap();
1890
1891        let actual = super::apply_row_id_and_deletes(batch, 0, 0, &config).unwrap();
1892        assert_eq!(
1893            actual["_row_last_updated_at_version"]
1894                .as_primitive::<UInt64Type>()
1895                .values(),
1896            &[14, 11, 13]
1897        );
1898    }
1899}