Skip to main content

lance_table/format/
manifest.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use chrono::prelude::*;
7use lance_core::deepsize::DeepSizeOf;
8use lance_file::datatypes::{Fields, FieldsWithMeta};
9use lance_file::version::{ConcreteFileVersion, stable_file_version};
10use lance_file::versions::v1::{
11    encoding::populate_schema_dictionaries, reader::FileReader as V1FileReader,
12};
13use lance_io::traits::{ProtoStruct, Reader};
14use object_store::path::Path;
15use prost::Message;
16use prost_types::Timestamp;
17use std::collections::{BTreeMap, HashMap};
18use std::ops::Range;
19use std::sync::Arc;
20
21use super::{Fragment, InlineRowIds, RowIdMeta};
22use crate::feature_flags::{FLAG_COVERED_INDEX_METADATA, STICKY_READER_FLAGS, STICKY_WRITER_FLAGS};
23use crate::feature_flags::{FLAG_STABLE_ROW_IDS, has_deprecated_v2_feature_flag};
24use crate::format::fragment::DataFileFieldInterner;
25use crate::format::pb;
26use lance_core::cache::LanceCache;
27use lance_core::datatypes::Schema;
28use lance_core::{Error, Result};
29use lance_io::object_store::{ObjectStore, ObjectStoreRegistry};
30use lance_io::utils::read_struct;
31
32/// Manifest of a dataset
33///
34///  * Schema
35///  * Version
36///  * Fragments.
37///  * Indices.
38#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
39pub struct Manifest {
40    /// Dataset schema.
41    pub schema: Schema,
42
43    /// Dataset version
44    pub version: u64,
45
46    /// Branch name, None if the dataset is the main branch.
47    pub branch: Option<String>,
48
49    /// Version of the writer library that wrote this manifest.
50    pub writer_version: Option<WriterVersion>,
51
52    /// Fragments, the pieces to build the dataset.
53    ///
54    /// This list is stored in order, sorted by fragment id.  However, the fragment id
55    /// sequence may have gaps.
56    pub fragments: Arc<Vec<Fragment>>,
57
58    /// The file position of the version aux data.
59    pub version_aux_data: usize,
60
61    /// The file position of the index metadata.
62    pub index_section: Option<usize>,
63
64    /// The creation timestamp with nanosecond resolution as 128-bit integer
65    pub timestamp_nanos: u128,
66
67    /// An optional string tag for this version
68    pub tag: Option<String>,
69
70    /// The reader flags
71    pub reader_feature_flags: u64,
72
73    /// The writer flags
74    pub writer_feature_flags: u64,
75
76    /// The max fragment id used so far
77    /// None means never set, Some(0) means max ID used so far is 0
78    pub max_fragment_id: Option<u32>,
79
80    /// The highest field ID allocated since non-reusable field identity was activated.
81    /// `None` means the dataset still uses the legacy live-reference allocator.
82    pub max_allocated_field_id: Option<i32>,
83
84    /// The path to the transaction file, relative to the root of the dataset
85    pub transaction_file: Option<String>,
86
87    /// The file position of the inline transaction content inside the manifest
88    pub transaction_section: Option<usize>,
89
90    /// Precomputed logic offset of each fragment
91    /// accelerating the fragment search using offset ranges.
92    fragment_offsets: Vec<usize>,
93
94    /// The max row id used so far.
95    pub next_row_id: u64,
96
97    /// The storage format of the data files.
98    pub data_storage_format: DataStorageFormat,
99
100    /// Table configuration.
101    pub config: HashMap<String, String>,
102
103    /// Table metadata.
104    ///
105    /// This is a key-value map that can be used to store arbitrary metadata
106    /// associated with the table. This is different than configuration, which
107    /// is used to tell libraries how to read, write, or manage the table.
108    pub table_metadata: HashMap<String, String>,
109
110    /* external base paths */
111    pub base_paths: HashMap<u32, BasePath>,
112}
113
114// We use the most significant bit to indicate that a transaction is detached
115pub const DETACHED_VERSION_MASK: u64 = 0x8000_0000_0000_0000;
116
117pub fn is_detached_version(version: u64) -> bool {
118    version & DETACHED_VERSION_MASK != 0
119}
120
121fn compute_fragment_offsets(fragments: &[Fragment]) -> Vec<usize> {
122    fragments
123        .iter()
124        .map(|f| f.num_rows().unwrap_or_default())
125        .chain([0]) // Make the last offset to be the full-length of the dataset.
126        .scan(0_usize, |offset, len| {
127            let start = *offset;
128            *offset += len;
129            Some(start)
130        })
131        .collect()
132}
133
134#[derive(Default)]
135pub struct ManifestSummary {
136    pub total_fragments: u64,
137    pub total_data_files: u64,
138    pub total_files_size: u64,
139    pub total_deletion_files: u64,
140    pub total_data_file_rows: u64,
141    pub total_deletion_file_rows: u64,
142    pub total_rows: u64,
143}
144
145impl From<ManifestSummary> for BTreeMap<String, String> {
146    fn from(summary: ManifestSummary) -> Self {
147        let mut stats_map = Self::new();
148        stats_map.insert(
149            "total_fragments".to_string(),
150            summary.total_fragments.to_string(),
151        );
152        stats_map.insert(
153            "total_data_files".to_string(),
154            summary.total_data_files.to_string(),
155        );
156        stats_map.insert(
157            "total_files_size".to_string(),
158            summary.total_files_size.to_string(),
159        );
160        stats_map.insert(
161            "total_deletion_files".to_string(),
162            summary.total_deletion_files.to_string(),
163        );
164        stats_map.insert(
165            "total_data_file_rows".to_string(),
166            summary.total_data_file_rows.to_string(),
167        );
168        stats_map.insert(
169            "total_deletion_file_rows".to_string(),
170            summary.total_deletion_file_rows.to_string(),
171        );
172        stats_map.insert("total_rows".to_string(), summary.total_rows.to_string());
173        stats_map
174    }
175}
176
177impl Manifest {
178    /// Whether this table requires independently addressed Managed Blob support.
179    pub fn has_managed_blobs(&self) -> bool {
180        self.reader_feature_flags & crate::feature_flags::FLAG_MANAGED_BLOBS != 0
181    }
182
183    pub fn new(
184        schema: Schema,
185        fragments: Arc<Vec<Fragment>>,
186        data_storage_format: DataStorageFormat,
187        base_paths: HashMap<u32, BasePath>,
188    ) -> Self {
189        let fragment_offsets = compute_fragment_offsets(&fragments);
190
191        Self {
192            schema,
193            version: 1,
194            branch: None,
195            writer_version: Some(WriterVersion::default()),
196            fragments,
197            version_aux_data: 0,
198            index_section: None,
199            timestamp_nanos: 0,
200            tag: None,
201            reader_feature_flags: 0, // These will be set on commit
202            writer_feature_flags: 0, // These will be set on commit
203            max_fragment_id: None,
204            max_allocated_field_id: None,
205            transaction_file: None,
206            transaction_section: None,
207            fragment_offsets,
208            next_row_id: 0,
209            data_storage_format,
210            config: HashMap::new(),
211            table_metadata: HashMap::new(),
212            base_paths,
213        }
214    }
215
216    pub fn new_from_previous(
217        previous: &Self,
218        schema: Schema,
219        fragments: Arc<Vec<Fragment>>,
220    ) -> Self {
221        let fragment_offsets = compute_fragment_offsets(&fragments);
222
223        Self {
224            schema,
225            version: previous.version + 1,
226            branch: previous.branch.clone(),
227            writer_version: Some(WriterVersion::default()),
228            fragments,
229            version_aux_data: 0,
230            index_section: None, // Caller should update index if they want to keep them.
231            timestamp_nanos: 0,  // This will be set on commit
232            tag: None,
233            reader_feature_flags: previous.reader_feature_flags & STICKY_READER_FLAGS,
234            writer_feature_flags: previous.writer_feature_flags & STICKY_WRITER_FLAGS,
235            max_fragment_id: previous.max_fragment_id,
236            max_allocated_field_id: previous.max_allocated_field_id,
237            transaction_file: None,
238            transaction_section: None,
239            fragment_offsets,
240            next_row_id: previous.next_row_id,
241            data_storage_format: previous.data_storage_format.clone(),
242            config: previous.config.clone(),
243            table_metadata: previous.table_metadata.clone(),
244            base_paths: previous.base_paths.clone(),
245        }
246    }
247
248    /// Performs a shallow_clone of the manifest entirely in memory without:
249    /// - Any persistent storage operations
250    /// - Modifications to the original data
251    /// - If the shallow clone is for branch, ref_name is the source branch
252    pub fn shallow_clone(
253        &self,
254        ref_name: Option<String>,
255        ref_path: String,
256        ref_base_id: u32,
257        branch_name: Option<String>,
258        transaction_file: String,
259    ) -> Self {
260        let cloned_fragments = self
261            .fragments
262            .as_ref()
263            .iter()
264            .map(|fragment| {
265                let mut cloned_fragment = fragment.clone();
266                for file in cloned_fragment.referenced_lance_files_mut() {
267                    if file.base_id.is_none() {
268                        file.base_id = Some(ref_base_id);
269                    }
270                }
271
272                if let Some(deletion) = &mut cloned_fragment.deletion_file
273                    && deletion.base_id.is_none()
274                {
275                    deletion.base_id = Some(ref_base_id);
276                }
277                cloned_fragment
278            })
279            .collect::<Vec<_>>();
280
281        Self {
282            schema: self.schema.clone(),
283            version: self.version,
284            branch: branch_name,
285            writer_version: self.writer_version.clone(),
286            fragments: Arc::new(cloned_fragments),
287            version_aux_data: self.version_aux_data,
288            index_section: None, // These will be set on commit
289            timestamp_nanos: self.timestamp_nanos,
290            tag: None,
291            // Not derivable from the manifest, so it would be lost like any other
292            // zeroed word: a clone of a table with covering indexes would come
293            // back unfenced, and since the clone copies the index metadata
294            // wholesale -- `covering_fields` included -- a build that predates
295            // covering could then open it and read carried columns as keyed ones.
296            // Kept unconditionally rather than derived from the cloned indexes:
297            // over-fencing a clone is harmless, under-fencing one is not.
298            // Sticky capabilities are also retained because the clone keeps the
299            // source file identities that require them. So are stable row ids:
300            // they are otherwise derived from the fragments, so a clone of a table
301            // whose rows were all deleted would lose them and stop assigning ids
302            // from `next_row_id`.
303            reader_feature_flags: self.reader_feature_flags
304                & (FLAG_COVERED_INDEX_METADATA | STICKY_READER_FLAGS | FLAG_STABLE_ROW_IDS),
305            writer_feature_flags: self.writer_feature_flags
306                & (FLAG_COVERED_INDEX_METADATA | STICKY_WRITER_FLAGS | FLAG_STABLE_ROW_IDS),
307            max_fragment_id: self.max_fragment_id,
308            max_allocated_field_id: self.max_allocated_field_id,
309            transaction_file: Some(transaction_file),
310            transaction_section: None,
311            fragment_offsets: self.fragment_offsets.clone(),
312            next_row_id: self.next_row_id,
313            data_storage_format: self.data_storage_format.clone(),
314            config: self.config.clone(),
315            base_paths: {
316                let mut base_paths = self.base_paths.clone();
317                let base_path = BasePath::new(ref_base_id, ref_path, ref_name, true);
318                base_paths.insert(ref_base_id, base_path);
319                base_paths
320            },
321            table_metadata: self.table_metadata.clone(),
322        }
323    }
324
325    /// Return the `timestamp_nanos` value as a Utc DateTime
326    pub fn timestamp(&self) -> DateTime<Utc> {
327        let nanos = self.timestamp_nanos % 1_000_000_000;
328        let seconds = ((self.timestamp_nanos - nanos) / 1_000_000_000) as i64;
329        Utc.from_utc_datetime(
330            &DateTime::from_timestamp(seconds, nanos as u32)
331                .unwrap_or_default()
332                .naive_utc(),
333        )
334    }
335
336    /// Set the `timestamp_nanos` value from a Utc DateTime
337    pub fn set_timestamp(&mut self, nanos: u128) {
338        self.timestamp_nanos = nanos;
339    }
340
341    /// Get a mutable reference to the config
342    pub fn config_mut(&mut self) -> &mut HashMap<String, String> {
343        &mut self.config
344    }
345
346    /// Get a mutable reference to the table metadata
347    pub fn table_metadata_mut(&mut self) -> &mut HashMap<String, String> {
348        &mut self.table_metadata
349    }
350
351    /// Get a mutable reference to the schema metadata
352    pub fn schema_metadata_mut(&mut self) -> &mut HashMap<String, String> {
353        &mut self.schema.metadata
354    }
355
356    /// Get a mutable reference to the field metadata for a specific field id
357    ///
358    /// Returns None if the field does not exist in the schema.
359    pub fn field_metadata_mut(&mut self, field_id: i32) -> Option<&mut HashMap<String, String>> {
360        self.schema
361            .field_by_id_mut(field_id)
362            .map(|field| &mut field.metadata)
363    }
364
365    /// Set the `config` from an iterator
366    #[deprecated(note = "Use config_mut() for direct access to config HashMap")]
367    pub fn update_config(&mut self, upsert_values: impl IntoIterator<Item = (String, String)>) {
368        self.config.extend(upsert_values);
369    }
370
371    /// Delete `config` keys using a slice of keys
372    #[deprecated(note = "Use config_mut() for direct access to config HashMap")]
373    pub fn delete_config_keys(&mut self, delete_keys: &[&str]) {
374        self.config
375            .retain(|key, _| !delete_keys.contains(&key.as_str()));
376    }
377
378    /// Replaces the schema metadata with the given key-value pairs.
379    #[deprecated(note = "Use schema_metadata_mut() for direct access to schema metadata HashMap")]
380    pub fn replace_schema_metadata(&mut self, new_metadata: HashMap<String, String>) {
381        self.schema.metadata = new_metadata;
382    }
383
384    /// Replaces the metadata of the field with the given id with the given key-value pairs.
385    ///
386    /// If the field does not exist in the schema, this is a no-op.
387    #[deprecated(
388        note = "Use field_metadata_mut(field_id) for direct access to field metadata HashMap"
389    )]
390    pub fn replace_field_metadata(
391        &mut self,
392        field_id: i32,
393        new_metadata: HashMap<String, String>,
394    ) -> Result<()> {
395        if let Some(field) = self.schema.field_by_id_mut(field_id) {
396            field.metadata = new_metadata;
397            Ok(())
398        } else {
399            Err(Error::invalid_input(format!(
400                "Field with id {} does not exist for replace_field_metadata",
401                field_id
402            )))
403        }
404    }
405
406    /// Copy inline row ids out of the manifest buffer they were decoded from
407    /// when they are a small share of it.
408    ///
409    /// Inline row ids decoded from a `Bytes` buffer are slices of that buffer
410    /// (see [`InlineRowIds`]), and a slice keeps the whole allocation alive.
411    /// That is the right trade when the row ids are most of the manifest, as
412    /// they are for a compacted stable-row-id table, but a manifest whose row
413    /// ids are a few percent of its bytes would pin the rest for nothing.
414    /// `buffer_len` is the size of the decoded buffer.
415    pub fn detach_sparse_inline_row_ids(&mut self, buffer_len: usize) {
416        let inline_bytes: usize = self
417            .fragments
418            .iter()
419            .filter_map(|fragment| match &fragment.row_id_meta {
420                Some(RowIdMeta::Inline(data)) => Some(data.len()),
421                _ => None,
422            })
423            .sum();
424        // Keep the slices while the row ids are at least a quarter of the buffer.
425        if inline_bytes == 0 || inline_bytes.saturating_mul(4) >= buffer_len {
426            return;
427        }
428        for fragment in Arc::make_mut(&mut self.fragments) {
429            if let Some(RowIdMeta::Inline(data)) = &fragment.row_id_meta {
430                let copied = InlineRowIds::from(Bytes::copy_from_slice(data));
431                fragment.row_id_meta = Some(RowIdMeta::Inline(copied));
432            }
433        }
434    }
435
436    /// Check the current fragment list and update the high water mark
437    pub fn update_max_fragment_id(&mut self) {
438        // If there are no fragments, don't update max_fragment_id
439        if self.fragments.is_empty() {
440            return;
441        }
442
443        let max_fragment_id = self
444            .fragments
445            .iter()
446            .map(|f| f.id)
447            .max()
448            .unwrap() // Safe because we checked fragments is not empty
449            .try_into()
450            .unwrap();
451
452        match self.max_fragment_id {
453            None => {
454                // First time being set
455                self.max_fragment_id = Some(max_fragment_id);
456            }
457            Some(current_max) => {
458                // Only update if the computed max is greater than current
459                // This preserves the high water mark even when fragments are deleted
460                if max_fragment_id > current_max {
461                    self.max_fragment_id = Some(max_fragment_id);
462                }
463            }
464        }
465    }
466
467    /// Return the max fragment id.
468    /// Note this does not support recycling of fragment ids.
469    ///
470    /// This will return None if there are no fragments and max_fragment_id was never set.
471    pub fn max_fragment_id(&self) -> Option<u64> {
472        if let Some(max_id) = self.max_fragment_id {
473            // Return the stored high water mark
474            Some(max_id.into())
475        } else {
476            // Not yet set, compute from fragment list
477            self.fragments.iter().map(|f| f.id).max()
478        }
479    }
480
481    /// Get the highest field ID that may not be allocated again.
482    ///
483    /// This is different than [Schema::max_field_id] because it also considers
484    /// the field ids in the data files that have been dropped from the schema,
485    /// including overlay files referenced by fragments.
486    pub fn max_field_id(&self) -> i32 {
487        self.max_allocated_field_id
488            .unwrap_or_else(|| self.max_referenced_field_id())
489    }
490
491    pub(crate) fn max_referenced_field_id(&self) -> i32 {
492        let schema_max_id = self.schema.max_field_id().unwrap_or(-1);
493        let fragment_max_id = self
494            .fragments
495            .iter()
496            .flat_map(|fragment| {
497                fragment
498                    .referenced_lance_files()
499                    .flat_map(|file| file.fields.iter())
500            })
501            .copied()
502            .max()
503            .unwrap_or(-1);
504        schema_max_id.max(fragment_max_id)
505    }
506
507    /// Whether the non-reusable-field-ID allocation contract is active.
508    pub fn uses_non_reusable_field_ids(&self) -> bool {
509        self.max_allocated_field_id.is_some()
510    }
511
512    /// Activate non-reusable field IDs at the maximum ID visible in this snapshot.
513    pub fn activate_non_reusable_field_ids(&mut self) {
514        if self.max_allocated_field_id.is_none() {
515            self.max_allocated_field_id = Some(self.max_referenced_field_id());
516        }
517    }
518
519    /// Advance the persistent field-ID high-water mark to cover this manifest.
520    pub fn update_max_field_id(&mut self) {
521        let max_referenced_field_id = self.max_referenced_field_id();
522        if let Some(max_allocated_field_id) = &mut self.max_allocated_field_id {
523            *max_allocated_field_id = (*max_allocated_field_id).max(max_referenced_field_id);
524        }
525    }
526
527    /// Return the fragments that are newer than the given manifest.
528    /// Note this does not support recycling of fragment ids.
529    pub fn fragments_since(&self, since: &Self) -> Result<Vec<Fragment>> {
530        if since.version >= self.version {
531            return Err(Error::invalid_input(format!(
532                "fragments_since: given version {} is newer than manifest version {}",
533                since.version, self.version
534            )));
535        }
536        let start = since.max_fragment_id();
537        Ok(self
538            .fragments
539            .iter()
540            .filter(|&f| start.map(|s| f.id > s).unwrap_or(true))
541            .cloned()
542            .collect())
543    }
544
545    /// Find the fragments that contain the rows, identified by the offset range.
546    ///
547    /// Note that the offsets are the logical offsets of rows, not row IDs.
548    ///
549    ///
550    /// Parameters
551    /// ----------
552    /// range: `Range<usize>`
553    ///     Offset range
554    ///
555    /// Returns
556    /// -------
557    /// Vec<(usize, Fragment)>
558    ///    A vector of `(starting_offset_of_fragment, fragment)` pairs.
559    ///
560    pub fn fragments_by_offset_range(&self, range: Range<usize>) -> Vec<(usize, &Fragment)> {
561        let start = range.start;
562        let end = range.end;
563        let idx = self
564            .fragment_offsets
565            .binary_search(&start)
566            .unwrap_or_else(|idx| idx - 1);
567
568        let mut fragments = vec![];
569        for i in idx..self.fragments.len() {
570            if self.fragment_offsets[i] >= end
571                || self.fragment_offsets[i] + self.fragments[i].num_rows().unwrap_or_default()
572                    <= start
573            {
574                break;
575            }
576            fragments.push((self.fragment_offsets[i], &self.fragments[i]));
577        }
578
579        fragments
580    }
581
582    /// Whether the dataset uses stable row ids.
583    pub fn uses_stable_row_ids(&self) -> bool {
584        self.reader_feature_flags & FLAG_STABLE_ROW_IDS != 0
585    }
586
587    /// Creates a serialized copy of the manifest, suitable for IPC or temp storage
588    /// and can be used to create a dataset
589    pub fn serialized(&self) -> Vec<u8> {
590        let pb_manifest: pb::Manifest = self.into();
591        pb_manifest.encode_to_vec()
592    }
593
594    /// Get the summary information of a manifest.
595    ///
596    /// This function calculates various statistics about the manifest, including:
597    /// - total_files_size: Total size of all data files in bytes
598    /// - total_fragments: Total number of fragments in the dataset
599    /// - total_data_files: Total number of data files across all fragments
600    /// - total_deletion_files: Total number of deletion files
601    /// - total_data_file_rows: Total number of rows in data files
602    /// - total_deletion_file_rows: Total number of deleted rows in deletion files
603    /// - total_rows: Total number of rows in the dataset
604    pub fn summary(&self) -> ManifestSummary {
605        // Calculate total fragments
606        let mut summary =
607            self.fragments
608                .iter()
609                .fold(ManifestSummary::default(), |mut summary, f| {
610                    // Count data files in the current fragment
611                    summary.total_data_files += f.files.len() as u64;
612                    // Sum the number of rows for the current fragment (if available)
613                    if let Some(num_rows) = f.num_rows() {
614                        summary.total_rows += num_rows as u64;
615                    }
616                    // Sum file sizes for all data files in the current fragment (if available)
617                    for data_file in &f.files {
618                        if let Some(size_bytes) = data_file.file_size_bytes.get() {
619                            summary.total_files_size += size_bytes.get();
620                        }
621                    }
622                    // Check and count if the current fragment has a deletion file
623                    if f.deletion_file.is_some() {
624                        summary.total_deletion_files += 1;
625                    }
626                    // Sum the number of deleted rows from the deletion file (if available)
627                    if let Some(deletion_file) = &f.deletion_file
628                        && let Some(num_deleted) = deletion_file.num_deleted_rows
629                    {
630                        summary.total_deletion_file_rows += num_deleted as u64;
631                    }
632                    summary
633                });
634        summary.total_fragments = self.fragments.len() as u64;
635        summary.total_data_file_rows = summary.total_rows + summary.total_deletion_file_rows;
636
637        summary
638    }
639}
640
641/// Populate dictionary values stored outside a V1 manifest.
642///
643/// Other exact file versions store their schema dictionaries inline, so this
644/// is a no-op for those manifests.
645///
646/// # Examples
647///
648/// ```
649/// # use lance_core::Result;
650/// # use lance_io::traits::Reader;
651/// # use lance_table::format::{Manifest, populate_manifest_schema_dictionaries};
652/// # async fn hydrate_v1_manifest(
653/// #     manifest: &mut Manifest,
654/// #     reader: &dyn Reader,
655/// # ) -> Result<()> {
656/// populate_manifest_schema_dictionaries(manifest, reader).await
657/// # }
658/// ```
659pub async fn populate_manifest_schema_dictionaries(
660    manifest: &mut Manifest,
661    reader: &dyn Reader,
662) -> Result<()> {
663    match manifest.data_storage_format.version {
664        ConcreteFileVersion::V1 => {
665            populate_schema_dictionaries(&mut manifest.schema, reader).await?;
666        }
667        ConcreteFileVersion::V2_0
668        | ConcreteFileVersion::V2_1
669        | ConcreteFileVersion::V2_2
670        | ConcreteFileVersion::V2_3 => {}
671    }
672    Ok(())
673}
674
675#[derive(Debug, Clone, PartialEq)]
676pub struct BasePath {
677    pub id: u32,
678    pub name: Option<String>,
679    pub is_dataset_root: bool,
680    /// The full URI string (e.g., "s3://bucket/path")
681    pub path: String,
682}
683
684impl BasePath {
685    /// Choose an unused exact base ID without reserving zero or overflowing at
686    /// `u32::MAX`. The caller must publish the binding with its references and
687    /// reject a concurrent attempt to bind the chosen ID to another location.
688    pub fn unused_id(bases: impl IntoIterator<Item = u32>) -> Result<u32> {
689        let mut ids = bases.into_iter().collect::<Vec<_>>();
690        ids.sort_unstable();
691        ids.dedup();
692        let mut candidate = 0u32;
693        for id in ids {
694            if id != candidate {
695                break;
696            }
697            candidate = candidate
698                .checked_add(1)
699                .ok_or_else(|| Error::invalid_input("All u32 base IDs are already registered"))?;
700        }
701        Ok(candidate)
702    }
703
704    /// Create a new BasePath
705    ///
706    /// # Arguments
707    ///
708    /// * `id` - Unique identifier for this base path
709    /// * `path` - Full URI string (e.g., "s3://bucket/path", "/local/path")
710    /// * `name` - Optional human-readable name for this base
711    /// * `is_dataset_root` - Whether this is the dataset root or a data-only base
712    pub fn new(id: u32, path: String, name: Option<String>, is_dataset_root: bool) -> Self {
713        Self {
714            id,
715            name,
716            is_dataset_root,
717            path,
718        }
719    }
720
721    /// Extract the object store path from this BasePath's URI.
722    ///
723    /// This is a synchronous operation that parses the URI without initializing an object store.
724    pub fn extract_path(&self, registry: Arc<ObjectStoreRegistry>) -> Result<Path> {
725        ObjectStore::extract_path_from_uri(registry, &self.path)
726    }
727}
728
729impl DeepSizeOf for BasePath {
730    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
731        self.name.deep_size_of_children(context)
732            + self.path.deep_size_of_children(context) * 2
733            + size_of::<bool>()
734    }
735}
736
737#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
738pub struct WriterVersion {
739    pub library: String,
740    pub version: String,
741    pub prerelease: Option<String>,
742    pub build_metadata: Option<String>,
743}
744
745#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
746pub struct DataStorageFormat {
747    pub file_format: String,
748    pub version: ConcreteFileVersion,
749}
750
751const LANCE_FORMAT_NAME: &str = "lance";
752
753impl DataStorageFormat {
754    pub fn new(version: ConcreteFileVersion) -> Self {
755        Self {
756            file_format: LANCE_FORMAT_NAME.to_string(),
757            version,
758        }
759    }
760
761    /// Return the exact file format version persisted by this manifest.
762    pub fn lance_file_format(&self) -> ConcreteFileVersion {
763        self.version
764    }
765}
766
767impl Default for DataStorageFormat {
768    fn default() -> Self {
769        Self::new(stable_file_version())
770    }
771}
772
773impl TryFrom<pb::manifest::DataStorageFormat> for DataStorageFormat {
774    type Error = Error;
775
776    fn try_from(pb: pb::manifest::DataStorageFormat) -> Result<Self> {
777        Ok(Self {
778            file_format: pb.file_format,
779            version: ConcreteFileVersion::from_manifest_string(&pb.version)?,
780        })
781    }
782}
783
784/// Options controlling how a new [`Manifest`] is assembled from a transaction.
785///
786/// The timestamp arrives already resolved to nanoseconds since the Unix epoch.
787/// Callers own the clock so that a caller wanting a mockable one keeps it: the
788/// `lance` crate mocks `SystemTime` under `cfg(test)`, which only takes effect in
789/// that crate.
790#[derive(Debug, Clone)]
791pub struct ManifestBuildConfig {
792    /// Recompute the manifest's feature flags from the fragments and settings
793    /// below. False leaves whatever flags the previous manifest carried.
794    pub auto_set_feature_flags: bool,
795    /// Value for the new manifest's timestamp, in nanoseconds since the Unix epoch.
796    pub timestamp_nanos: u128,
797    /// Request the stable row id feature. The flag is also inherited from the
798    /// previous manifest, so false does not turn it off for a dataset that has it.
799    pub use_stable_row_ids: bool,
800    /// Overwrite only: force the legacy (true) or v2 (false) file format. `None`
801    /// keeps the format the dataset already had.
802    pub use_legacy_format: Option<bool>,
803    /// Overwrite only: force this storage format, taking precedence over
804    /// `use_legacy_format`. `None` keeps the format the dataset already had.
805    pub storage_format: Option<DataStorageFormat>,
806    /// Skip writing a detached transaction file for this commit.
807    pub disable_transaction_file: bool,
808    /// When `Some`, this commit is the second step of `migrate_to_stable_row_ids`.
809    /// It bypasses the "cannot enable stable row ids on existing dataset" guard and
810    /// sets `manifest.next_row_id` to the provided value before activating the flag.
811    pub migration_next_row_id: Option<u64>,
812    /// Whether this commit atomically activates non-reusable field IDs.
813    pub activate_non_reusable_field_ids: bool,
814    /// Row lineage sequences of the current manifest's fragments that live
815    /// outside the manifest, read ahead of the build. An update that rewrites
816    /// rows needs the existing row ids and created-at versions to carry each
817    /// row's lineage over, and a partial column rewrite needs the existing
818    /// last-updated-at versions; the build cannot read a data file itself. Only
819    /// consulted for fragments whose sequences are spilled.
820    pub spilled_row_lineage: std::sync::Arc<crate::rowids::version::SpilledRowLineage>,
821}
822
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
824pub enum VersionPart {
825    Major,
826    Minor,
827    Patch,
828}
829
830fn bump_version(version: &mut semver::Version, part: VersionPart) {
831    match part {
832        VersionPart::Major => {
833            version.major += 1;
834            version.minor = 0;
835            version.patch = 0;
836        }
837        VersionPart::Minor => {
838            version.minor += 1;
839            version.patch = 0;
840        }
841        VersionPart::Patch => {
842            version.patch += 1;
843        }
844    }
845}
846
847impl WriterVersion {
848    /// Split a version string into clean version (major.minor.patch), prerelease, and build metadata.
849    ///
850    /// Returns None if the input is not a valid semver string.
851    ///
852    /// For example:
853    /// - "2.0.0-rc.1" -> Some(("2.0.0", Some("rc.1"), None))
854    /// - "2.0.0-rc.1+build.123" -> Some(("2.0.0", Some("rc.1"), Some("build.123")))
855    /// - "2.0.0+build.123" -> Some(("2.0.0", None, Some("build.123")))
856    /// - "not-a-version" -> None
857    fn split_version(full_version: &str) -> Option<(String, Option<String>, Option<String>)> {
858        let mut parsed = semver::Version::parse(full_version).ok()?;
859
860        let prerelease = if parsed.pre.is_empty() {
861            None
862        } else {
863            Some(parsed.pre.to_string())
864        };
865
866        let build_metadata = if parsed.build.is_empty() {
867            None
868        } else {
869            Some(parsed.build.to_string())
870        };
871
872        // Remove prerelease and build metadata to get clean version
873        parsed.pre = semver::Prerelease::EMPTY;
874        parsed.build = semver::BuildMetadata::EMPTY;
875        Some((parsed.to_string(), prerelease, build_metadata))
876    }
877
878    /// Try to parse the version string as a semver string. Returns None if
879    /// not successful.
880    #[deprecated(note = "Use `lance_lib_version()` instead")]
881    pub fn semver(&self) -> Option<(u32, u32, u32, Option<&str>)> {
882        // First split by '-' to separate the version from the pre-release tag
883        let (version_part, tag) = if let Some(dash_idx) = self.version.find('-') {
884            (
885                &self.version[..dash_idx],
886                Some(&self.version[dash_idx + 1..]),
887            )
888        } else {
889            (self.version.as_str(), None)
890        };
891
892        let mut parts = version_part.split('.');
893        let major = parts.next().unwrap_or("0").parse().ok()?;
894        let minor = parts.next().unwrap_or("0").parse().ok()?;
895        let patch = parts.next().unwrap_or("0").parse().ok()?;
896
897        Some((major, minor, patch, tag))
898    }
899
900    /// If the library is "lance", parse the version as semver and return it.
901    /// Returns None if the library is not "lance" or the version cannot be parsed as semver.
902    ///
903    /// This method reconstructs the full semantic version by combining the version field
904    /// with the prerelease and build_metadata fields (if present). For example:
905    /// - version="2.0.0" + prerelease=Some("rc.1") -> "2.0.0-rc.1"
906    /// - version="2.0.0" + prerelease=Some("rc.1") + build_metadata=Some("build.123") -> "2.0.0-rc.1+build.123"
907    pub fn lance_lib_version(&self) -> Option<semver::Version> {
908        if self.library != "lance" {
909            return None;
910        }
911
912        let mut version = semver::Version::parse(&self.version).ok()?;
913
914        if let Some(ref prerelease) = self.prerelease {
915            version.pre = semver::Prerelease::new(prerelease).ok()?;
916        }
917
918        if let Some(ref build_metadata) = self.build_metadata {
919            version.build = semver::BuildMetadata::new(build_metadata).ok()?;
920        }
921
922        Some(version)
923    }
924
925    #[deprecated(
926        note = "Use `lance_lib_version()` instead, which safely checks the library field and returns Option"
927    )]
928    #[allow(deprecated)]
929    pub fn semver_or_panic(&self) -> (u32, u32, u32, Option<&str>) {
930        self.semver()
931            .unwrap_or_else(|| panic!("Invalid writer version: {}", self.version))
932    }
933
934    /// Check if this is a Lance library version older than the given major/minor/patch.
935    ///
936    /// # Panics
937    ///
938    /// Panics if the library is not "lance" or the version cannot be parsed as semver.
939    #[deprecated(note = "Use `lance_lib_version()` and its `older_than` method instead.")]
940    pub fn older_than(&self, major: u32, minor: u32, patch: u32) -> bool {
941        let version = self
942            .lance_lib_version()
943            .expect("Not lance library or invalid version");
944        let other = semver::Version {
945            major: major.into(),
946            minor: minor.into(),
947            patch: patch.into(),
948            pre: semver::Prerelease::EMPTY,
949            build: semver::BuildMetadata::EMPTY,
950        };
951        version < other
952    }
953
954    #[deprecated(note = "This is meant for testing and will be made private in future version.")]
955    pub fn bump(&self, part: VersionPart, keep_tag: bool) -> Self {
956        let mut version = self.lance_lib_version().expect("Should be lance version");
957        bump_version(&mut version, part);
958        if !keep_tag {
959            version.pre = semver::Prerelease::EMPTY;
960        }
961        let (clean_version, prerelease, build_metadata) = Self::split_version(&version.to_string())
962            .expect("Bumped version should be valid semver");
963        Self {
964            library: self.library.clone(),
965            version: clean_version,
966            prerelease,
967            build_metadata,
968        }
969    }
970}
971
972impl Default for WriterVersion {
973    #[cfg(not(test))]
974    fn default() -> Self {
975        let full_version = env!("CARGO_PKG_VERSION");
976        let (version, prerelease, build_metadata) =
977            Self::split_version(full_version).expect("CARGO_PKG_VERSION should be valid semver");
978        Self {
979            library: "lance".to_string(),
980            version,
981            prerelease,
982            build_metadata,
983        }
984    }
985
986    // Unit tests always run as if they are in the next version.
987    #[cfg(test)]
988    #[allow(deprecated)]
989    fn default() -> Self {
990        let full_version = env!("CARGO_PKG_VERSION");
991        let (version, prerelease, build_metadata) =
992            Self::split_version(full_version).expect("CARGO_PKG_VERSION should be valid semver");
993        Self {
994            library: "lance".to_string(),
995            version,
996            prerelease,
997            build_metadata,
998        }
999        .bump(VersionPart::Patch, true)
1000    }
1001}
1002
1003impl ProtoStruct for Manifest {
1004    type Proto = pb::Manifest;
1005}
1006
1007impl From<pb::BasePath> for BasePath {
1008    fn from(p: pb::BasePath) -> Self {
1009        Self::new(p.id, p.path, p.name, p.is_dataset_root)
1010    }
1011}
1012
1013impl From<BasePath> for pb::BasePath {
1014    fn from(p: BasePath) -> Self {
1015        Self {
1016            id: p.id,
1017            name: p.name,
1018            is_dataset_root: p.is_dataset_root,
1019            path: p.path,
1020        }
1021    }
1022}
1023
1024impl TryFrom<pb::Manifest> for Manifest {
1025    type Error = Error;
1026
1027    fn try_from(p: pb::Manifest) -> Result<Self> {
1028        let timestamp_nanos = p.timestamp.map(|ts| {
1029            let sec = ts.seconds as u128 * 1e9 as u128;
1030            let nanos = ts.nanos as u128;
1031            sec + nanos
1032        });
1033        // We only use the writer version if it is fully set.
1034        let writer_version = match p.writer_version {
1035            Some(pb::manifest::WriterVersion {
1036                library,
1037                version,
1038                prerelease,
1039                build_metadata,
1040            }) => Some(WriterVersion {
1041                library,
1042                version,
1043                prerelease,
1044                build_metadata,
1045            }),
1046            _ => None,
1047        };
1048        let mut interner = DataFileFieldInterner::default();
1049        let fragments = Arc::new(
1050            p.fragments
1051                .into_iter()
1052                .map(|f| interner.intern_fragment(f))
1053                .collect::<Result<Vec<_>>>()?,
1054        );
1055        let fragment_offsets = compute_fragment_offsets(fragments.as_slice());
1056        let fields_with_meta = FieldsWithMeta {
1057            fields: Fields(p.fields),
1058            metadata: p.schema_metadata,
1059        };
1060
1061        if FLAG_STABLE_ROW_IDS & p.reader_feature_flags != 0
1062            && !fragments.iter().all(|frag| frag.row_id_meta.is_some())
1063        {
1064            return Err(Error::internal("All fragments must have row ids"));
1065        }
1066
1067        let data_storage_format = match p.data_format {
1068            None => {
1069                if let Some(inferred_version) = Fragment::try_infer_version(fragments.as_ref())? {
1070                    // If there are fragments, they are a better indicator
1071                    DataStorageFormat::new(inferred_version)
1072                } else {
1073                    // No fragments to inspect, best we can do is look at writer flags
1074                    if has_deprecated_v2_feature_flag(p.writer_feature_flags) {
1075                        DataStorageFormat::new(stable_file_version())
1076                    } else {
1077                        DataStorageFormat::new(ConcreteFileVersion::V1)
1078                    }
1079                }
1080            }
1081            Some(format) => DataStorageFormat::try_from(format)?,
1082        };
1083
1084        let schema = Schema::try_from(fields_with_meta)?;
1085
1086        Ok(Self {
1087            schema,
1088            version: p.version,
1089            branch: p.branch,
1090            writer_version,
1091            version_aux_data: p.version_aux_data as usize,
1092            index_section: p.index_section.map(|i| i as usize),
1093            timestamp_nanos: timestamp_nanos.unwrap_or(0),
1094            tag: if p.tag.is_empty() { None } else { Some(p.tag) },
1095            reader_feature_flags: p.reader_feature_flags,
1096            writer_feature_flags: p.writer_feature_flags,
1097            max_fragment_id: p.max_fragment_id,
1098            max_allocated_field_id: p.max_allocated_field_id,
1099            fragments,
1100            transaction_file: if p.transaction_file.is_empty() {
1101                None
1102            } else {
1103                Some(p.transaction_file)
1104            },
1105            transaction_section: p
1106                .transaction_section
1107                .or(p.transaction_section_deprecated)
1108                .map(|i| i as usize),
1109            fragment_offsets,
1110            next_row_id: p.next_row_id,
1111            data_storage_format,
1112            config: p.config,
1113            table_metadata: p.table_metadata,
1114            base_paths: p
1115                .base_paths
1116                .iter()
1117                .map(|item| (item.id, item.clone().into()))
1118                .collect(),
1119        })
1120    }
1121}
1122
1123impl From<&Manifest> for pb::Manifest {
1124    fn from(m: &Manifest) -> Self {
1125        let timestamp_nanos = if m.timestamp_nanos == 0 {
1126            None
1127        } else {
1128            let nanos = m.timestamp_nanos % 1e9 as u128;
1129            let seconds = ((m.timestamp_nanos - nanos) / 1e9 as u128) as i64;
1130            Some(Timestamp {
1131                seconds,
1132                nanos: nanos as i32,
1133            })
1134        };
1135        let fields_with_meta: FieldsWithMeta = (&m.schema).into();
1136        Self {
1137            fields: fields_with_meta.fields.0,
1138            schema_metadata: m
1139                .schema
1140                .metadata
1141                .iter()
1142                .map(|(k, v)| (k.clone(), v.as_bytes().to_vec()))
1143                .collect(),
1144            version: m.version,
1145            branch: m.branch.clone(),
1146            clustering: Vec::new(),
1147            max_clustering_id: 0,
1148            writer_version: m
1149                .writer_version
1150                .as_ref()
1151                .map(|wv| pb::manifest::WriterVersion {
1152                    library: wv.library.clone(),
1153                    version: wv.version.clone(),
1154                    prerelease: wv.prerelease.clone(),
1155                    build_metadata: wv.build_metadata.clone(),
1156                }),
1157            fragments: m.fragments.iter().map(pb::DataFragment::from).collect(),
1158            fragment_tree: None,
1159            table_metadata: m.table_metadata.clone(),
1160            version_aux_data: m.version_aux_data as u64,
1161            index_section: m.index_section.map(|i| i as u64),
1162            timestamp: timestamp_nanos,
1163            tag: m.tag.clone().unwrap_or_default(),
1164            reader_feature_flags: m.reader_feature_flags,
1165            writer_feature_flags: m.writer_feature_flags,
1166            max_fragment_id: m.max_fragment_id,
1167            max_allocated_field_id: m.max_allocated_field_id,
1168            transaction_file: m.transaction_file.clone().unwrap_or_default(),
1169            next_row_id: m.next_row_id,
1170            data_format: Some(pb::manifest::DataStorageFormat {
1171                file_format: m.data_storage_format.file_format.clone(),
1172                version: m
1173                    .data_storage_format
1174                    .version
1175                    .to_manifest_string()
1176                    .to_string(),
1177            }),
1178            config: m.config.clone(),
1179            base_paths: m
1180                .base_paths
1181                .values()
1182                .map(|base_path| pb::BasePath {
1183                    id: base_path.id,
1184                    name: base_path.name.clone(),
1185                    is_dataset_root: base_path.is_dataset_root,
1186                    path: base_path.path.clone(),
1187                })
1188                .collect(),
1189            transaction_section: m.transaction_section.map(|i| i as u64),
1190            transaction_section_deprecated: None,
1191        }
1192    }
1193}
1194
1195#[async_trait]
1196pub trait SelfDescribingFileReader {
1197    /// Open a file reader without any cached schema
1198    ///
1199    /// In this case the schema will first need to be loaded
1200    /// from the file itself.
1201    ///
1202    /// When loading files from a dataset it is preferable to use
1203    /// the fragment reader to avoid this overhead.
1204    async fn try_new_self_described(
1205        object_store: &ObjectStore,
1206        path: &Path,
1207        cache: Option<&LanceCache>,
1208    ) -> Result<Self>
1209    where
1210        Self: Sized,
1211    {
1212        let reader = object_store.open(path).await?;
1213        Self::try_new_self_described_from_reader(reader.into(), cache).await
1214    }
1215
1216    async fn try_new_self_described_from_reader(
1217        reader: Arc<dyn Reader>,
1218        cache: Option<&LanceCache>,
1219    ) -> Result<Self>
1220    where
1221        Self: Sized;
1222}
1223
1224#[async_trait]
1225impl SelfDescribingFileReader for V1FileReader {
1226    async fn try_new_self_described_from_reader(
1227        reader: Arc<dyn Reader>,
1228        cache: Option<&LanceCache>,
1229    ) -> Result<Self> {
1230        let metadata = Self::read_metadata(reader.as_ref(), cache).await?;
1231        let manifest_position = metadata.manifest_position.ok_or(Error::internal(format!(
1232            "Attempt to open file at {} as self-describing but it did not contain a manifest",
1233            reader.path(),
1234        )))?;
1235        let mut manifest: Manifest = read_struct(reader.as_ref(), manifest_position).await?;
1236        populate_manifest_schema_dictionaries(&mut manifest, reader.as_ref()).await?;
1237        let schema = manifest.schema;
1238        let max_field_id = schema.max_field_id().unwrap_or_default();
1239        Self::try_new_from_reader(
1240            reader.path(),
1241            reader.clone(),
1242            Some(metadata),
1243            schema,
1244            0,
1245            0,
1246            max_field_id,
1247            cache,
1248        )
1249        .await
1250    }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255    use crate::feature_flags::{FLAG_NON_REUSABLE_FIELD_IDS, FLAG_USE_V2_FORMAT_DEPRECATED};
1256    use crate::format::overlay::{DataOverlayFile, OverlayCoverage, TOMBSTONE_FIELD_ID};
1257    use crate::format::{DataFile, DeletionFile, DeletionFileType};
1258    use std::num::NonZero;
1259
1260    use super::*;
1261
1262    use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1263    use lance_core::datatypes::Field;
1264    use roaring::RoaringBitmap;
1265
1266    #[test]
1267    fn clustering_proto_retains_definitions_references_and_generic_metadata() {
1268        let metadata = HashMap::from([
1269            ("org.example.cluster.bucket".into(), "[0, 100)".into()),
1270            (
1271                "org.example.unknown".into(),
1272                "  opaque value: 東京  ".into(),
1273            ),
1274            ("org.example.empty".into(), String::new()),
1275        ]);
1276        // Test the persisted messages only; runtime maintenance is not enabled yet.
1277        let manifest = pb::Manifest {
1278            writer_feature_flags: crate::feature_flags::FLAG_OBJECT_METADATA,
1279            max_clustering_id: 7,
1280            clustering: vec![
1281                pb::Clustering {
1282                    id: 1,
1283                    columns: vec![0, 1],
1284                    provider: "org.example.cluster".into(),
1285                    version: 8,
1286                },
1287                pb::Clustering {
1288                    id: 5,
1289                    columns: vec![1],
1290                    provider: "org.example.other".into(),
1291                    version: 9,
1292                },
1293            ],
1294            table_metadata: HashMap::from([
1295                (
1296                    "org.example.cluster.1.boundaries".into(),
1297                    "[0, 100, 200]".into(),
1298                ),
1299                (
1300                    "org.example.cluster.1.unknown_option".into(),
1301                    "  opaque value: 東京  ".into(),
1302                ),
1303            ]),
1304            fragments: vec![
1305                pb::DataFragment {
1306                    id: 1,
1307                    clustering_id: Some(1),
1308                    metadata: metadata.clone(),
1309                    ..Default::default()
1310                },
1311                pb::DataFragment {
1312                    id: 2,
1313                    clustering_id: Some(5),
1314                    ..Default::default()
1315                },
1316                pb::DataFragment {
1317                    id: 3,
1318                    clustering_id: None,
1319                    metadata: metadata.clone(),
1320                    ..Default::default()
1321                },
1322            ],
1323            ..Default::default()
1324        };
1325        assert_eq!(
1326            pb::Manifest::decode(manifest.encode_to_vec().as_slice()).unwrap(),
1327            manifest
1328        );
1329        assert_eq!(manifest.reader_feature_flags, 0);
1330        let segments = pb::IndexSection {
1331            indices: vec![pb::IndexMetadata {
1332                name: "vectors".into(),
1333                fields: vec![1],
1334                clustering_id: Some(5),
1335                metadata,
1336                ..Default::default()
1337            }],
1338        };
1339        assert_eq!(
1340            pb::IndexSection::decode(segments.encode_to_vec().as_slice()).unwrap(),
1341            segments
1342        );
1343        assert_eq!(manifest.clustering.len(), 2);
1344        assert_eq!(manifest.max_clustering_id, 7);
1345        assert_eq!(manifest.fragments[2].clustering_id, None);
1346        let allocation_only = pb::Manifest {
1347            writer_feature_flags: crate::feature_flags::FLAG_OBJECT_METADATA,
1348            max_clustering_id: manifest.max_clustering_id,
1349            ..Default::default()
1350        };
1351        assert_eq!(
1352            pb::Manifest::decode(allocation_only.encode_to_vec().as_slice()).unwrap(),
1353            allocation_only
1354        );
1355        // Proto presence distinguishes an absent reference from an invalid zero.
1356        let zero = pb::DataFragment {
1357            clustering_id: Some(0),
1358            ..Default::default()
1359        };
1360        assert_eq!(
1361            pb::DataFragment::decode(zero.encode_to_vec().as_slice())
1362                .unwrap()
1363                .clustering_id,
1364            Some(0)
1365        );
1366    }
1367
1368    /// A shallow clone points every local file at the parent through `base_id`.
1369    /// An overlay's data file lives in the parent too, so it needs the same
1370    /// stamp; without it the clone looks for the overlay under its own root.
1371    #[test]
1372    fn shallow_clone_stamps_base_id_on_overlay_files() {
1373        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
1374            "a",
1375            arrow_schema::DataType::Int64,
1376            false,
1377        )]);
1378        let schema = Schema::try_from(&arrow_schema).unwrap();
1379
1380        let mut fragment = Fragment::with_file_legacy(0, "base.lance", &schema, Some(10));
1381        fragment.overlays = vec![DataOverlayFile {
1382            data_file: DataFile::new_legacy_from_fields("overlay.lance", vec![0], None),
1383            coverage: OverlayCoverage::Shared(Arc::new(RoaringBitmap::from_iter([0_u32]))),
1384            committed_version: 1,
1385        }];
1386        let manifest = Manifest::new(
1387            schema,
1388            Arc::new(vec![fragment]),
1389            DataStorageFormat::default(),
1390            HashMap::new(),
1391        );
1392
1393        let cloned = manifest.shallow_clone(
1394            Some("parent".to_string()),
1395            "memory://parent".to_string(),
1396            7,
1397            None,
1398            String::new(),
1399        );
1400
1401        let fragment = &cloned.fragments[0];
1402        assert_eq!(fragment.files[0].base_id, Some(7));
1403        assert_eq!(
1404            fragment.overlays[0].data_file.base_id,
1405            Some(7),
1406            "the overlay's data file resolves against the parent as well"
1407        );
1408    }
1409
1410    #[test]
1411    fn old_empty_manifest_recovers_v1_or_current_stable() {
1412        let old_manifest = pb::Manifest {
1413            data_format: None,
1414            ..Default::default()
1415        };
1416        let recovered_v1 = Manifest::try_from(old_manifest.clone()).unwrap();
1417        assert_eq!(
1418            recovered_v1.data_storage_format.lance_file_format(),
1419            ConcreteFileVersion::V1
1420        );
1421
1422        let recovered_stable = Manifest::try_from(pb::Manifest {
1423            writer_feature_flags: FLAG_USE_V2_FORMAT_DEPRECATED,
1424            ..old_manifest
1425        })
1426        .unwrap();
1427        assert_eq!(
1428            recovered_stable.data_storage_format.lance_file_format(),
1429            stable_file_version()
1430        );
1431    }
1432
1433    #[test]
1434    fn manifest_persistence_rejects_selectors_and_public_aliases() {
1435        for version in ["stable", "next", "legacy", "0.3"] {
1436            let manifest = pb::Manifest {
1437                data_format: Some(pb::manifest::DataStorageFormat {
1438                    file_format: LANCE_FORMAT_NAME.to_string(),
1439                    version: version.to_string(),
1440                }),
1441                ..Default::default()
1442            };
1443            assert!(Manifest::try_from(manifest).is_err(), "accepted {version}");
1444        }
1445    }
1446
1447    #[test]
1448    fn manifest_codec_writes_canonical_exact_string() {
1449        let manifest = Manifest::new(
1450            Schema::default(),
1451            Arc::new(Vec::new()),
1452            DataStorageFormat::new(ConcreteFileVersion::V2_0),
1453            HashMap::new(),
1454        );
1455        let encoded = pb::Manifest::from(&manifest);
1456        assert_eq!(encoded.data_format.unwrap().version, "2.0");
1457    }
1458
1459    #[test]
1460    fn missing_format_infers_exact_version_and_rejects_mixed_files() {
1461        let v2_0 = Fragment::new(0).with_file(
1462            "v2_0.lance",
1463            vec![0],
1464            vec![0],
1465            ConcreteFileVersion::V2_0,
1466            None,
1467        );
1468        let manifest = Manifest::new(
1469            Schema::default(),
1470            Arc::new(vec![v2_0.clone()]),
1471            DataStorageFormat::new(ConcreteFileVersion::V1),
1472            HashMap::new(),
1473        );
1474        let mut encoded = pb::Manifest::from(&manifest);
1475        encoded.data_format = None;
1476        let recovered = Manifest::try_from(encoded).unwrap();
1477        assert_eq!(
1478            recovered.data_storage_format.lance_file_format(),
1479            ConcreteFileVersion::V2_0
1480        );
1481
1482        let v2_1 = Fragment::new(1).with_file(
1483            "v2_1.lance",
1484            vec![0],
1485            vec![0],
1486            ConcreteFileVersion::V2_1,
1487            None,
1488        );
1489        let mixed_manifest = Manifest::new(
1490            Schema::default(),
1491            Arc::new(vec![v2_0, v2_1]),
1492            DataStorageFormat::new(ConcreteFileVersion::V2_0),
1493            HashMap::new(),
1494        );
1495        let mut encoded = pb::Manifest::from(&mixed_manifest);
1496        encoded.data_format = None;
1497        let error = Manifest::try_from(encoded).unwrap_err();
1498        assert!(
1499            error
1500                .to_string()
1501                .contains("All data files must have the same version")
1502        );
1503    }
1504
1505    #[test]
1506    fn test_detach_sparse_inline_row_ids_copies_only_small_shares() {
1507        let arrow_schema = ArrowSchema::new(vec![ArrowField::new("a", DataType::Int32, false)]);
1508        // Two 10 byte row id sequences sliced out of one buffer.
1509        let buffer = Bytes::from(vec![7u8; 64]);
1510        let fragments = [0..10, 20..30].into_iter().enumerate().map(|(id, range)| {
1511            let mut fragment = Fragment::new(id as u64);
1512            fragment.row_id_meta = Some(RowIdMeta::Inline(InlineRowIds::from(buffer.slice(range))));
1513            fragment
1514        });
1515        let mut manifest = Manifest::new(
1516            Schema::try_from(&arrow_schema).unwrap(),
1517            Arc::new(fragments.collect()),
1518            DataStorageFormat::default(),
1519            HashMap::new(),
1520        );
1521        let ptr = |manifest: &Manifest, i: usize| match &manifest.fragments[i].row_id_meta {
1522            Some(RowIdMeta::Inline(data)) => data.bytes().as_ptr(),
1523            _ => unreachable!(),
1524        };
1525        let in_buffer = |p: *const u8| buffer.as_ptr_range().contains(&p);
1526
1527        // 20 of 80 bytes is a quarter: the slices stay.
1528        manifest.detach_sparse_inline_row_ids(80);
1529        assert!(in_buffer(ptr(&manifest, 0)) && in_buffer(ptr(&manifest, 1)));
1530
1531        // 20 of 81 bytes is below a quarter: copied out, contents intact.
1532        manifest.detach_sparse_inline_row_ids(81);
1533        assert!(!in_buffer(ptr(&manifest, 0)) && !in_buffer(ptr(&manifest, 1)));
1534        let Some(RowIdMeta::Inline(copied)) = &manifest.fragments[1].row_id_meta else {
1535            unreachable!()
1536        };
1537        assert_eq!(&**copied, &buffer[20..30]);
1538    }
1539
1540    #[test]
1541    fn test_writer_version() {
1542        let wv = WriterVersion::default();
1543        assert_eq!(wv.library, "lance");
1544
1545        // Parse the actual cargo version to check if it has a pre-release tag
1546        let cargo_version = env!("CARGO_PKG_VERSION");
1547        let expected_tag = if cargo_version.contains('-') {
1548            Some(cargo_version.split('-').nth(1).unwrap())
1549        } else {
1550            None
1551        };
1552
1553        // Verify the version field contains only major.minor.patch
1554        let version_parts: Vec<&str> = wv.version.split('.').collect();
1555        assert_eq!(
1556            version_parts.len(),
1557            3,
1558            "Version should be major.minor.patch"
1559        );
1560        assert!(
1561            !wv.version.contains('-'),
1562            "Version field should not contain prerelease"
1563        );
1564
1565        // Verify the prerelease field matches the expected tag
1566        assert_eq!(wv.prerelease.as_deref(), expected_tag);
1567        // Build metadata should be None for default version
1568        assert_eq!(wv.build_metadata, None);
1569
1570        // Verify lance_lib_version() reconstructs the full semver correctly
1571        let version = wv.lance_lib_version().unwrap();
1572        assert_eq!(
1573            version.major,
1574            env!("CARGO_PKG_VERSION_MAJOR").parse::<u64>().unwrap()
1575        );
1576        assert_eq!(
1577            version.minor,
1578            env!("CARGO_PKG_VERSION_MINOR").parse::<u64>().unwrap()
1579        );
1580        assert_eq!(
1581            version.patch,
1582            // Unit tests run against (major,minor,patch + 1)
1583            env!("CARGO_PKG_VERSION_PATCH").parse::<u64>().unwrap() + 1
1584        );
1585        assert_eq!(version.pre.as_str(), expected_tag.unwrap_or(""));
1586
1587        for part in &[VersionPart::Major, VersionPart::Minor, VersionPart::Patch] {
1588            let mut bumped_version = version.clone();
1589            bump_version(&mut bumped_version, *part);
1590            assert!(version < bumped_version);
1591        }
1592    }
1593
1594    #[test]
1595    fn test_writer_version_split() {
1596        // Test splitting version with prerelease
1597        let (version, prerelease, build_metadata) =
1598            WriterVersion::split_version("2.0.0-rc.1").unwrap();
1599        assert_eq!(version, "2.0.0");
1600        assert_eq!(prerelease, Some("rc.1".to_string()));
1601        assert_eq!(build_metadata, None);
1602
1603        // Test splitting version without prerelease
1604        let (version, prerelease, build_metadata) = WriterVersion::split_version("2.0.0").unwrap();
1605        assert_eq!(version, "2.0.0");
1606        assert_eq!(prerelease, None);
1607        assert_eq!(build_metadata, None);
1608
1609        // Test splitting version with prerelease and build metadata
1610        let (version, prerelease, build_metadata) =
1611            WriterVersion::split_version("2.0.0-rc.1+build.123").unwrap();
1612        assert_eq!(version, "2.0.0");
1613        assert_eq!(prerelease, Some("rc.1".to_string()));
1614        assert_eq!(build_metadata, Some("build.123".to_string()));
1615
1616        // Test splitting version with only build metadata
1617        let (version, prerelease, build_metadata) =
1618            WriterVersion::split_version("2.0.0+build.123").unwrap();
1619        assert_eq!(version, "2.0.0");
1620        assert_eq!(prerelease, None);
1621        assert_eq!(build_metadata, Some("build.123".to_string()));
1622
1623        // Test with invalid version returns None
1624        assert!(WriterVersion::split_version("not-a-version").is_none());
1625    }
1626
1627    #[test]
1628    fn test_writer_version_comparison_with_prerelease() {
1629        let v1 = WriterVersion {
1630            library: "lance".to_string(),
1631            version: "2.0.0".to_string(),
1632            prerelease: Some("rc.1".to_string()),
1633            build_metadata: None,
1634        };
1635
1636        let v2 = WriterVersion {
1637            library: "lance".to_string(),
1638            version: "2.0.0".to_string(),
1639            prerelease: None,
1640            build_metadata: None,
1641        };
1642
1643        let semver1 = v1.lance_lib_version().unwrap();
1644        let semver2 = v2.lance_lib_version().unwrap();
1645
1646        // rc.1 should be less than the release version
1647        assert!(semver1 < semver2);
1648    }
1649
1650    #[test]
1651    fn test_writer_version_with_build_metadata() {
1652        let v = WriterVersion {
1653            library: "lance".to_string(),
1654            version: "2.0.0".to_string(),
1655            prerelease: Some("rc.1".to_string()),
1656            build_metadata: Some("build.123".to_string()),
1657        };
1658
1659        let semver = v.lance_lib_version().unwrap();
1660        assert_eq!(semver.to_string(), "2.0.0-rc.1+build.123");
1661        assert_eq!(semver.major, 2);
1662        assert_eq!(semver.minor, 0);
1663        assert_eq!(semver.patch, 0);
1664        assert_eq!(semver.pre.as_str(), "rc.1");
1665        assert_eq!(semver.build.as_str(), "build.123");
1666    }
1667
1668    #[test]
1669    fn test_writer_version_non_semver() {
1670        // Test that Lance library can have non-semver version strings
1671        let v = WriterVersion {
1672            library: "lance".to_string(),
1673            version: "custom-build-v1".to_string(),
1674            prerelease: None,
1675            build_metadata: None,
1676        };
1677
1678        // lance_lib_version should return None for non-semver
1679        assert!(v.lance_lib_version().is_none());
1680
1681        // But the WriterVersion itself should still be valid and usable
1682        assert_eq!(v.library, "lance");
1683        assert_eq!(v.version, "custom-build-v1");
1684    }
1685
1686    #[test]
1687    #[allow(deprecated)]
1688    fn test_older_than_with_prerelease() {
1689        // Test that older_than correctly handles prerelease
1690        let v_rc = WriterVersion {
1691            library: "lance".to_string(),
1692            version: "2.0.0".to_string(),
1693            prerelease: Some("rc.1".to_string()),
1694            build_metadata: None,
1695        };
1696
1697        // 2.0.0-rc.1 should be older than 2.0.0
1698        assert!(v_rc.older_than(2, 0, 0));
1699
1700        // 2.0.0-rc.1 should be older than 2.0.1
1701        assert!(v_rc.older_than(2, 0, 1));
1702
1703        // 2.0.0-rc.1 should not be older than 1.9.9
1704        assert!(!v_rc.older_than(1, 9, 9));
1705
1706        let v_release = WriterVersion {
1707            library: "lance".to_string(),
1708            version: "2.0.0".to_string(),
1709            prerelease: None,
1710            build_metadata: None,
1711        };
1712
1713        // 2.0.0 should not be older than 2.0.0
1714        assert!(!v_release.older_than(2, 0, 0));
1715
1716        // 2.0.0 should be older than 2.0.1
1717        assert!(v_release.older_than(2, 0, 1));
1718    }
1719
1720    #[test]
1721    fn test_fragments_by_offset_range() {
1722        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
1723            "a",
1724            arrow_schema::DataType::Int64,
1725            false,
1726        )]);
1727        let schema = Schema::try_from(&arrow_schema).unwrap();
1728        let fragments = vec![
1729            Fragment::with_file_legacy(0, "path1", &schema, Some(10)),
1730            Fragment::with_file_legacy(1, "path2", &schema, Some(15)),
1731            Fragment::with_file_legacy(2, "path3", &schema, Some(20)),
1732        ];
1733        let manifest = Manifest::new(
1734            schema,
1735            Arc::new(fragments),
1736            DataStorageFormat::default(),
1737            HashMap::new(),
1738        );
1739
1740        let actual = manifest.fragments_by_offset_range(0..10);
1741        assert_eq!(actual.len(), 1);
1742        assert_eq!(actual[0].0, 0);
1743        assert_eq!(actual[0].1.id, 0);
1744
1745        let actual = manifest.fragments_by_offset_range(5..15);
1746        assert_eq!(actual.len(), 2);
1747        assert_eq!(actual[0].0, 0);
1748        assert_eq!(actual[0].1.id, 0);
1749        assert_eq!(actual[1].0, 10);
1750        assert_eq!(actual[1].1.id, 1);
1751
1752        let actual = manifest.fragments_by_offset_range(15..50);
1753        assert_eq!(actual.len(), 2);
1754        assert_eq!(actual[0].0, 10);
1755        assert_eq!(actual[0].1.id, 1);
1756        assert_eq!(actual[1].0, 25);
1757        assert_eq!(actual[1].1.id, 2);
1758
1759        // Out of range
1760        let actual = manifest.fragments_by_offset_range(45..100);
1761        assert!(actual.is_empty());
1762
1763        assert!(manifest.fragments_by_offset_range(200..400).is_empty());
1764    }
1765
1766    #[test]
1767    fn test_max_field_id() {
1768        // Validate that max field id handles varying field ids by fragment.
1769        let mut field0 =
1770            Field::try_from(ArrowField::new("a", arrow_schema::DataType::Int64, false)).unwrap();
1771        field0.set_id(-1, &mut 0);
1772        let mut field2 =
1773            Field::try_from(ArrowField::new("b", arrow_schema::DataType::Int64, false)).unwrap();
1774        field2.set_id(-1, &mut 2);
1775
1776        let schema = Schema {
1777            fields: vec![field0, field2],
1778            metadata: Default::default(),
1779        };
1780        let fragments = vec![
1781            Fragment {
1782                id: 0,
1783                files: vec![DataFile::new_legacy_from_fields(
1784                    "path1",
1785                    vec![0, 1, 2],
1786                    None,
1787                )],
1788                overlays: vec![],
1789                deletion_file: None,
1790                row_id_meta: None,
1791                physical_rows: None,
1792                created_at_version_meta: None,
1793                last_updated_at_version_meta: None,
1794            },
1795            Fragment {
1796                id: 1,
1797                files: vec![
1798                    DataFile::new_legacy_from_fields("path2", vec![0, 1, 43], None),
1799                    DataFile::new_legacy_from_fields("path3", vec![2], None),
1800                ],
1801                overlays: vec![],
1802                deletion_file: None,
1803                row_id_meta: None,
1804                physical_rows: None,
1805                created_at_version_meta: None,
1806                last_updated_at_version_meta: None,
1807            },
1808        ];
1809
1810        let manifest = Manifest::new(
1811            schema,
1812            Arc::new(fragments),
1813            DataStorageFormat::default(),
1814            HashMap::new(),
1815        );
1816
1817        assert_eq!(manifest.max_field_id(), 43);
1818    }
1819
1820    #[test]
1821    fn test_max_field_id_includes_overlay_files() {
1822        let mut field0 =
1823            Field::try_from(ArrowField::new("a", arrow_schema::DataType::Int64, false)).unwrap();
1824        field0.set_id(-1, &mut 0);
1825        let schema = Schema {
1826            fields: vec![field0],
1827            metadata: Default::default(),
1828        };
1829
1830        let mut fragment = Fragment {
1831            id: 0,
1832            files: vec![DataFile::new_legacy_from_fields("path1", vec![0], None)],
1833            overlays: vec![],
1834            deletion_file: None,
1835            row_id_meta: None,
1836            physical_rows: None,
1837            created_at_version_meta: None,
1838            last_updated_at_version_meta: None,
1839        };
1840        fragment.overlays = vec![DataOverlayFile {
1841            data_file: DataFile::new_legacy_from_fields("overlay.lance", vec![43], None),
1842            coverage: OverlayCoverage::Shared(Arc::new(RoaringBitmap::from_iter([0_u32]))),
1843            committed_version: 1,
1844        }];
1845
1846        let manifest = Manifest::new(
1847            schema,
1848            Arc::new(vec![fragment]),
1849            DataStorageFormat::default(),
1850            HashMap::new(),
1851        );
1852
1853        assert_eq!(manifest.max_field_id(), 43);
1854    }
1855
1856    #[test]
1857    fn non_reusable_field_id_high_water_mark_survives_dropped_references_and_round_trip() {
1858        let arrow_schema = ArrowSchema::new(vec![
1859            ArrowField::new("a", arrow_schema::DataType::Int64, false),
1860            ArrowField::new("b", arrow_schema::DataType::Int64, false),
1861        ]);
1862        let schema = Schema::try_from(&arrow_schema).unwrap();
1863        let mut fragment = Fragment::new(0);
1864        fragment.files.push(DataFile::new(
1865            "ab.lance",
1866            vec![0, 1],
1867            vec![0, 1],
1868            ConcreteFileVersion::V2_0,
1869            None,
1870            None,
1871        ));
1872        let mut manifest = Manifest::new(
1873            schema,
1874            Arc::new(vec![fragment]),
1875            DataStorageFormat::default(),
1876            HashMap::new(),
1877        );
1878        manifest.activate_non_reusable_field_ids();
1879        manifest.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1880        assert_eq!(manifest.max_allocated_field_id, Some(1));
1881
1882        manifest.schema.fields.pop();
1883        let file = &mut Arc::make_mut(&mut manifest.fragments)[0].files[0];
1884        Arc::make_mut(&mut file.fields)[1] = TOMBSTONE_FIELD_ID;
1885        manifest.update_max_field_id();
1886
1887        assert_eq!(manifest.max_referenced_field_id(), 0);
1888        assert_eq!(manifest.max_field_id(), 1);
1889
1890        let encoded = manifest.serialized();
1891        let mut recovered =
1892            Manifest::try_from(pb::Manifest::decode(encoded.as_slice()).unwrap()).unwrap();
1893        assert_eq!(recovered.max_allocated_field_id, Some(1));
1894        assert_eq!(recovered.max_field_id(), 1);
1895        assert_eq!(
1896            recovered.fragments[0].files[0].fields.as_ref(),
1897            &[0, TOMBSTONE_FIELD_ID]
1898        );
1899        assert_eq!(
1900            recovered.fragments[0].files[0].column_indices.as_ref(),
1901            &[0, 1]
1902        );
1903        assert_eq!(
1904            recovered.reader_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS,
1905            0
1906        );
1907        assert_ne!(
1908            recovered.writer_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS,
1909            0
1910        );
1911
1912        recovered.schema.fields.push(
1913            Field::try_from(ArrowField::new("c", arrow_schema::DataType::Int64, false)).unwrap(),
1914        );
1915        let max_field_id = recovered.max_field_id();
1916        recovered
1917            .schema
1918            .try_set_field_id(Some(max_field_id))
1919            .unwrap();
1920        assert_eq!(recovered.schema.field("c").unwrap().id, 2);
1921        recovered.update_max_field_id();
1922        assert_eq!(recovered.max_allocated_field_id, Some(2));
1923    }
1924
1925    #[test]
1926    fn shallow_clone_preserves_non_reusable_field_id_allocation_state() {
1927        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
1928            "a",
1929            arrow_schema::DataType::Int64,
1930            false,
1931        )]);
1932        let schema = Schema::try_from(&arrow_schema).unwrap();
1933        let mut manifest = Manifest::new(
1934            schema,
1935            Arc::new(vec![]),
1936            DataStorageFormat::default(),
1937            HashMap::new(),
1938        );
1939        manifest.max_allocated_field_id = Some(41);
1940        manifest.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1941
1942        let cloned = manifest.shallow_clone(
1943            Some("parent".to_string()),
1944            "memory://parent".to_string(),
1945            7,
1946            None,
1947            String::new(),
1948        );
1949
1950        assert_eq!(cloned.max_allocated_field_id, Some(41));
1951        assert_eq!(cloned.reader_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS, 0);
1952        assert_ne!(cloned.writer_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS, 0);
1953    }
1954
1955    #[test]
1956    fn test_config() {
1957        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
1958            "a",
1959            arrow_schema::DataType::Int64,
1960            false,
1961        )]);
1962        let schema = Schema::try_from(&arrow_schema).unwrap();
1963        let fragments = vec![
1964            Fragment::with_file_legacy(0, "path1", &schema, Some(10)),
1965            Fragment::with_file_legacy(1, "path2", &schema, Some(15)),
1966            Fragment::with_file_legacy(2, "path3", &schema, Some(20)),
1967        ];
1968        let mut manifest = Manifest::new(
1969            schema,
1970            Arc::new(fragments),
1971            DataStorageFormat::default(),
1972            HashMap::new(),
1973        );
1974
1975        let mut config = manifest.config.clone();
1976        config.insert("lance.test".to_string(), "value".to_string());
1977        config.insert("other-key".to_string(), "other-value".to_string());
1978
1979        manifest.config_mut().extend(config.clone());
1980        assert_eq!(manifest.config, config.clone());
1981
1982        config.remove("other-key");
1983        manifest.config_mut().remove("other-key");
1984        assert_eq!(manifest.config, config);
1985    }
1986
1987    #[rstest::rstest]
1988    #[case::only_current(Some(22), None, Some(22))]
1989    #[case::only_deprecated(None, Some(21), Some(21))]
1990    #[case::current_wins_over_deprecated(Some(22), Some(21), Some(22))]
1991    #[case::neither(None, None, None)]
1992    fn test_transaction_section_field_precedence(
1993        #[case] current: Option<u64>,
1994        #[case] deprecated: Option<u64>,
1995        #[case] expected: Option<usize>,
1996    ) {
1997        let arrow_schema = ArrowSchema::new(vec![ArrowField::new("a", DataType::Int64, false)]);
1998        let manifest = Manifest::new(
1999            Schema::try_from(&arrow_schema).unwrap(),
2000            Arc::new(vec![]),
2001            DataStorageFormat::default(),
2002            HashMap::new(),
2003        );
2004        let mut pb_manifest = pb::Manifest::from(&manifest);
2005        pb_manifest.transaction_section = current;
2006        pb_manifest.transaction_section_deprecated = deprecated;
2007
2008        let manifest = Manifest::try_from(pb_manifest).unwrap();
2009        assert_eq!(manifest.transaction_section, expected);
2010
2011        // Whatever was read, only the current field is ever written back.
2012        let pb_manifest = pb::Manifest::from(&manifest);
2013        assert_eq!(pb_manifest.transaction_section, expected.map(|p| p as u64));
2014        assert_eq!(pb_manifest.transaction_section_deprecated, None);
2015    }
2016
2017    #[test]
2018    fn test_manifest_summary() {
2019        // Step 1: test empty manifest summary
2020        let arrow_schema = ArrowSchema::new(vec![
2021            ArrowField::new("id", arrow_schema::DataType::Int64, false),
2022            ArrowField::new("name", arrow_schema::DataType::Utf8, true),
2023        ]);
2024        let schema = Schema::try_from(&arrow_schema).unwrap();
2025
2026        let empty_manifest = Manifest::new(
2027            schema.clone(),
2028            Arc::new(vec![]),
2029            DataStorageFormat::default(),
2030            HashMap::new(),
2031        );
2032
2033        let empty_summary = empty_manifest.summary();
2034        assert_eq!(empty_summary.total_rows, 0);
2035        assert_eq!(empty_summary.total_files_size, 0);
2036        assert_eq!(empty_summary.total_fragments, 0);
2037        assert_eq!(empty_summary.total_data_files, 0);
2038        assert_eq!(empty_summary.total_deletion_file_rows, 0);
2039        assert_eq!(empty_summary.total_data_file_rows, 0);
2040        assert_eq!(empty_summary.total_deletion_files, 0);
2041
2042        // Step 2: write empty files and verify summary
2043        let empty_fragments = vec![
2044            Fragment::with_file_legacy(0, "empty_file1.lance", &schema, Some(0)),
2045            Fragment::with_file_legacy(1, "empty_file2.lance", &schema, Some(0)),
2046        ];
2047
2048        let empty_files_manifest = Manifest::new(
2049            schema.clone(),
2050            Arc::new(empty_fragments),
2051            DataStorageFormat::default(),
2052            HashMap::new(),
2053        );
2054
2055        let empty_files_summary = empty_files_manifest.summary();
2056        assert_eq!(empty_files_summary.total_rows, 0);
2057        assert_eq!(empty_files_summary.total_files_size, 0);
2058        assert_eq!(empty_files_summary.total_fragments, 2);
2059        assert_eq!(empty_files_summary.total_data_files, 2);
2060        assert_eq!(empty_files_summary.total_deletion_file_rows, 0);
2061        assert_eq!(empty_files_summary.total_data_file_rows, 0);
2062        assert_eq!(empty_files_summary.total_deletion_files, 0);
2063
2064        // Step 3: write real data and verify summary
2065        let real_fragments = vec![
2066            Fragment::with_file_legacy(0, "data_file1.lance", &schema, Some(100)),
2067            Fragment::with_file_legacy(1, "data_file2.lance", &schema, Some(250)),
2068            Fragment::with_file_legacy(2, "data_file3.lance", &schema, Some(75)),
2069        ];
2070
2071        let real_data_manifest = Manifest::new(
2072            schema.clone(),
2073            Arc::new(real_fragments),
2074            DataStorageFormat::default(),
2075            HashMap::new(),
2076        );
2077
2078        let real_data_summary = real_data_manifest.summary();
2079        assert_eq!(real_data_summary.total_rows, 425); // 100 + 250 + 75
2080        assert_eq!(real_data_summary.total_files_size, 0); // Zero for unknown
2081        assert_eq!(real_data_summary.total_fragments, 3);
2082        assert_eq!(real_data_summary.total_data_files, 3);
2083        assert_eq!(real_data_summary.total_deletion_file_rows, 0);
2084        assert_eq!(real_data_summary.total_data_file_rows, 425);
2085        assert_eq!(real_data_summary.total_deletion_files, 0);
2086
2087        // Step 4: write deletion files and verify summary
2088        let mut fragment_with_deletion = Fragment::new(0)
2089            .with_file(
2090                "data_with_deletion.lance",
2091                vec![0, 1],
2092                vec![0, 1],
2093                stable_file_version(),
2094                NonZero::new(1000),
2095            )
2096            .with_physical_rows(50);
2097        fragment_with_deletion.deletion_file = Some(DeletionFile {
2098            read_version: 123,
2099            id: 456,
2100            file_type: DeletionFileType::Array,
2101            num_deleted_rows: Some(10),
2102            base_id: None,
2103        });
2104
2105        let manifest_with_deletion = Manifest::new(
2106            schema,
2107            Arc::new(vec![fragment_with_deletion]),
2108            DataStorageFormat::default(),
2109            HashMap::new(),
2110        );
2111
2112        let deletion_summary = manifest_with_deletion.summary();
2113        assert_eq!(deletion_summary.total_rows, 40); // 50 - 10
2114        assert_eq!(deletion_summary.total_files_size, 1000);
2115        assert_eq!(deletion_summary.total_fragments, 1);
2116        assert_eq!(deletion_summary.total_data_files, 1);
2117        assert_eq!(deletion_summary.total_deletion_file_rows, 10);
2118        assert_eq!(deletion_summary.total_data_file_rows, 50);
2119        assert_eq!(deletion_summary.total_deletion_files, 1);
2120
2121        //Just verify the transformation is OK
2122        let stats_map: BTreeMap<String, String> = deletion_summary.into();
2123        assert_eq!(stats_map.len(), 7)
2124    }
2125}