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::{fmt, sync::Arc};
5
6use arrow_array::{BooleanArray, RecordBatch, RecordBatchOptions, UInt64Array, make_array};
7use arrow_buffer::NullBuffer;
8use futures::{
9    FutureExt, Stream, StreamExt,
10    future::{BoxFuture, Shared},
11    stream::{BoxStream, FuturesOrdered},
12};
13use lance_arrow::RecordBatchExt;
14use lance_core::{
15    Error, ROW_ADDR, ROW_ADDR_FIELD, ROW_CREATED_AT_VERSION_FIELD, ROW_ID, ROW_ID_FIELD,
16    ROW_LAST_UPDATED_AT_VERSION_FIELD, Result,
17    utils::{address::RowAddress, deletion::DeletionVector},
18};
19use lance_io::ReadBatchParams;
20use tracing::instrument;
21
22use crate::rowids::RowIdSequence;
23
24pub type ReadBatchFut = BoxFuture<'static, Result<RecordBatch>>;
25/// A task, emitted by a file reader, that will produce a batch (of the
26/// given size)
27pub struct ReadBatchTask {
28    pub task: ReadBatchFut,
29    pub num_rows: u32,
30}
31pub type ReadBatchTaskStream = BoxStream<'static, ReadBatchTask>;
32pub type ReadBatchFutStream = BoxStream<'static, ReadBatchFut>;
33
34type SharedReadBatchFut = Shared<BoxFuture<'static, std::result::Result<RecordBatch, Arc<Error>>>>;
35
36#[derive(Debug)]
37struct SharedReadError(Arc<Error>);
38
39impl fmt::Display for SharedReadError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        self.0.fmt(f)
42    }
43}
44
45impl std::error::Error for SharedReadError {
46    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
47        Some(self.0.as_ref())
48    }
49}
50
51struct PendingReadBatch {
52    task: Option<ReadBatchFut>,
53    shared_task: Option<SharedReadBatchFut>,
54    offset: u32,
55    num_rows: u32,
56}
57
58impl PendingReadBatch {
59    fn new(task: ReadBatchTask) -> Self {
60        Self {
61            task: Some(task.task),
62            shared_task: None,
63            offset: 0,
64            num_rows: task.num_rows,
65        }
66    }
67
68    fn take(&mut self, num_rows: u32) -> ReadBatchFut {
69        debug_assert!(num_rows <= self.num_rows);
70
71        if self.offset == 0 && num_rows == self.num_rows && self.shared_task.is_none() {
72            self.num_rows = 0;
73            let Some(task) = self.task.take() else {
74                return async {
75                    Err(Error::internal(
76                        "missing read task while merging aligned streams".to_string(),
77                    ))
78                }
79                .boxed();
80            };
81            return task;
82        }
83
84        let shared_task = self
85            .shared_task
86            .get_or_insert_with(|| {
87                let task = self.task.take();
88                async move {
89                    let Some(task) = task else {
90                        return Err(Arc::new(Error::internal(
91                            "missing read task while splitting a merged stream".to_string(),
92                        )));
93                    };
94                    task.await.map_err(Arc::new)
95                }
96                .boxed()
97                .shared()
98            })
99            .clone();
100        let offset = self.offset;
101        self.offset += num_rows;
102        self.num_rows -= num_rows;
103
104        async move {
105            match shared_task.await {
106                Ok(batch) => Ok(batch.slice(offset as usize, num_rows as usize)),
107                Err(error) => Err(Error::wrapped(Box::new(SharedReadError(error)))),
108            }
109        }
110        .boxed()
111    }
112}
113
114struct MergeStream {
115    streams: Vec<ReadBatchTaskStream>,
116    pending: Vec<Option<PendingReadBatch>>,
117    index: usize,
118}
119
120impl MergeStream {
121    fn emit(&mut self) -> ReadBatchTask {
122        let num_rows = self
123            .pending
124            .iter()
125            .filter_map(|pending| pending.as_ref().map(|pending| pending.num_rows))
126            .min()
127            .unwrap_or_default();
128        let mut batches = FuturesOrdered::new();
129        for pending in &mut self.pending {
130            let Some(pending_batch) = pending.as_mut() else {
131                continue;
132            };
133            batches.push_back(pending_batch.take(num_rows));
134            if pending_batch.num_rows == 0 {
135                *pending = None;
136            }
137        }
138        let task = async move {
139            let Some(first) = batches.next().await else {
140                return Err(Error::internal(
141                    "cannot merge an empty set of read batches".to_string(),
142                ));
143            };
144            let mut batch = first?;
145            while let Some(next) = batches.next().await {
146                let next = next?;
147                batch = batch.merge(&next)?;
148            }
149            Ok(batch)
150        }
151        .boxed();
152        ReadBatchTask { task, num_rows }
153    }
154}
155
156impl Stream for MergeStream {
157    type Item = ReadBatchTask;
158
159    fn poll_next(
160        mut self: std::pin::Pin<&mut Self>,
161        cx: &mut std::task::Context<'_>,
162    ) -> std::task::Poll<Option<Self::Item>> {
163        loop {
164            if self.pending.iter().all(Option::is_some) {
165                return std::task::Poll::Ready(Some(self.emit()));
166            }
167
168            let index = self.index;
169            if self.pending[index].is_some() {
170                self.index = (index + 1) % self.streams.len();
171                continue;
172            }
173            match self.streams[index].poll_next_unpin(cx) {
174                std::task::Poll::Ready(Some(batch_task)) => {
175                    self.pending[index] = Some(PendingReadBatch::new(batch_task));
176                    self.index = (index + 1) % self.streams.len();
177                }
178                std::task::Poll::Ready(None) => {
179                    return std::task::Poll::Ready(None);
180                }
181                std::task::Poll::Pending => {
182                    return std::task::Poll::Pending;
183                }
184            }
185        }
186    }
187}
188
189/// Given multiple streams of batch tasks, merge them into a single stream
190///
191/// This pulls one batch from each stream and then combines the columns from
192/// all of the batches into a single batch.  The order of the batches in the
193/// streams is maintained and the merged batch columns will be in order from first
194/// to last stream. If the streams use different batch boundaries then batches are
195/// sliced so each merged output remains row-aligned.
196///
197/// This stream ends as soon as any of the input streams ends (we do not
198/// verify that the other input streams are finished as well)
199pub fn merge_streams(streams: Vec<ReadBatchTaskStream>) -> ReadBatchTaskStream {
200    if streams.is_empty() {
201        return futures::stream::empty().boxed();
202    }
203    let pending = (0..streams.len()).map(|_| None).collect();
204    MergeStream {
205        streams,
206        pending,
207        index: 0,
208    }
209    .boxed()
210}
211
212/// Apply a mask to the batch, where rows are "deleted" by the _rowid column null.
213///
214/// This is used partly as a performance optimization (cheaper to null than to filter)
215/// but also because there are cases where we want to load the physical rows.  For example,
216/// we may be replacing a column based on some UDF and we want to provide a value for the
217/// deleted rows to ensure the fragments are aligned.
218fn apply_deletions_as_nulls(batch: RecordBatch, mask: &BooleanArray) -> Result<RecordBatch> {
219    // Transform mask into null buffer. Null means deleted, though note that
220    // null buffers are actually validity buffers, so True means not null
221    // and thus not deleted.
222    let mask_buffer = NullBuffer::new(mask.values().clone());
223
224    if mask_buffer.null_count() == 0 {
225        // No rows are deleted
226        return Ok(batch);
227    }
228
229    // For each column convert to data
230    let new_columns = batch
231        .schema()
232        .fields()
233        .iter()
234        .zip(batch.columns())
235        .map(|(field, col)| {
236            if field.name() == ROW_ID || field.name() == ROW_ADDR {
237                let col_data = col.to_data();
238                // If it already has a validity bitmap, then AND it with the mask.
239                // Otherwise, use the boolean buffer as the mask.
240                let null_buffer = NullBuffer::union(col_data.nulls(), Some(&mask_buffer));
241
242                Ok(col_data
243                    .into_builder()
244                    .null_bit_buffer(null_buffer.map(|b| b.buffer().clone()))
245                    .build()
246                    .map(make_array)?)
247            } else {
248                Ok(col.clone())
249            }
250        })
251        .collect::<Result<Vec<_>>>()?;
252
253    Ok(RecordBatch::try_new_with_options(
254        batch.schema(),
255        new_columns,
256        &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
257    )?)
258}
259
260/// Extract version values for a batch selection by binary-searching over
261/// precomputed RLE run offsets. Single-run fragments (the common case)
262/// take the O(1) fast path.
263fn version_values_for_selection(
264    sequence: &crate::rowids::version::RowDatasetVersionSequence,
265    params: &ReadBatchParams,
266    batch_offset: u32,
267    num_rows: u32,
268) -> Result<Vec<u64>> {
269    let selection = params
270        .slice(batch_offset as usize, num_rows as usize)
271        .unwrap()
272        .to_ranges()
273        .unwrap();
274
275    if sequence.runs.len() == 1 {
276        return Ok(vec![sequence.runs[0].version(); num_rows as usize]);
277    }
278
279    let mut versions = Vec::with_capacity(num_rows as usize);
280    let run_offsets: Vec<usize> = sequence
281        .runs
282        .iter()
283        .scan(0usize, |acc, run| {
284            let start = *acc;
285            *acc += run.len();
286            Some(start)
287        })
288        .collect();
289    let total_len: usize = sequence.runs.iter().map(|r| r.len()).sum();
290
291    for r in &selection {
292        for pos in r.start..r.end {
293            let pos = pos as usize;
294            if pos >= total_len {
295                return Err(lance_core::Error::internal(format!(
296                    "version column position {} out of range (total_len={})",
297                    pos, total_len
298                )));
299            }
300            let run_idx = match run_offsets.binary_search(&pos) {
301                Ok(idx) => idx,
302                Err(idx) => idx - 1,
303            };
304            versions.push(sequence.runs[run_idx].version());
305        }
306    }
307    Ok(versions)
308}
309
310/// Configuration needed to apply row ids and deletions to a batch
311#[derive(Debug)]
312pub struct RowIdAndDeletesConfig {
313    /// The row ids that were requested
314    pub params: ReadBatchParams,
315    /// Whether to include the row id column in the final batch
316    pub with_row_id: bool,
317    /// Whether to include the row address column in the final batch
318    pub with_row_addr: bool,
319    /// Whether to include the last updated at version column in the final batch
320    pub with_row_last_updated_at_version: bool,
321    /// Whether to include the created at version column in the final batch
322    pub with_row_created_at_version: bool,
323    /// An optional deletion vector to apply to the batch
324    pub deletion_vector: Option<Arc<DeletionVector>>,
325    /// An optional row id sequence to use for the row id column.
326    pub row_id_sequence: Option<Arc<RowIdSequence>>,
327    /// The last_updated_at version sequence
328    pub last_updated_at_sequence: Option<Arc<crate::rowids::version::RowDatasetVersionSequence>>,
329    /// The created_at version sequence
330    pub created_at_sequence: Option<Arc<crate::rowids::version::RowDatasetVersionSequence>>,
331    /// Whether to make deleted rows null instead of filtering them out
332    pub make_deletions_null: bool,
333    /// The total number of rows that will be loaded
334    ///
335    /// This is needed to convert ReadbatchParams::RangeTo into a valid range
336    pub total_num_rows: u32,
337}
338
339impl RowIdAndDeletesConfig {
340    fn has_system_cols(&self) -> bool {
341        self.with_row_id
342            || self.with_row_addr
343            || self.with_row_last_updated_at_version
344            || self.with_row_created_at_version
345    }
346}
347
348#[instrument(level = "debug", skip_all)]
349pub fn apply_row_id_and_deletes(
350    batch: RecordBatch,
351    batch_offset: u32,
352    fragment_id: u32,
353    config: &RowIdAndDeletesConfig,
354) -> Result<RecordBatch> {
355    let mut deletion_vector = config.deletion_vector.as_ref();
356    // Convert Some(NoDeletions) into None to simplify logic below
357    if let Some(deletion_vector_inner) = deletion_vector
358        && matches!(deletion_vector_inner.as_ref(), DeletionVector::NoDeletions)
359    {
360        deletion_vector = None;
361    }
362    let has_deletions = deletion_vector.is_some();
363    debug_assert!(batch.num_columns() > 0 || config.has_system_cols() || has_deletions);
364
365    // If row id sequence is None, then row id IS row address.
366    let should_fetch_row_addr = config.with_row_addr
367        || (config.with_row_id && config.row_id_sequence.is_none())
368        || has_deletions;
369
370    let num_rows = batch.num_rows() as u32;
371
372    let row_addrs =
373        if should_fetch_row_addr {
374            let _rowaddrs = tracing::span!(tracing::Level::DEBUG, "fetch_row_addrs").entered();
375            let mut row_addrs = Vec::with_capacity(num_rows as usize);
376            for offset_range in config
377                .params
378                .slice(batch_offset as usize, num_rows as usize)
379                .unwrap()
380                .iter_offset_ranges()?
381            {
382                row_addrs.extend(offset_range.map(|row_offset| {
383                    u64::from(RowAddress::new_from_parts(fragment_id, row_offset))
384                }));
385            }
386
387            Some(Arc::new(UInt64Array::from(row_addrs)))
388        } else {
389            None
390        };
391
392    let row_ids = if config.with_row_id {
393        let _rowids = tracing::span!(tracing::Level::DEBUG, "fetch_row_ids").entered();
394        if let Some(row_id_sequence) = &config.row_id_sequence {
395            let selection = config
396                .params
397                .slice(batch_offset as usize, num_rows as usize)
398                .unwrap()
399                .to_ranges()
400                .unwrap();
401            let row_ids = row_id_sequence
402                .select(
403                    selection
404                        .iter()
405                        .flat_map(|r| r.start as usize..r.end as usize),
406                )
407                .collect::<UInt64Array>();
408            Some(Arc::new(row_ids))
409        } else {
410            // If we don't have a row id sequence, can assume the row ids are
411            // the same as the row addresses.
412            row_addrs.clone()
413        }
414    } else {
415        None
416    };
417
418    let span = tracing::span!(tracing::Level::DEBUG, "apply_deletions");
419    let _enter = span.enter();
420    let deletion_mask = deletion_vector.and_then(|v| {
421        let row_addrs: &[u64] = row_addrs.as_ref().unwrap().values();
422        v.build_predicate(row_addrs.iter())
423    });
424
425    let batch = if config.with_row_id {
426        let row_id_arr = row_ids.unwrap();
427        batch.try_with_column(ROW_ID_FIELD.clone(), row_id_arr)?
428    } else {
429        batch
430    };
431
432    let batch = if config.with_row_addr {
433        let row_addr_arr = row_addrs.unwrap();
434        batch.try_with_column(ROW_ADDR_FIELD.clone(), row_addr_arr)?
435    } else {
436        batch
437    };
438
439    // Add version columns if requested
440    let batch = if config.with_row_last_updated_at_version || config.with_row_created_at_version {
441        let mut batch = batch;
442
443        if config.with_row_last_updated_at_version {
444            let version_arr = if let Some(sequence) = &config.last_updated_at_sequence {
445                Arc::new(UInt64Array::from(version_values_for_selection(
446                    sequence,
447                    &config.params,
448                    batch_offset,
449                    num_rows,
450                )?))
451            } else {
452                // Default to version 1 if sequence not provided
453                Arc::new(UInt64Array::from(vec![1u64; num_rows as usize]))
454            };
455            batch =
456                batch.try_with_column(ROW_LAST_UPDATED_AT_VERSION_FIELD.clone(), version_arr)?;
457        }
458
459        if config.with_row_created_at_version {
460            let version_arr = if let Some(sequence) = &config.created_at_sequence {
461                Arc::new(UInt64Array::from(version_values_for_selection(
462                    sequence,
463                    &config.params,
464                    batch_offset,
465                    num_rows,
466                )?))
467            } else {
468                // Default to version 1 if sequence not provided
469                Arc::new(UInt64Array::from(vec![1u64; num_rows as usize]))
470            };
471            batch = batch.try_with_column(ROW_CREATED_AT_VERSION_FIELD.clone(), version_arr)?;
472        }
473
474        batch
475    } else {
476        batch
477    };
478
479    match (deletion_mask, config.make_deletions_null) {
480        (None, _) => Ok(batch),
481        (Some(mask), false) => Ok(arrow::compute::filter_record_batch(&batch, &mask)?),
482        (Some(mask), true) => Ok(apply_deletions_as_nulls(batch, &mask)?),
483    }
484}
485
486/// Given a stream of batch tasks this function will add a row ids column (if requested)
487/// and also apply a deletions vector to the batch.
488///
489/// This converts from BatchTaskStream to BatchFutStream because, if we are applying a
490/// deletion vector, it is impossible to know how many output rows we will have.
491pub fn wrap_with_row_id_and_delete(
492    stream: ReadBatchTaskStream,
493    fragment_id: u32,
494    config: RowIdAndDeletesConfig,
495) -> ReadBatchFutStream {
496    let config = Arc::new(config);
497    let mut offset = 0;
498    stream
499        .map(move |batch_task| {
500            let config = config.clone();
501            let this_offset = offset;
502            let num_rows = batch_task.num_rows;
503            offset += num_rows;
504            batch_task
505                .task
506                .map(move |batch| {
507                    apply_row_id_and_deletes(batch?, this_offset, fragment_id, config.as_ref())
508                })
509                .boxed()
510        })
511        .boxed()
512}
513
514#[cfg(test)]
515mod tests {
516    use std::sync::Arc;
517
518    use arrow::{array::AsArray, datatypes::UInt64Type};
519    use arrow_array::{RecordBatch, UInt32Array, types::Int32Type};
520    use arrow_schema::ArrowError;
521    use futures::{
522        FutureExt, StreamExt, TryStreamExt,
523        stream::{self, BoxStream},
524    };
525    use lance_core::{
526        ROW_ID,
527        utils::{address::RowAddress, deletion::DeletionVector},
528    };
529    use lance_datagen::{BatchCount, RowCount};
530    use lance_io::{ReadBatchParams, stream::arrow_stream_to_lance_stream};
531    use roaring::RoaringBitmap;
532
533    use crate::utils::stream::ReadBatchTask;
534
535    use super::RowIdAndDeletesConfig;
536
537    fn batch_task_stream(
538        datagen_stream: BoxStream<'static, std::result::Result<RecordBatch, ArrowError>>,
539    ) -> super::ReadBatchTaskStream {
540        arrow_stream_to_lance_stream(datagen_stream)
541            .map(|batch| ReadBatchTask {
542                num_rows: batch.as_ref().unwrap().num_rows() as u32,
543                task: std::future::ready(batch).boxed(),
544            })
545            .boxed()
546    }
547
548    #[tokio::test]
549    async fn test_basic_zip() {
550        let left = batch_task_stream(
551            lance_datagen::gen_batch()
552                .col("x", lance_datagen::array::step::<Int32Type>())
553                .into_reader_stream(RowCount::from(100), BatchCount::from(10))
554                .0,
555        );
556        let right = batch_task_stream(
557            lance_datagen::gen_batch()
558                .col("y", lance_datagen::array::step::<Int32Type>())
559                .into_reader_stream(RowCount::from(100), BatchCount::from(10))
560                .0,
561        );
562
563        let merged = super::merge_streams(vec![left, right])
564            .map(|batch_task| batch_task.task)
565            .buffered(1)
566            .try_collect::<Vec<_>>()
567            .await
568            .unwrap();
569
570        let expected = lance_datagen::gen_batch()
571            .col("x", lance_datagen::array::step::<Int32Type>())
572            .col("y", lance_datagen::array::step::<Int32Type>())
573            .into_reader_rows(RowCount::from(100), BatchCount::from(10))
574            .collect::<Result<Vec<_>, ArrowError>>()
575            .unwrap();
576        assert_eq!(merged, expected);
577    }
578
579    #[tokio::test]
580    async fn test_zip_with_different_batch_boundaries() {
581        let left_batch =
582            arrow_array::record_batch!(("x", Int32, (0..10).collect::<Vec<_>>())).unwrap();
583        let right_batch =
584            arrow_array::record_batch!(("y", Int32, (10..20).collect::<Vec<_>>())).unwrap();
585        let left = batch_task_stream(
586            stream::iter([Ok(left_batch.slice(0, 6)), Ok(left_batch.slice(6, 4))]).boxed(),
587        );
588        let right = batch_task_stream(
589            stream::iter([Ok(right_batch.slice(0, 4)), Ok(right_batch.slice(4, 6))]).boxed(),
590        );
591
592        let merged = super::merge_streams(vec![left, right])
593            .map(|batch_task| batch_task.task)
594            .buffered(3)
595            .try_collect::<Vec<_>>()
596            .await
597            .unwrap();
598
599        let expected = vec![
600            arrow_array::record_batch!(
601                ("x", Int32, (0..4).collect::<Vec<_>>()),
602                ("y", Int32, (10..14).collect::<Vec<_>>())
603            )
604            .unwrap(),
605            arrow_array::record_batch!(
606                ("x", Int32, (4..6).collect::<Vec<_>>()),
607                ("y", Int32, (14..16).collect::<Vec<_>>())
608            )
609            .unwrap(),
610            arrow_array::record_batch!(
611                ("x", Int32, (6..10).collect::<Vec<_>>()),
612                ("y", Int32, (16..20).collect::<Vec<_>>())
613            )
614            .unwrap(),
615        ];
616        assert_eq!(merged, expected);
617    }
618
619    async fn check_row_id(params: ReadBatchParams, expected: impl IntoIterator<Item = u32>) {
620        let expected = Vec::from_iter(expected);
621
622        for has_columns in [false, true] {
623            for fragment_id in [0, 10] {
624                // 100 rows across 10 batches of 10 rows
625                let mut datagen = lance_datagen::gen_batch();
626                if has_columns {
627                    datagen = datagen.col("x", lance_datagen::array::rand::<Int32Type>());
628                }
629                let data = batch_task_stream(
630                    datagen
631                        .into_reader_stream(RowCount::from(10), BatchCount::from(10))
632                        .0,
633                );
634
635                let config = RowIdAndDeletesConfig {
636                    params: params.clone(),
637                    with_row_id: true,
638                    with_row_addr: false,
639                    with_row_last_updated_at_version: false,
640                    with_row_created_at_version: false,
641                    deletion_vector: None,
642                    row_id_sequence: None,
643                    last_updated_at_sequence: None,
644                    created_at_sequence: None,
645                    make_deletions_null: false,
646                    total_num_rows: 100,
647                };
648                let stream = super::wrap_with_row_id_and_delete(data, fragment_id, config);
649                let batches = stream.buffered(1).try_collect::<Vec<_>>().await.unwrap();
650
651                let mut offset = 0;
652                let expected = expected.clone();
653                for batch in batches {
654                    let actual_row_ids =
655                        batch[ROW_ID].as_primitive::<UInt64Type>().values().to_vec();
656                    let expected_row_ids = expected[offset..offset + 10]
657                        .iter()
658                        .map(|row_offset| {
659                            RowAddress::new_from_parts(fragment_id, *row_offset).into()
660                        })
661                        .collect::<Vec<u64>>();
662                    assert_eq!(actual_row_ids, expected_row_ids);
663                    offset += batch.num_rows();
664                }
665            }
666        }
667    }
668
669    #[tokio::test]
670    async fn test_row_id() {
671        let some_indices = (0..100).rev().collect::<Vec<u32>>();
672        let some_indices_arr = UInt32Array::from(some_indices.clone());
673        check_row_id(ReadBatchParams::RangeFull, 0..100).await;
674        check_row_id(ReadBatchParams::Indices(some_indices_arr), some_indices).await;
675        check_row_id(ReadBatchParams::Range(1000..1100), 1000..1100).await;
676        check_row_id(
677            ReadBatchParams::RangeFrom(std::ops::RangeFrom { start: 1000 }),
678            1000..1100,
679        )
680        .await;
681        check_row_id(
682            ReadBatchParams::RangeTo(std::ops::RangeTo { end: 1000 }),
683            0..100,
684        )
685        .await;
686    }
687
688    #[tokio::test]
689    async fn test_deletes() {
690        let no_deletes: Option<Arc<DeletionVector>> = None;
691        let no_deletes_2 = Some(Arc::new(DeletionVector::NoDeletions));
692        let delete_some_bitmap = Some(Arc::new(DeletionVector::Bitmap(RoaringBitmap::from_iter(
693            0..35,
694        ))));
695        let delete_some_set = Some(Arc::new(DeletionVector::Set((0..35).collect())));
696
697        for deletion_vector in [
698            no_deletes,
699            no_deletes_2,
700            delete_some_bitmap,
701            delete_some_set,
702        ] {
703            for has_columns in [false, true] {
704                for with_row_id in [false, true] {
705                    for make_deletions_null in [false, true] {
706                        for frag_id in [0, 1] {
707                            let has_deletions = if let Some(dv) = &deletion_vector {
708                                !matches!(dv.as_ref(), DeletionVector::NoDeletions)
709                            } else {
710                                false
711                            };
712                            if !has_columns && !has_deletions && !with_row_id {
713                                // This is an invalid case and should be prevented upstream,
714                                // no meaningful work is being done!
715                                continue;
716                            }
717                            if make_deletions_null && !with_row_id {
718                                // This is an invalid case and should be prevented upstream
719                                // we cannot make the row_id column null if it isn't present
720                                continue;
721                            }
722
723                            let mut datagen = lance_datagen::gen_batch();
724                            if has_columns {
725                                datagen =
726                                    datagen.col("x", lance_datagen::array::rand::<Int32Type>());
727                            }
728                            // 100 rows across 10 batches of 10 rows
729                            let data = batch_task_stream(
730                                datagen
731                                    .into_reader_stream(RowCount::from(10), BatchCount::from(10))
732                                    .0,
733                            );
734
735                            let config = RowIdAndDeletesConfig {
736                                params: ReadBatchParams::RangeFull,
737                                with_row_id,
738                                with_row_addr: false,
739                                with_row_last_updated_at_version: false,
740                                with_row_created_at_version: false,
741                                deletion_vector: deletion_vector.clone(),
742                                row_id_sequence: None,
743                                last_updated_at_sequence: None,
744                                created_at_sequence: None,
745                                make_deletions_null,
746                                total_num_rows: 100,
747                            };
748                            let stream = super::wrap_with_row_id_and_delete(data, frag_id, config);
749                            let batches = stream
750                                .buffered(1)
751                                .filter_map(|batch| {
752                                    std::future::ready(
753                                        batch
754                                            .map(|batch| {
755                                                if batch.num_rows() == 0 {
756                                                    None
757                                                } else {
758                                                    Some(batch)
759                                                }
760                                            })
761                                            .transpose(),
762                                    )
763                                })
764                                .try_collect::<Vec<_>>()
765                                .await
766                                .unwrap();
767
768                            let total_num_rows =
769                                batches.iter().map(|b| b.num_rows()).sum::<usize>();
770                            let total_num_nulls = if make_deletions_null {
771                                batches
772                                    .iter()
773                                    .map(|b| b[ROW_ID].null_count())
774                                    .sum::<usize>()
775                            } else {
776                                0
777                            };
778                            let total_actually_deleted = total_num_nulls + (100 - total_num_rows);
779
780                            let expected_deletions = match &deletion_vector {
781                                None => 0,
782                                Some(deletion_vector) => match deletion_vector.as_ref() {
783                                    DeletionVector::NoDeletions => 0,
784                                    DeletionVector::Bitmap(b) => b.len() as usize,
785                                    DeletionVector::Set(s) => s.len(),
786                                },
787                            };
788                            assert_eq!(total_actually_deleted, expected_deletions);
789                            if expected_deletions > 0 && with_row_id {
790                                if make_deletions_null {
791                                    // If we make deletions null we get 3 batches of all-null and then
792                                    // a batch of half-null
793                                    assert_eq!(
794                                        batches[3][ROW_ID].as_primitive::<UInt64Type>().value(0),
795                                        u64::from(RowAddress::new_from_parts(frag_id, 30))
796                                    );
797                                    assert_eq!(batches[3][ROW_ID].null_count(), 5);
798                                } else {
799                                    // If we materialize deletions the first row will be 35
800                                    assert_eq!(
801                                        batches[0][ROW_ID].as_primitive::<UInt64Type>().value(0),
802                                        u64::from(RowAddress::new_from_parts(frag_id, 35))
803                                    );
804                                }
805                            }
806                            if !with_row_id {
807                                assert!(batches[0].column_by_name(ROW_ID).is_none());
808                            }
809                        }
810                    }
811                }
812            }
813        }
814    }
815
816    #[tokio::test]
817    async fn test_version_column_with_deletions() {
818        use crate::rowids::segment::U64Segment;
819        use crate::rowids::version::{RowDatasetVersionRun, RowDatasetVersionSequence};
820
821        let seq = Arc::new(RowDatasetVersionSequence {
822            runs: vec![RowDatasetVersionRun {
823                span: U64Segment::Range(0..100),
824                version: 42,
825            }],
826        });
827
828        let data = batch_task_stream(
829            lance_datagen::gen_batch()
830                .col("x", lance_datagen::array::rand::<Int32Type>())
831                .into_reader_stream(RowCount::from(10), BatchCount::from(10))
832                .0,
833        );
834
835        let config = RowIdAndDeletesConfig {
836            params: ReadBatchParams::RangeFull,
837            with_row_id: true,
838            with_row_addr: false,
839            with_row_last_updated_at_version: false,
840            with_row_created_at_version: true,
841            deletion_vector: Some(Arc::new(DeletionVector::Bitmap(RoaringBitmap::from_iter(
842                0..35,
843            )))),
844            row_id_sequence: None,
845            last_updated_at_sequence: None,
846            created_at_sequence: Some(seq),
847            make_deletions_null: false,
848            total_num_rows: 100,
849        };
850        let stream = super::wrap_with_row_id_and_delete(data, 0, config);
851        let batches: Vec<_> = stream
852            .buffered(1)
853            .try_filter(|b| std::future::ready(b.num_rows() > 0))
854            .try_collect()
855            .await
856            .unwrap();
857
858        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
859        assert_eq!(total_rows, 65);
860
861        for batch in &batches {
862            let versions = batch
863                .column_by_name("_row_created_at_version")
864                .unwrap()
865                .as_primitive::<UInt64Type>()
866                .values();
867            assert!(versions.iter().all(|&v| v == 42));
868        }
869    }
870
871    #[tokio::test]
872    async fn test_version_column_multi_run() {
873        use crate::rowids::segment::U64Segment;
874        use crate::rowids::version::{RowDatasetVersionRun, RowDatasetVersionSequence};
875
876        // 3 runs: 0..40 v1, 40..70 v2, 70..100 v3
877        let seq = Arc::new(RowDatasetVersionSequence {
878            runs: vec![
879                RowDatasetVersionRun {
880                    span: U64Segment::Range(0..40),
881                    version: 1,
882                },
883                RowDatasetVersionRun {
884                    span: U64Segment::Range(40..70),
885                    version: 2,
886                },
887                RowDatasetVersionRun {
888                    span: U64Segment::Range(70..100),
889                    version: 3,
890                },
891            ],
892        });
893
894        // Delete 0..20 and 60..80 (spans run boundary).
895        // Survivors: 20..40 (v1), 40..60 (v2), 80..100 (v3) = 60 rows
896        let mut deletions = RoaringBitmap::from_iter(0..20);
897        deletions.extend(60..80);
898
899        let data = batch_task_stream(
900            lance_datagen::gen_batch()
901                .col("x", lance_datagen::array::rand::<Int32Type>())
902                .into_reader_stream(RowCount::from(10), BatchCount::from(10))
903                .0,
904        );
905
906        let config = RowIdAndDeletesConfig {
907            params: ReadBatchParams::RangeFull,
908            with_row_id: true,
909            with_row_addr: false,
910            with_row_last_updated_at_version: false,
911            with_row_created_at_version: true,
912            deletion_vector: Some(Arc::new(DeletionVector::Bitmap(deletions))),
913            row_id_sequence: None,
914            last_updated_at_sequence: None,
915            created_at_sequence: Some(seq),
916            make_deletions_null: false,
917            total_num_rows: 100,
918        };
919        let stream = super::wrap_with_row_id_and_delete(data, 0, config);
920        let batches: Vec<_> = stream
921            .buffered(1)
922            .try_filter(|b| std::future::ready(b.num_rows() > 0))
923            .try_collect()
924            .await
925            .unwrap();
926
927        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
928        assert_eq!(total_rows, 60);
929
930        let all_versions: Vec<u64> = batches
931            .iter()
932            .flat_map(|b| {
933                b.column_by_name("_row_created_at_version")
934                    .unwrap()
935                    .as_primitive::<UInt64Type>()
936                    .values()
937                    .to_vec()
938            })
939            .collect();
940
941        assert!(all_versions[..20].iter().all(|&v| v == 1));
942        assert!(all_versions[20..40].iter().all(|&v| v == 2));
943        assert!(all_versions[40..60].iter().all(|&v| v == 3));
944    }
945}