Skip to main content

lance_table/rowids/
version.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Row version tracking for cross-version diff functionality
5//!
6//! This module provides data structures and functionality to track the latest
7//! update version for each row in a Lance dataset, enabling efficient
8//! cross-version diff operations.
9
10use std::{ops::Range, sync::Arc};
11
12use lance_core::Error;
13use lance_core::Result;
14use lance_core::deepsize::DeepSizeOf;
15use prost::Message;
16use serde::de::Deserializer;
17use serde::ser::Serializer;
18use serde::{Deserialize, Serialize};
19
20use crate::format::{ExternalFile, Fragment, pb};
21use crate::rowids::segment::U64Segment;
22use crate::rowids::{RowIdSequence, read_row_ids};
23
24/// A run of identical versions over a contiguous span of row positions.
25///
26/// Span is expressed as a U64Segment over row offsets (0..N within a fragment),
27/// not over row IDs. This keeps the encoding aligned with RowIdSequence order
28/// and enables zipped iteration without building a map.
29#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
30pub struct RowDatasetVersionRun {
31    pub span: U64Segment,
32    pub version: u64,
33}
34
35impl RowDatasetVersionRun {
36    /// Number of rows covered by this run.
37    pub fn len(&self) -> usize {
38        self.span.len()
39    }
40
41    /// Whether this run covers no rows.
42    pub fn is_empty(&self) -> bool {
43        self.span.is_empty()
44    }
45
46    /// The version value of this run.
47    pub fn version(&self) -> u64 {
48        self.version
49    }
50}
51
52/// Sequence of dataset versions
53///
54/// Stores version runs aligned to the positional order of RowIdSequence.
55/// Provides sequential iterators and optional lightweight indexing for
56/// efficient random access.
57#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf, Default)]
58pub struct RowDatasetVersionSequence {
59    pub runs: Vec<RowDatasetVersionRun>,
60}
61
62/// A reusable cursor for reading ranges from a version sequence in one pass.
63///
64/// The cursor caches the current run length. Readers normally request adjacent
65/// batches, so this avoids rebuilding all run offsets and rescanning the run
66/// prefix for every batch. A backwards selection lazily builds a run-offset
67/// index. Once built, non-adjacent selections use it in either direction.
68#[derive(Debug, Default)]
69pub(crate) struct RowDatasetVersionCursor {
70    run_index: usize,
71    offset_in_run: usize,
72    position: usize,
73    run_len: Option<usize>,
74    run_offsets: Option<Vec<usize>>,
75    indexed_total_len: usize,
76    #[cfg(test)]
77    indexed_seek_count: usize,
78}
79
80impl RowDatasetVersionCursor {
81    fn seek_indexed(
82        &mut self,
83        sequence: &RowDatasetVersionSequence,
84        position: usize,
85    ) -> Result<()> {
86        if self.run_offsets.is_none() {
87            let mut total_len = 0;
88            let run_offsets = sequence
89                .runs
90                .iter()
91                .map(|run| {
92                    let offset = total_len;
93                    total_len += run.len();
94                    offset
95                })
96                .collect();
97            self.run_offsets = Some(run_offsets);
98            self.indexed_total_len = total_len;
99        }
100
101        if position >= self.indexed_total_len {
102            return Err(Error::internal(format!(
103                "version column position {} out of range (total_len={})",
104                position, self.indexed_total_len
105            )));
106        }
107
108        let run_offsets = self.run_offsets.as_ref().unwrap();
109        let mut run_index = match run_offsets.binary_search(&position) {
110            Ok(run_index) => run_index,
111            Err(run_index) => run_index - 1,
112        };
113        while run_index + 1 < run_offsets.len() && run_offsets[run_index + 1] <= position {
114            run_index += 1;
115        }
116        self.run_index = run_index;
117        self.offset_in_run = position - run_offsets[run_index];
118        self.position = position;
119        self.run_len = None;
120        #[cfg(test)]
121        {
122            self.indexed_seek_count += 1;
123        }
124        Ok(())
125    }
126
127    fn current_run<'a>(
128        &mut self,
129        sequence: &'a RowDatasetVersionSequence,
130    ) -> Option<(&'a RowDatasetVersionRun, usize)> {
131        loop {
132            let run = sequence.runs.get(self.run_index)?;
133            let run_len = *self.run_len.get_or_insert_with(|| match &run.span {
134                // Version runs are normally positional ranges. Keep this hot
135                // path local instead of using the general segment length path.
136                U64Segment::Range(range) => (range.end - range.start) as usize,
137                span => span.len(),
138            });
139            if self.offset_in_run < run_len {
140                return Some((run, run_len));
141            }
142            self.run_index += 1;
143            self.offset_in_run = 0;
144            self.run_len = None;
145        }
146    }
147
148    /// Append the versions in `selection` to `versions`.
149    pub(crate) fn extend_range(
150        &mut self,
151        sequence: &RowDatasetVersionSequence,
152        selection: Range<usize>,
153        versions: &mut Vec<u64>,
154    ) -> Result<()> {
155        if selection.is_empty() {
156            return Ok(());
157        }
158        if selection.start < self.position
159            || (self.run_offsets.is_some() && selection.start != self.position)
160        {
161            self.seek_indexed(sequence, selection.start)?;
162        }
163
164        while self.position < selection.start {
165            let Some((_, run_len)) = self.current_run(sequence) else {
166                return Err(Error::internal(format!(
167                    "version column position {} out of range (total_len={})",
168                    selection.start, self.position
169                )));
170            };
171            let advance = (selection.start - self.position).min(run_len - self.offset_in_run);
172            self.offset_in_run += advance;
173            self.position += advance;
174        }
175
176        while self.position < selection.end {
177            let Some((run, run_len)) = self.current_run(sequence) else {
178                return Err(Error::internal(format!(
179                    "version column position {} out of range (total_len={})",
180                    self.position, self.position
181                )));
182            };
183            let count = (selection.end - self.position).min(run_len - self.offset_in_run);
184            versions.extend(std::iter::repeat_n(run.version(), count));
185            self.offset_in_run += count;
186            self.position += count;
187        }
188        Ok(())
189    }
190}
191
192impl RowDatasetVersionSequence {
193    /// Create a new empty version sequence
194    pub fn new() -> Self {
195        Self { runs: Vec::new() }
196    }
197
198    /// Create a version sequence with a single uniform run of `row_count` rows.
199    pub fn from_uniform_row_count(row_count: u64, version: u64) -> Self {
200        if row_count == 0 {
201            return Self::new();
202        }
203        let run = RowDatasetVersionRun {
204            span: U64Segment::Range(0..row_count),
205            version,
206        };
207        Self { runs: vec![run] }
208    }
209
210    /// Number of rows tracked by this sequence (sum of run lengths).
211    pub fn len(&self) -> u64 {
212        self.runs.iter().map(|s| s.len() as u64).sum()
213    }
214
215    /// Empty if there are no runs or all runs are empty.
216    pub fn is_empty(&self) -> bool {
217        self.runs.is_empty() || self.runs.iter().all(|s| s.is_empty())
218    }
219
220    /// Returns a forward iterator over versions, expanding runs lazily.
221    pub fn versions(&self) -> VersionsIter<'_> {
222        VersionsIter::new(&self.runs)
223    }
224
225    /// Create a reusable cursor for sequential range reads.
226    pub(crate) fn cursor(&self) -> RowDatasetVersionCursor {
227        RowDatasetVersionCursor::default()
228    }
229
230    /// Random access: get the version at global row position `index`.
231    pub fn version_at(&self, index: usize) -> Option<u64> {
232        let mut offset = 0usize;
233        for run in &self.runs {
234            let len = run.len();
235            if index < offset + len {
236                return Some(run.version());
237            }
238            offset += len;
239        }
240        None
241    }
242
243    /// Get the version associated with a specific row id.
244    /// This reconstructs the positional offset from RowIdSequence and then
245    /// performs `version_at` lookup.
246    pub fn get_version_for_row_id(&self, row_ids: &RowIdSequence, row_id: u64) -> Option<u64> {
247        let mut offset = 0usize;
248        for seg in &row_ids.0 {
249            if seg.range().is_some_and(|r| r.contains(&row_id))
250                && let Some(local) = seg.position(row_id)
251            {
252                return self.version_at(offset + local);
253            }
254            offset += seg.len();
255        }
256        None
257    }
258
259    /// Convenience: collect row IDs with version strictly greater than `threshold`.
260    pub fn rows_with_version_greater_than(
261        &self,
262        row_ids: &RowIdSequence,
263        threshold: u64,
264    ) -> Vec<u64> {
265        row_ids
266            .iter()
267            .zip(self.versions())
268            .filter_map(|(rid, v)| if v > threshold { Some(rid) } else { None })
269            .collect()
270    }
271
272    /// Delete rows by positional offsets (e.g., from a deletion vector)
273    pub fn mask(&mut self, positions: impl IntoIterator<Item = u32>) -> Result<()> {
274        let mut local_positions: Vec<u32> = Vec::new();
275        let mut positions_iter = positions.into_iter();
276        let mut curr_position = positions_iter.next();
277        let mut offset: usize = 0;
278        let mut cutoff: usize = 0;
279
280        for run in self.runs.iter_mut() {
281            cutoff += run.span.len();
282            while let Some(position) = curr_position {
283                if position as usize >= cutoff {
284                    break;
285                }
286                local_positions.push(position - offset as u32);
287                curr_position = positions_iter.next();
288            }
289
290            if !local_positions.is_empty() {
291                run.span.mask(local_positions.as_slice());
292                local_positions.clear();
293            }
294            offset = cutoff;
295        }
296
297        self.runs.retain(|r| !r.span.is_empty());
298        Ok(())
299    }
300}
301
302/// Iterator over versions expanding runs lazily.
303pub struct VersionsIter<'a> {
304    runs: &'a [RowDatasetVersionRun],
305    run_idx: usize,
306    remaining_in_run: usize,
307    current_version: u64,
308}
309
310impl<'a> VersionsIter<'a> {
311    fn new(runs: &'a [RowDatasetVersionRun]) -> Self {
312        let mut it = Self {
313            runs,
314            run_idx: 0,
315            remaining_in_run: 0,
316            current_version: 0,
317        };
318        it.advance_run();
319        it
320    }
321
322    fn advance_run(&mut self) {
323        if self.run_idx < self.runs.len() {
324            let run = &self.runs[self.run_idx];
325            self.remaining_in_run = run.len();
326            self.current_version = run.version();
327        } else {
328            self.remaining_in_run = 0;
329        }
330    }
331}
332
333impl<'a> Iterator for VersionsIter<'a> {
334    type Item = u64;
335
336    fn next(&mut self) -> Option<Self::Item> {
337        if self.remaining_in_run == 0 {
338            // Move to next run
339            self.run_idx += 1;
340            if self.run_idx >= self.runs.len() {
341                return None;
342            }
343            self.advance_run();
344        }
345        self.remaining_in_run = self.remaining_in_run.saturating_sub(1);
346        Some(self.current_version)
347    }
348}
349
350/// Metadata about the location of dataset version sequence data
351/// Following the same pattern as RowIdMeta
352///
353/// When stored inline, identical byte sequences are shared across fragments
354/// via `Arc<[u8]>` to reduce manifest memory for large tables.
355#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
356pub enum RowDatasetVersionMeta {
357    /// Small sequences stored inline in the fragment metadata
358    Inline(Arc<[u8]>),
359    /// Large sequences stored in external files
360    External(ExternalFile),
361}
362
363// Custom Serialize: convert Arc<[u8]> to slice for transparent JSON output
364impl Serialize for RowDatasetVersionMeta {
365    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
366        #[derive(Serialize)]
367        #[serde(untagged)]
368        enum Helper<'a> {
369            Inline { inline: &'a [u8] },
370            External { external: &'a ExternalFile },
371        }
372
373        match self {
374            Self::Inline(data) => Helper::Inline {
375                inline: data.as_ref(),
376            }
377            .serialize(serializer),
378            Self::External(file) => Helper::External { external: file }.serialize(serializer),
379        }
380    }
381}
382
383// Custom Deserialize: read Vec<u8> and convert to Arc<[u8]>
384impl<'de> Deserialize<'de> for RowDatasetVersionMeta {
385    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
386        #[derive(Deserialize)]
387        #[serde(untagged)]
388        enum Helper {
389            Inline { inline: Vec<u8> },
390            External { external: ExternalFile },
391        }
392
393        match Helper::deserialize(deserializer)? {
394            Helper::Inline { inline } => Ok(Self::Inline(Arc::from(inline))),
395            Helper::External { external } => Ok(Self::External(external)),
396        }
397    }
398}
399
400impl RowDatasetVersionMeta {
401    /// Create inline metadata from a version sequence
402    pub fn from_sequence(sequence: &RowDatasetVersionSequence) -> lance_core::Result<Self> {
403        let bytes = write_dataset_versions(sequence);
404        Ok(Self::Inline(Arc::from(bytes)))
405    }
406
407    /// Create external metadata reference
408    pub fn from_external_file(path: String, offset: u64, size: u64) -> Self {
409        Self::External(ExternalFile { path, offset, size })
410    }
411
412    /// Load the version sequence from this metadata
413    pub fn load_sequence(&self) -> lance_core::Result<RowDatasetVersionSequence> {
414        match self {
415            Self::Inline(data) => read_dataset_versions(data),
416            Self::External(_file) => {
417                todo!("External file loading not yet implemented")
418            }
419        }
420    }
421}
422
423/// Helper function to convert RowDatasetVersionMeta to protobuf format for last_updated_at
424pub fn last_updated_at_version_meta_to_pb(
425    meta: &Option<RowDatasetVersionMeta>,
426) -> Option<pb::data_fragment::LastUpdatedAtVersionSequence> {
427    meta.as_ref().map(|m| match m {
428        RowDatasetVersionMeta::Inline(data) => {
429            pb::data_fragment::LastUpdatedAtVersionSequence::InlineLastUpdatedAtVersions(
430                data.to_vec(),
431            )
432        }
433        RowDatasetVersionMeta::External(file) => {
434            pb::data_fragment::LastUpdatedAtVersionSequence::ExternalLastUpdatedAtVersions(
435                pb::ExternalFile {
436                    path: file.path.clone(),
437                    offset: file.offset,
438                    size: file.size,
439                },
440            )
441        }
442    })
443}
444
445/// Helper function to convert RowDatasetVersionMeta to protobuf format for created_at
446pub fn created_at_version_meta_to_pb(
447    meta: &Option<RowDatasetVersionMeta>,
448) -> Option<pb::data_fragment::CreatedAtVersionSequence> {
449    meta.as_ref().map(|m| match m {
450        RowDatasetVersionMeta::Inline(data) => {
451            pb::data_fragment::CreatedAtVersionSequence::InlineCreatedAtVersions(data.to_vec())
452        }
453        RowDatasetVersionMeta::External(file) => {
454            pb::data_fragment::CreatedAtVersionSequence::ExternalCreatedAtVersions(
455                pb::ExternalFile {
456                    path: file.path.clone(),
457                    offset: file.offset,
458                    size: file.size,
459                },
460            )
461        }
462    })
463}
464
465/// Serialize a dataset version sequence to a buffer (following RowIdSequence pattern)
466pub fn write_dataset_versions(sequence: &RowDatasetVersionSequence) -> Vec<u8> {
467    // Convert to protobuf sequence
468    let pb_sequence = pb::RowDatasetVersionSequence {
469        runs: sequence
470            .runs
471            .iter()
472            .map(|run| pb::RowDatasetVersionRun {
473                span: Some(pb::U64Segment::from(run.span.clone())),
474                version: run.version,
475            })
476            .collect(),
477    };
478
479    pb_sequence.encode_to_vec()
480}
481
482/// Deserialize a dataset version sequence from bytes (following RowIdSequence pattern)
483pub fn read_dataset_versions(data: &[u8]) -> lance_core::Result<RowDatasetVersionSequence> {
484    let pb_sequence = pb::RowDatasetVersionSequence::decode(data).map_err(|e| {
485        Error::internal(format!("Failed to decode RowDatasetVersionSequence: {}", e))
486    })?;
487
488    let segments = pb_sequence
489        .runs
490        .into_iter()
491        .map(|pb_run| {
492            let positions_pb = pb_run.span.ok_or_else(|| {
493                Error::internal("Missing positions in RowDatasetVersionRun".to_string())
494            })?;
495            let segment = U64Segment::try_from(positions_pb)?;
496            Ok(RowDatasetVersionRun {
497                span: segment,
498                version: pb_run.version,
499            })
500        })
501        .collect::<Result<Vec<_>>>()?;
502
503    Ok(RowDatasetVersionSequence { runs: segments })
504}
505
506/// Re-chunk a sequence of dataset version runs into new chunk sizes (aligned with RowIdSequence rechunking)
507pub fn rechunk_version_sequences(
508    sequences: impl IntoIterator<Item = RowDatasetVersionSequence>,
509    chunk_sizes: impl IntoIterator<Item = u64>,
510    allow_incomplete: bool,
511) -> Result<Vec<RowDatasetVersionSequence>> {
512    let chunk_sizes_vec: Vec<u64> = chunk_sizes.into_iter().collect();
513    let total_chunks = chunk_sizes_vec.len();
514    let mut chunked_sequences: Vec<RowDatasetVersionSequence> = Vec::with_capacity(total_chunks);
515
516    let mut run_iter = sequences
517        .into_iter()
518        .flat_map(|sequence| sequence.runs.into_iter())
519        .peekable();
520
521    let too_few_segments_error = |chunk_index: usize, expected_chunk_size: u64, remaining: u64| {
522        Error::invalid_input(format!(
523            "Got too few version runs for chunk {}. Expected chunk size: {}, remaining needed: {}",
524            chunk_index, expected_chunk_size, remaining
525        ))
526    };
527
528    let too_many_segments_error = |processed_chunks: usize, total_chunk_sizes: usize| {
529        Error::invalid_input(format!(
530            "Got too many version runs for the provided chunk lengths. Processed {} chunks out of {} expected",
531            processed_chunks, total_chunk_sizes
532        ))
533    };
534
535    let mut segment_offset = 0_u64;
536
537    for (chunk_index, chunk_size) in chunk_sizes_vec.iter().enumerate() {
538        let chunk_size = *chunk_size;
539        let mut out_seq = RowDatasetVersionSequence::new();
540        let mut remaining = chunk_size;
541
542        while remaining > 0 {
543            let remaining_in_segment = run_iter
544                .peek()
545                .map_or(0, |run| run.span.len() as u64 - segment_offset);
546
547            if remaining_in_segment == 0 {
548                if run_iter.next().is_some() {
549                    segment_offset = 0;
550                    continue;
551                } else if allow_incomplete {
552                    break;
553                } else {
554                    return Err(too_few_segments_error(chunk_index, chunk_size, remaining));
555                }
556            }
557
558            match remaining_in_segment.cmp(&remaining) {
559                std::cmp::Ordering::Greater => {
560                    let run = run_iter.peek().unwrap();
561                    let seg = run.span.slice(segment_offset as usize, remaining as usize);
562                    out_seq.runs.push(RowDatasetVersionRun {
563                        span: seg,
564                        version: run.version,
565                    });
566                    segment_offset += remaining;
567                    remaining = 0;
568                }
569                std::cmp::Ordering::Equal | std::cmp::Ordering::Less => {
570                    let run = run_iter.next().ok_or_else(|| {
571                        too_few_segments_error(chunk_index, chunk_size, remaining)
572                    })?;
573                    let seg = run
574                        .span
575                        .slice(segment_offset as usize, remaining_in_segment as usize);
576                    out_seq.runs.push(RowDatasetVersionRun {
577                        span: seg,
578                        version: run.version,
579                    });
580                    segment_offset = 0;
581                    remaining -= remaining_in_segment;
582                }
583            }
584        }
585
586        chunked_sequences.push(out_seq);
587    }
588
589    if run_iter.peek().is_some() {
590        return Err(too_many_segments_error(
591            chunked_sequences.len(),
592            total_chunks,
593        ));
594    }
595
596    Ok(chunked_sequences)
597}
598
599/// Build version metadata for a fragment if it has physical rows and no existing metadata.
600pub fn build_version_meta(
601    fragment: &Fragment,
602    current_version: u64,
603) -> Option<RowDatasetVersionMeta> {
604    if let Some(physical_rows) = fragment.physical_rows
605        && physical_rows > 0
606    {
607        // Verify row_id_meta exists (sanity check for stable row IDs)
608        if fragment.row_id_meta.is_none() {
609            panic!("Can not find row id meta, please make sure you have enabled stable row id.")
610        }
611
612        // Use physical_rows directly as the authoritative row count
613        // This is correct even for compacted fragments where row_id_meta might
614        // have been partially copied
615        let version_sequence = RowDatasetVersionSequence::from_uniform_row_count(
616            physical_rows as u64,
617            current_version,
618        );
619
620        return Some(RowDatasetVersionMeta::from_sequence(&version_sequence).unwrap());
621    }
622    None
623}
624
625/// Refresh row-level latest update version metadata for a full fragment rewrite-column update.
626///
627/// This sets a uniform version sequence for all rows in the fragment to `current_version`.
628pub fn refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
629    fragment: &mut Fragment,
630    current_version: u64,
631) -> Result<()> {
632    let row_count = if let Some(pr) = fragment.physical_rows {
633        pr as u64
634    } else if let Some(row_id_meta) = fragment.row_id_meta.as_ref() {
635        match row_id_meta {
636            crate::format::RowIdMeta::Inline(data) => {
637                let sequence = read_row_ids(data).unwrap();
638                sequence.len()
639            }
640            // Follow existing behavior: external sequence not yet supported here
641            crate::format::RowIdMeta::External(_file) => 0,
642        }
643    } else {
644        0
645    };
646
647    if row_count > 0 {
648        let version_seq =
649            RowDatasetVersionSequence::from_uniform_row_count(row_count, current_version);
650        let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq)?;
651        fragment.last_updated_at_version_meta = Some(version_meta);
652    }
653
654    Ok(())
655}
656
657/// Refresh row-level latest update version metadata for a partial fragment rewrite-column update.
658///
659/// `updated_offsets` are local row offsets (within the fragment) that have been updated.
660/// Existing version metadata is preserved and only the updated positions are set to `current_version`.
661/// If no existing metadata is present, positions default to `prev_version`.
662pub fn refresh_row_latest_update_meta_for_partial_frag_rewrite_cols(
663    fragment: &mut Fragment,
664    updated_offsets: &[usize],
665    current_version: u64,
666    prev_version: u64,
667) -> Result<()> {
668    // Determine row count for fragment
669    let row_count_u64: u64 = if let Some(pr) = fragment.physical_rows {
670        pr as u64
671    } else if let Some(row_id_meta) = fragment.row_id_meta.as_ref() {
672        match row_id_meta {
673            crate::format::RowIdMeta::Inline(data) => {
674                let sequence = read_row_ids(data).unwrap();
675                sequence.len()
676            }
677            crate::format::RowIdMeta::External(_file) => {
678                // Preserve original behavior for external sequences
679                todo!("External file loading not yet implemented")
680            }
681        }
682    } else {
683        0
684    };
685
686    if row_count_u64 > 0 {
687        // Build base version vector from existing meta or previous dataset version
688        let mut base_versions: Vec<u64> = Vec::with_capacity(row_count_u64 as usize);
689        if let Some(meta) = fragment.last_updated_at_version_meta.as_ref() {
690            if let Ok(base_seq) = meta.load_sequence() {
691                base_versions.extend(base_seq.versions().take(row_count_u64 as usize));
692                base_versions.resize(row_count_u64 as usize, prev_version);
693            } else {
694                base_versions.resize(row_count_u64 as usize, prev_version);
695            }
696        } else {
697            base_versions.resize(row_count_u64 as usize, prev_version);
698        }
699
700        // Apply updates to updated positions
701        for &pos in updated_offsets {
702            if pos < base_versions.len() {
703                base_versions[pos] = current_version;
704            }
705        }
706
707        // Compress into runs
708        let mut runs: Vec<RowDatasetVersionRun> = Vec::new();
709        if !base_versions.is_empty() {
710            let mut start = 0usize;
711            let mut curr_ver = base_versions[0];
712            for (idx, &ver) in base_versions.iter().enumerate().skip(1) {
713                if ver != curr_ver {
714                    runs.push(RowDatasetVersionRun {
715                        span: U64Segment::Range(start as u64..idx as u64),
716                        version: curr_ver,
717                    });
718                    start = idx;
719                    curr_ver = ver;
720                }
721            }
722            runs.push(RowDatasetVersionRun {
723                span: U64Segment::Range(start as u64..base_versions.len() as u64),
724                version: curr_ver,
725            });
726        }
727        let new_seq = RowDatasetVersionSequence { runs };
728        let new_meta = RowDatasetVersionMeta::from_sequence(&new_seq)?;
729        fragment.last_updated_at_version_meta = Some(new_meta);
730    }
731
732    Ok(())
733}
734
735// Protobuf conversion implementations
736impl TryFrom<pb::data_fragment::LastUpdatedAtVersionSequence> for RowDatasetVersionMeta {
737    type Error = Error;
738
739    fn try_from(value: pb::data_fragment::LastUpdatedAtVersionSequence) -> Result<Self> {
740        match value {
741            pb::data_fragment::LastUpdatedAtVersionSequence::InlineLastUpdatedAtVersions(data) => {
742                Ok(Self::Inline(Arc::from(data)))
743            }
744            pb::data_fragment::LastUpdatedAtVersionSequence::ExternalLastUpdatedAtVersions(
745                file,
746            ) => Ok(Self::External(ExternalFile {
747                path: file.path,
748                offset: file.offset,
749                size: file.size,
750            })),
751        }
752    }
753}
754
755impl TryFrom<pb::data_fragment::CreatedAtVersionSequence> for RowDatasetVersionMeta {
756    type Error = Error;
757
758    fn try_from(value: pb::data_fragment::CreatedAtVersionSequence) -> Result<Self> {
759        match value {
760            pb::data_fragment::CreatedAtVersionSequence::InlineCreatedAtVersions(data) => {
761                Ok(Self::Inline(Arc::from(data)))
762            }
763            pb::data_fragment::CreatedAtVersionSequence::ExternalCreatedAtVersions(file) => {
764                Ok(Self::External(ExternalFile {
765                    path: file.path,
766                    offset: file.offset,
767                    size: file.size,
768                }))
769            }
770        }
771    }
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    #[test]
779    fn test_version_random_access() {
780        let seq = RowDatasetVersionSequence {
781            runs: vec![
782                RowDatasetVersionRun {
783                    span: U64Segment::Range(0..3),
784                    version: 1,
785                },
786                RowDatasetVersionRun {
787                    span: U64Segment::Range(0..2),
788                    version: 2,
789                },
790                RowDatasetVersionRun {
791                    span: U64Segment::Range(0..1),
792                    version: 3,
793                },
794            ],
795        };
796        assert_eq!(seq.version_at(0), Some(1));
797        assert_eq!(seq.version_at(2), Some(1));
798        assert_eq!(seq.version_at(3), Some(2));
799        assert_eq!(seq.version_at(4), Some(2));
800        assert_eq!(seq.version_at(5), Some(3));
801        assert_eq!(seq.version_at(6), None);
802    }
803
804    #[test]
805    fn test_partial_refresh_streams_many_lineage_runs() {
806        const ROWS: usize = 10_000;
807        let prior_sequence = RowDatasetVersionSequence {
808            runs: (0..ROWS)
809                .map(|position| RowDatasetVersionRun {
810                    span: U64Segment::Range(position as u64..position as u64 + 1),
811                    version: (position % 2 + 1) as u64,
812                })
813                .collect(),
814        };
815        let mut fragment = Fragment::new(1);
816        fragment.physical_rows = Some(ROWS);
817        fragment.last_updated_at_version_meta =
818            Some(RowDatasetVersionMeta::from_sequence(&prior_sequence).unwrap());
819
820        refresh_row_latest_update_meta_for_partial_frag_rewrite_cols(
821            &mut fragment,
822            &[ROWS - 1],
823            3,
824            1,
825        )
826        .unwrap();
827
828        let refreshed = fragment
829            .last_updated_at_version_meta
830            .unwrap()
831            .load_sequence()
832            .unwrap();
833        assert_eq!(refreshed.len(), ROWS as u64);
834        assert_eq!(refreshed.version_at(0), Some(1));
835        assert_eq!(refreshed.version_at(1), Some(2));
836        assert_eq!(refreshed.version_at(ROWS - 2), Some(1));
837        assert_eq!(refreshed.version_at(ROWS - 1), Some(3));
838    }
839
840    #[test]
841    fn test_serialization_round_trip() {
842        let seq = RowDatasetVersionSequence {
843            runs: vec![
844                RowDatasetVersionRun {
845                    span: U64Segment::Range(0..4),
846                    version: 42,
847                },
848                RowDatasetVersionRun {
849                    span: U64Segment::Range(0..3),
850                    version: 99,
851                },
852            ],
853        };
854        let bytes = write_dataset_versions(&seq);
855        let seq2 = read_dataset_versions(&bytes).unwrap();
856        assert_eq!(seq2.runs.len(), 2);
857        assert_eq!(seq2.len(), 7);
858        assert_eq!(seq2.version_at(0), Some(42));
859        assert_eq!(seq2.version_at(5), Some(99));
860    }
861
862    #[test]
863    fn test_get_version_for_row_id() {
864        let seq = RowDatasetVersionSequence {
865            runs: vec![
866                RowDatasetVersionRun {
867                    span: U64Segment::Range(0..2),
868                    version: 8,
869                },
870                RowDatasetVersionRun {
871                    span: U64Segment::Range(0..2),
872                    version: 9,
873                },
874            ],
875        };
876        let rows = RowIdSequence::from(10..14); // row ids: 10,11,12,13
877        assert_eq!(seq.get_version_for_row_id(&rows, 10), Some(8));
878        assert_eq!(seq.get_version_for_row_id(&rows, 11), Some(8));
879        assert_eq!(seq.get_version_for_row_id(&rows, 12), Some(9));
880        assert_eq!(seq.get_version_for_row_id(&rows, 13), Some(9));
881        assert_eq!(seq.get_version_for_row_id(&rows, 99), None);
882    }
883
884    #[test]
885    fn test_version_cursor_ranges_gaps_and_rewind() {
886        let seq = RowDatasetVersionSequence {
887            runs: vec![
888                RowDatasetVersionRun {
889                    span: U64Segment::Range(0..3),
890                    version: 10,
891                },
892                RowDatasetVersionRun {
893                    span: U64Segment::Range(3..3),
894                    version: 99,
895                },
896                RowDatasetVersionRun {
897                    span: U64Segment::Range(3..5),
898                    version: 20,
899                },
900                RowDatasetVersionRun {
901                    span: U64Segment::Range(5..9),
902                    version: 30,
903                },
904            ],
905        };
906        let expected = [10, 10, 10, 20, 20, 30, 30, 30, 30];
907        let mut cursor = seq.cursor();
908        let mut actual = Vec::new();
909
910        cursor.extend_range(&seq, 0..2, &mut actual).unwrap();
911        cursor.extend_range(&seq, 2..5, &mut actual).unwrap();
912        cursor.extend_range(&seq, 7..9, &mut actual).unwrap();
913        assert_eq!(actual, [10, 10, 10, 20, 20, 30, 30]);
914
915        actual.clear();
916        cursor.extend_range(&seq, 1..6, &mut actual).unwrap();
917        assert_eq!(actual, expected[1..6]);
918
919        cursor.extend_range(&seq, 6..6, &mut actual).unwrap();
920        assert_eq!(actual, expected[1..6]);
921    }
922
923    #[test]
924    fn test_version_cursor_descending_ranges_use_indexed_seek() {
925        const RUNS: usize = 10_000;
926        let seq = RowDatasetVersionSequence {
927            runs: (0..RUNS)
928                .map(|position| RowDatasetVersionRun {
929                    span: U64Segment::Range(position as u64..position as u64 + 1),
930                    version: position as u64,
931                })
932                .collect(),
933        };
934        let mut cursor = seq.cursor();
935        let mut actual = Vec::with_capacity(RUNS);
936
937        for position in (0..RUNS).rev() {
938            cursor
939                .extend_range(&seq, position..position + 1, &mut actual)
940                .unwrap();
941        }
942
943        assert_eq!(actual, (0..RUNS as u64).rev().collect::<Vec<_>>());
944        assert_eq!(cursor.run_offsets.as_ref().unwrap().len(), RUNS);
945    }
946
947    #[test]
948    fn test_version_cursor_alternating_ranges_use_indexed_seek_both_directions() {
949        const RUNS: usize = 10_000;
950        let seq = RowDatasetVersionSequence {
951            runs: (0..RUNS)
952                .map(|position| RowDatasetVersionRun {
953                    span: U64Segment::Range(position as u64..position as u64 + 1),
954                    version: position as u64,
955                })
956                .collect(),
957        };
958        let mut cursor = seq.cursor();
959        let mut actual = Vec::with_capacity(RUNS);
960
961        for selection_index in 0..RUNS {
962            let position = if selection_index % 2 == 0 {
963                RUNS - 1
964            } else {
965                0
966            };
967            cursor
968                .extend_range(&seq, position..position + 1, &mut actual)
969                .unwrap();
970        }
971
972        let expected = (0..RUNS)
973            .map(|selection_index| {
974                if selection_index % 2 == 0 {
975                    (RUNS - 1) as u64
976                } else {
977                    0
978                }
979            })
980            .collect::<Vec<_>>();
981        assert_eq!(actual, expected);
982        assert_eq!(cursor.run_offsets.as_ref().unwrap().len(), RUNS);
983        assert_eq!(cursor.indexed_seek_count, RUNS - 1);
984    }
985
986    #[test]
987    fn test_version_cursor_reports_out_of_bounds() {
988        let seq = RowDatasetVersionSequence::from_uniform_row_count(4, 7);
989        let mut cursor = seq.cursor();
990        let mut actual = Vec::new();
991        let error = cursor.extend_range(&seq, 4..5, &mut actual).unwrap_err();
992        assert!(matches!(error, Error::Internal { .. }));
993        assert!(
994            error
995                .to_string()
996                .contains("position 4 out of range (total_len=4)")
997        );
998        assert!(actual.is_empty());
999    }
1000
1001    #[test]
1002    fn test_version_cursor_non_range_span() {
1003        let non_range_span = U64Segment::from_slice(&[0, 2, 4, 6, 8]);
1004        assert!(!matches!(non_range_span, U64Segment::Range(_)));
1005        let seq = RowDatasetVersionSequence {
1006            runs: vec![
1007                RowDatasetVersionRun {
1008                    span: non_range_span,
1009                    version: 4,
1010                },
1011                RowDatasetVersionRun {
1012                    span: U64Segment::Range(0..3),
1013                    version: 8,
1014                },
1015            ],
1016        };
1017        let mut actual = Vec::new();
1018        seq.cursor().extend_range(&seq, 0..8, &mut actual).unwrap();
1019        assert_eq!(actual, [4, 4, 4, 4, 4, 8, 8, 8]);
1020    }
1021}