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