Skip to main content

lance/
dataset.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Lance Dataset
5//!
6
7use arrow_array::{RecordBatch, RecordBatchReader};
8use arrow_schema::DataType;
9use byteorder::{ByteOrder, LittleEndian};
10use chrono::{Duration, prelude::*};
11use deepsize::DeepSizeOf;
12use futures::future::BoxFuture;
13use futures::stream::{self, BoxStream, StreamExt, TryStreamExt};
14use futures::{FutureExt, Stream};
15
16use crate::dataset::metadata::UpdateFieldMetadataBuilder;
17use crate::dataset::transaction::translate_schema_metadata_updates;
18use crate::index::DatasetIndexExt;
19use crate::session::caches::{DSMetadataCache, ManifestKey, TransactionKey};
20use crate::session::index_caches::DSIndexCache;
21use itertools::Itertools;
22use lance_core::ROW_ADDR;
23use lance_core::datatypes::{OnMissing, OnTypeMismatch, Projectable, Projection};
24use lance_core::traits::DatasetTakeRows;
25use lance_core::utils::address::RowAddress;
26use lance_core::utils::tracing::{
27    DATASET_CLEANING_EVENT, DATASET_DELETING_EVENT, DATASET_DROPPING_COLUMN_EVENT,
28    TRACE_DATASET_EVENTS,
29};
30use lance_datafusion::projection::ProjectionPlan;
31use lance_file::datatypes::populate_schema_dictionary;
32use lance_file::reader::{FileReader, FileReaderOptions};
33use lance_file::version::LanceFileVersion;
34use lance_index::{IndexType, progress::IndexBuildProgress};
35use lance_io::object_store::{
36    LanceNamespaceStorageOptionsProvider, ObjectStore, ObjectStoreParams, StorageOptions,
37    StorageOptionsAccessor, StorageOptionsProvider,
38};
39use lance_io::scheduler::{ScanScheduler, SchedulerConfig};
40use lance_io::utils::{
41    CachedFileSize, read_last_block, read_message, read_metadata_offset, read_struct,
42};
43use lance_namespace::LanceNamespace;
44use lance_table::format::{
45    DataFile, DataStorageFormat, DeletionFile, Fragment, IndexMetadata, Manifest, RowIdMeta, pb,
46};
47use lance_table::io::commit::{
48    CommitConfig, CommitError, CommitHandler, CommitLock, ManifestLocation, ManifestNamingScheme,
49    VERSIONS_DIR, external_manifest::ExternalManifestCommitHandler, migrate_scheme_to_v2,
50    write_manifest_file_to_path,
51};
52
53use crate::io::commit::namespace_manifest::LanceNamespaceExternalManifestStore;
54use lance_table::io::manifest::{read_manifest, read_manifest_indexes};
55use object_store::path::Path;
56use prost::Message;
57use roaring::RoaringBitmap;
58use rowids::get_row_id_index;
59use serde::{Deserialize, Serialize};
60use std::borrow::Cow;
61use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
62use std::fmt::Debug;
63use std::num::NonZero;
64use std::ops::Range;
65use std::pin::Pin;
66use std::sync::Arc;
67use tracing::{info, instrument};
68
69pub(crate) mod blob;
70mod branch_location;
71pub mod builder;
72pub mod cleanup;
73pub mod delta;
74pub mod fragment;
75mod hash_joiner;
76pub mod index;
77pub mod mem_wal;
78mod metadata;
79pub mod optimize;
80pub mod progress;
81pub mod refs;
82pub(crate) mod rowids;
83pub mod scanner;
84mod schema_evolution;
85pub mod sql;
86pub mod statistics;
87mod take;
88pub mod transaction;
89pub mod udtf;
90pub mod updater;
91mod utils;
92pub mod write;
93
94pub(crate) use take::row_offsets_to_row_addresses;
95
96use self::builder::DatasetBuilder;
97use self::cleanup::RemovalStats;
98use self::fragment::FileFragment;
99use self::refs::Refs;
100use self::scanner::{DatasetRecordBatchStream, Scanner};
101use self::transaction::{Operation, Transaction, TransactionBuilder, UpdateMapEntry};
102use self::write::write_fragments_internal;
103use crate::dataset::branch_location::BranchLocation;
104use crate::dataset::cleanup::{CleanupPolicy, CleanupPolicyBuilder};
105use crate::dataset::refs::{BranchContents, BranchIdentifier, Branches, Tags};
106use crate::dataset::sql::SqlQueryBuilder;
107use crate::datatypes::Schema;
108use crate::index::retain_supported_indices;
109use crate::io::commit::{
110    commit_detached_transaction, commit_new_dataset, commit_transaction,
111    detect_overlapping_fragments,
112};
113use crate::session::Session;
114use crate::utils::temporal::{SystemTime, timestamp_to_nanos, utc_now};
115use crate::{Error, Result};
116pub use blob::{BlobFile, ReadBlob, ReadBlobsBuilder, ReadBlobsStream};
117use hash_joiner::HashJoiner;
118pub use lance_core::ROW_ID;
119use lance_core::box_error;
120use lance_index::scalar::lance_format::LanceIndexStore;
121use lance_namespace::models::{DeclareTableRequest, DescribeTableRequest};
122use lance_table::feature_flags::{apply_feature_flags, can_read_dataset};
123use lance_table::io::deletion::{DELETIONS_DIR, relative_deletion_file_path};
124pub use schema_evolution::{
125    BatchInfo, BatchUDF, ColumnAlteration, NewColumnTransform, UDFCheckpointStore,
126};
127pub use take::TakeBuilder;
128pub use write::merge_insert::{
129    MergeInsertBuilder, MergeInsertJob, MergeStats, UncommittedMergeInsert, WhenMatched,
130    WhenNotMatched, WhenNotMatchedBySource,
131};
132
133use crate::dataset::index::LanceIndexStoreExt;
134pub use write::update::{UpdateBuilder, UpdateJob};
135#[allow(deprecated)]
136pub use write::{
137    AutoCleanupParams, CommitBuilder, DeleteBuilder, DeleteResult, ExternalBlobMode, InsertBuilder,
138    WriteDestination, WriteMode, WriteParams, WriteProgressFn, WriteStats, write_fragments,
139};
140
141pub(crate) const INDICES_DIR: &str = "_indices";
142pub(crate) const DATA_DIR: &str = "data";
143pub(crate) const TRANSACTIONS_DIR: &str = "_transactions";
144
145// We default to 6GB for the index cache, since indices are often large but
146// worth caching.
147pub const DEFAULT_INDEX_CACHE_SIZE: usize = 6 * 1024 * 1024 * 1024;
148// Default to 1 GiB for the metadata cache. Column metadata can be like 40MB,
149// so this should be enough for a few hundred columns. Other metadata is much
150// smaller.
151pub const DEFAULT_METADATA_CACHE_SIZE: usize = 1024 * 1024 * 1024;
152
153/// Lance Dataset
154#[derive(Clone)]
155pub struct Dataset {
156    pub object_store: Arc<ObjectStore>,
157    pub(crate) commit_handler: Arc<dyn CommitHandler>,
158    /// Uri of the dataset.
159    ///
160    /// On cloud storage, we can not use [Dataset::base] to build the full uri because the
161    /// `bucket` is swallowed in the inner [ObjectStore].
162    uri: String,
163    pub(crate) base: Path,
164    pub manifest: Arc<Manifest>,
165    // Path for the manifest that is loaded. Used to get additional information,
166    // such as the index metadata.
167    pub(crate) manifest_location: ManifestLocation,
168    pub(crate) session: Arc<Session>,
169    pub refs: Refs,
170
171    // Bitmap of fragment ids in this dataset.
172    pub(crate) fragment_bitmap: Arc<RoaringBitmap>,
173
174    // These are references to session caches, but with the dataset URI as a prefix.
175    pub(crate) index_cache: Arc<DSIndexCache>,
176    pub(crate) metadata_cache: Arc<DSMetadataCache>,
177
178    /// File reader options to use when reading data files.
179    pub(crate) file_reader_options: Option<FileReaderOptions>,
180
181    /// Object store parameters used when opening this dataset.
182    /// These are used when creating object stores for additional base paths.
183    pub(crate) store_params: Option<Box<ObjectStoreParams>>,
184    /// Optional runtime-only object store parameters keyed by base path URI.
185    pub(crate) base_store_params: Option<Arc<HashMap<String, ObjectStoreParams>>>,
186}
187
188impl std::fmt::Debug for Dataset {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        f.debug_struct("Dataset")
191            .field("uri", &self.uri)
192            .field("base", &self.base)
193            .field("version", &self.manifest.version)
194            .field("cache_num_items", &self.session.approx_num_items())
195            .field("base_store_params", &self.base_store_params.is_some())
196            .finish()
197    }
198}
199
200/// Dataset Version
201#[derive(Deserialize, Serialize, Debug)]
202pub struct Version {
203    /// version number
204    pub version: u64,
205
206    /// Timestamp of dataset creation in UTC.
207    pub timestamp: DateTime<Utc>,
208
209    /// Key-value pairs of metadata.
210    pub metadata: BTreeMap<String, String>,
211}
212
213/// Convert Manifest to Data Version.
214impl From<&Manifest> for Version {
215    fn from(m: &Manifest) -> Self {
216        Self {
217            version: m.version,
218            timestamp: m.timestamp(),
219            metadata: m.summary().into(),
220        }
221    }
222}
223
224/// Customize read behavior of a dataset.
225#[derive(Clone, Debug)]
226pub struct ReadParams {
227    /// Size of the index cache in bytes. This cache stores index data in memory
228    /// for faster lookups. The default is 6 GiB.
229    pub index_cache_size_bytes: usize,
230
231    /// Size of the metadata cache in bytes. This cache stores metadata in memory
232    /// for faster open table and scans. The default is 1 GiB.
233    pub metadata_cache_size_bytes: usize,
234
235    /// If present, dataset will use this shared [`Session`] instead creating a new one.
236    ///
237    /// This is useful for sharing the same session across multiple datasets.
238    pub session: Option<Arc<Session>>,
239
240    pub store_options: Option<ObjectStoreParams>,
241
242    /// If present, dataset will use this to resolve the latest version
243    ///
244    /// Lance needs to be able to make atomic updates to the manifest.  This involves
245    /// coordination between readers and writers and we can usually rely on the filesystem
246    /// to do this coordination for us.
247    ///
248    /// Some file systems (e.g. S3) do not support atomic operations.  In this case, for
249    /// safety, we recommend an external commit mechanism (such as dynamodb) and, on the
250    /// read path, we need to reach out to that external mechanism to figure out the latest
251    /// version of the dataset.
252    ///
253    /// If this is not set then a default behavior is chosen that is appropriate for the
254    /// filesystem.
255    ///
256    /// If a custom object store is provided (via store_params.object_store) then this
257    /// must also be provided.
258    pub commit_handler: Option<Arc<dyn CommitHandler>>,
259
260    /// File reader options to use when reading data files.
261    ///
262    /// This allows control over features like caching repetition indices and validation.
263    /// Options set here act as dataset-level defaults and can be overridden on a
264    /// per-scan basis via [`Scanner::batch_size_bytes`](crate::dataset::scanner::Scanner::batch_size_bytes) or
265    /// [`Scanner::with_file_reader_options`](crate::dataset::scanner::Scanner::with_file_reader_options).
266    pub file_reader_options: Option<FileReaderOptions>,
267}
268
269impl ReadParams {
270    /// Set the cache size for indices. Set to zero, to disable the cache.
271    #[deprecated(
272        since = "0.30.0",
273        note = "Use `index_cache_size_bytes` instead, which accepts a size in bytes."
274    )]
275    pub fn index_cache_size(&mut self, cache_size: usize) -> &mut Self {
276        let assumed_entry_size = 20 * 1024 * 1024; // 20 MiB per entry
277        self.index_cache_size_bytes = cache_size * assumed_entry_size;
278        self
279    }
280
281    pub fn index_cache_size_bytes(&mut self, cache_size: usize) -> &mut Self {
282        self.index_cache_size_bytes = cache_size;
283        self
284    }
285
286    /// Set the cache size for the file metadata. Set to zero to disable this cache.
287    #[deprecated(
288        since = "0.30.0",
289        note = "Use `metadata_cache_size_bytes` instead, which accepts a size in bytes."
290    )]
291    pub fn metadata_cache_size(&mut self, cache_size: usize) -> &mut Self {
292        let assumed_entry_size = 10 * 1024 * 1024; // 10 MiB per entry
293        self.metadata_cache_size_bytes = cache_size * assumed_entry_size;
294        self
295    }
296
297    /// Set the cache size for the file metadata in bytes.
298    pub fn metadata_cache_size_bytes(&mut self, cache_size: usize) -> &mut Self {
299        self.metadata_cache_size_bytes = cache_size;
300        self
301    }
302
303    /// Set a shared session for the datasets.
304    pub fn session(&mut self, session: Arc<Session>) -> &mut Self {
305        self.session = Some(session);
306        self
307    }
308
309    /// Use the explicit locking to resolve the latest version
310    pub fn set_commit_lock<T: CommitLock + Send + Sync + 'static>(&mut self, lock: Arc<T>) {
311        self.commit_handler = Some(Arc::new(lock));
312    }
313
314    /// Set the file reader options.
315    pub fn file_reader_options(&mut self, options: FileReaderOptions) -> &mut Self {
316        self.file_reader_options = Some(options);
317        self
318    }
319}
320
321impl Default for ReadParams {
322    fn default() -> Self {
323        Self {
324            index_cache_size_bytes: DEFAULT_INDEX_CACHE_SIZE,
325            metadata_cache_size_bytes: DEFAULT_METADATA_CACHE_SIZE,
326            session: None,
327            store_options: None,
328            commit_handler: None,
329            file_reader_options: None,
330        }
331    }
332}
333
334#[derive(Debug, Clone)]
335pub enum ProjectionRequest {
336    Schema(Arc<Schema>),
337    Sql(Vec<(String, String)>),
338}
339
340impl ProjectionRequest {
341    pub fn from_columns(
342        columns: impl IntoIterator<Item = impl AsRef<str>>,
343        dataset_schema: &Schema,
344    ) -> Self {
345        let columns = columns
346            .into_iter()
347            .map(|s| s.as_ref().to_string())
348            .collect::<Vec<_>>();
349
350        let schema = dataset_schema
351            .project_preserve_system_columns(&columns)
352            .unwrap();
353        Self::Schema(Arc::new(schema))
354    }
355
356    pub fn from_schema(schema: Schema) -> Self {
357        Self::Schema(Arc::new(schema))
358    }
359
360    /// Provide a list of projection with SQL transform.
361    ///
362    /// # Parameters
363    /// - `columns`: A list of tuples where the first element is resulted column name and the second
364    ///   element is the SQL expression.
365    pub fn from_sql(
366        columns: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
367    ) -> Self {
368        Self::Sql(
369            columns
370                .into_iter()
371                .map(|(a, b)| (a.into(), b.into()))
372                .collect(),
373        )
374    }
375
376    pub fn into_projection_plan(self, dataset: Arc<Dataset>) -> Result<ProjectionPlan> {
377        match self {
378            Self::Schema(schema) => {
379                // The schema might contain system columns (_rowid, _rowaddr) which are not
380                // in the dataset schema. We handle these specially in ProjectionPlan::from_schema.
381                let system_columns_present = schema
382                    .fields
383                    .iter()
384                    .any(|f| lance_core::is_system_column(&f.name));
385
386                if system_columns_present {
387                    // If system columns are present, we can't use project_by_schema directly
388                    // Just pass the schema to ProjectionPlan::from_schema which handles it
389                    ProjectionPlan::from_schema(dataset, schema.as_ref())
390                } else {
391                    // No system columns, use normal path with validation
392                    let projection = dataset.schema().project_by_schema(
393                        schema.as_ref(),
394                        OnMissing::Error,
395                        OnTypeMismatch::Error,
396                    )?;
397                    ProjectionPlan::from_schema(dataset, &projection)
398                }
399            }
400            Self::Sql(columns) => ProjectionPlan::from_expressions(dataset, &columns),
401        }
402    }
403}
404
405impl From<Arc<Schema>> for ProjectionRequest {
406    fn from(schema: Arc<Schema>) -> Self {
407        Self::Schema(schema)
408    }
409}
410
411impl From<Schema> for ProjectionRequest {
412    fn from(schema: Schema) -> Self {
413        Self::from(Arc::new(schema))
414    }
415}
416
417impl Dataset {
418    /// Open an existing dataset.
419    ///
420    /// See also [DatasetBuilder].
421    #[instrument]
422    pub async fn open(uri: &str) -> Result<Self> {
423        DatasetBuilder::from_uri(uri).load().await
424    }
425
426    /// Check out a dataset version with a ref
427    pub async fn checkout_version(&self, version: impl Into<refs::Ref>) -> Result<Self> {
428        let reference: refs::Ref = version.into();
429        match reference {
430            refs::Ref::Version(branch, version_number) => {
431                self.checkout_by_ref(version_number, branch.as_deref())
432                    .await
433            }
434            refs::Ref::VersionNumber(version_number) => {
435                self.checkout_by_ref(Some(version_number), self.manifest.branch.as_deref())
436                    .await
437            }
438            refs::Ref::Tag(tag_name) => {
439                let tag_contents = self.tags().get(tag_name.as_str()).await?;
440                self.checkout_by_ref(Some(tag_contents.version), tag_contents.branch.as_deref())
441                    .await
442            }
443        }
444    }
445
446    pub fn tags(&self) -> Tags<'_> {
447        self.refs.tags()
448    }
449
450    pub fn branches(&self) -> Branches<'_> {
451        self.refs.branches()
452    }
453
454    /// Check out the latest version of the dataset
455    pub async fn checkout_latest(&mut self) -> Result<()> {
456        let (manifest, manifest_location) = self.latest_manifest().await?;
457        self.manifest = manifest;
458        self.manifest_location = manifest_location;
459        self.fragment_bitmap = Arc::new(
460            self.manifest
461                .fragments
462                .iter()
463                .map(|f| f.id as u32)
464                .collect(),
465        );
466        Ok(())
467    }
468
469    /// Check out the latest version of the branch
470    pub async fn checkout_branch(&self, branch: &str) -> Result<Self> {
471        self.checkout_by_ref(None, Some(branch)).await
472    }
473
474    /// This is a two-phase operation:
475    /// - Create the branch dataset by shallow cloning.
476    /// - Create the branch metadata (a.k.a. `BranchContents`).
477    ///
478    /// These two phases are not atomic. We consider `BranchContents` as the source of truth
479    /// for the branch.
480    ///
481    /// The cleanup procedure should:
482    /// - Clean up zombie branch datasets that have no related `BranchContents`.
483    /// - Delete broken `BranchContents` entries that have no related branch dataset.
484    ///
485    /// If `create_branch` stops at phase 1, it may leave a zombie branch dataset,
486    /// which can be cleaned up later. Such a zombie dataset may cause a branch creation
487    /// failure if we use the same name to `create_branch`. In that case, you need to call
488    /// `force_delete_branch` to interactively clean up the zombie dataset.
489    pub async fn create_branch(
490        &mut self,
491        branch: &str,
492        version: impl Into<refs::Ref>,
493        store_params: Option<ObjectStoreParams>,
494    ) -> Result<Self> {
495        let (source_branch, version_number) = self.resolve_reference(version.into()).await?;
496        let branch_location = self.branch_location().find_branch(Some(branch))?;
497        let clone_op = Operation::Clone {
498            is_shallow: true,
499            ref_name: source_branch.clone(),
500            ref_version: version_number,
501            ref_path: String::from(self.uri()),
502            branch_name: Some(branch.to_string()),
503        };
504        let transaction = Transaction::new(version_number, clone_op, None);
505
506        let builder = CommitBuilder::new(WriteDestination::Uri(branch_location.uri.as_str()))
507            .with_store_params(store_params.unwrap_or_default())
508            .with_object_store(Arc::new(self.object_store().clone()))
509            .with_commit_handler(self.commit_handler.clone())
510            .with_storage_format(self.manifest.data_storage_format.lance_file_version()?);
511        let dataset = builder.execute(transaction).await?;
512
513        // Create BranchContents after shallow_clone
514        self.branches()
515            .create(branch, version_number, source_branch.as_deref())
516            .await?;
517        Ok(dataset)
518    }
519
520    pub async fn delete_branch(&mut self, branch: &str) -> Result<()> {
521        self.branches().delete(branch, false).await
522    }
523
524    /// Delete the branch even if the BranchContents is not found.
525    /// This could be useful when we have zombie branches and want to clean them up immediately.
526    pub async fn force_delete_branch(&mut self, branch: &str) -> Result<()> {
527        self.branches().delete(branch, true).await
528    }
529
530    pub async fn list_branches(&self) -> Result<HashMap<String, BranchContents>> {
531        self.branches().list().await
532    }
533
534    fn already_checked_out(&self, location: &ManifestLocation, branch_name: Option<&str>) -> bool {
535        // We check the e_tag here just in case it has been overwritten. This can
536        // happen if the table has been dropped then re-created recently.
537        self.manifest.branch.as_deref() == branch_name
538            && self.manifest.version == location.version
539            && self.manifest_location.naming_scheme == location.naming_scheme
540            && location.e_tag.as_ref().is_some_and(|e_tag| {
541                self.manifest_location
542                    .e_tag
543                    .as_ref()
544                    .is_some_and(|current_e_tag| e_tag == current_e_tag)
545            })
546    }
547
548    async fn checkout_by_ref(
549        &self,
550        version_number: Option<u64>,
551        branch: Option<&str>,
552    ) -> Result<Self> {
553        let new_location = self.branch_location().find_branch(branch)?;
554
555        let manifest_location = if let Some(version_number) = version_number {
556            self.commit_handler
557                .resolve_version_location(
558                    &new_location.path,
559                    version_number,
560                    &self.object_store.inner,
561                )
562                .await?
563        } else {
564            self.commit_handler
565                .resolve_latest_location(&new_location.path, &self.object_store)
566                .await?
567        };
568
569        if self.already_checked_out(&manifest_location, branch) {
570            return Ok(self.clone());
571        }
572
573        let manifest = Self::load_manifest(
574            self.object_store.as_ref(),
575            &manifest_location,
576            &new_location.uri,
577            self.session.as_ref(),
578        )
579        .await?;
580        Self::checkout_manifest(
581            self.object_store.clone(),
582            new_location.path,
583            new_location.uri,
584            Arc::new(manifest),
585            manifest_location,
586            self.session.clone(),
587            self.commit_handler.clone(),
588            self.file_reader_options.clone(),
589            self.store_params.as_deref().cloned(),
590            self.base_store_params.clone(),
591        )
592    }
593
594    pub(crate) async fn load_manifest(
595        object_store: &ObjectStore,
596        manifest_location: &ManifestLocation,
597        uri: &str,
598        session: &Session,
599    ) -> Result<Manifest> {
600        let object_reader = if let Some(size) = manifest_location.size {
601            object_store
602                .open_with_size(&manifest_location.path, size as usize)
603                .await
604        } else {
605            object_store.open(&manifest_location.path).await
606        };
607        let object_reader = object_reader.map_err(|e| match &e {
608            Error::NotFound { uri, .. } => Error::dataset_not_found(uri.clone(), box_error(e)),
609            _ => e,
610        })?;
611
612        let last_block =
613            read_last_block(object_reader.as_ref())
614                .await
615                .map_err(|err| match err {
616                    object_store::Error::NotFound { path, source } => {
617                        Error::dataset_not_found(path, source)
618                    }
619                    _ => Error::io_source(err.into()),
620                })?;
621        let offset = read_metadata_offset(&last_block)?;
622
623        // If manifest is in the last block, we can decode directly from memory.
624        let manifest_size = object_reader.size().await?;
625        let mut manifest = if manifest_size - offset <= last_block.len() {
626            let manifest_len = manifest_size - offset;
627            let offset_in_block = last_block.len() - manifest_len;
628            let message_len =
629                LittleEndian::read_u32(&last_block[offset_in_block..offset_in_block + 4]) as usize;
630            let message_data = &last_block[offset_in_block + 4..offset_in_block + 4 + message_len];
631            Manifest::try_from(lance_table::format::pb::Manifest::decode(message_data)?)
632        } else {
633            read_struct(object_reader.as_ref(), offset).await
634        }?;
635
636        if !can_read_dataset(manifest.reader_feature_flags) {
637            let message = format!(
638                "This dataset cannot be read by this version of Lance. \
639                 Please upgrade Lance to read this dataset.\n Flags: {}",
640                manifest.reader_feature_flags
641            );
642            return Err(Error::not_supported_source(message.into()));
643        }
644
645        // If indices were also in the last block, we can take the opportunity to
646        // decode them now and cache them.
647        if let Some(index_offset) = manifest.index_section
648            && manifest_size - index_offset <= last_block.len()
649        {
650            let offset_in_block = last_block.len() - (manifest_size - index_offset);
651            let message_len =
652                LittleEndian::read_u32(&last_block[offset_in_block..offset_in_block + 4]) as usize;
653            let message_data = &last_block[offset_in_block + 4..offset_in_block + 4 + message_len];
654            let section = lance_table::format::pb::IndexSection::decode(message_data)?;
655            let mut indices: Vec<IndexMetadata> = section
656                .indices
657                .into_iter()
658                .map(IndexMetadata::try_from)
659                .collect::<Result<Vec<_>>>()?;
660            retain_supported_indices(&mut indices);
661            let ds_index_cache = session.index_cache.for_dataset(uri);
662            let metadata_key = crate::session::index_caches::IndexMetadataKey {
663                version: manifest_location.version,
664            };
665            ds_index_cache
666                .insert_with_key(&metadata_key, Arc::new(indices))
667                .await;
668        }
669
670        // If transaction is also in the last block, we can take the opportunity to
671        // decode them now and cache them.
672        if let Some(transaction_offset) = manifest.transaction_section
673            && manifest_size - transaction_offset <= last_block.len()
674        {
675            let offset_in_block = last_block.len() - (manifest_size - transaction_offset);
676            let message_len =
677                LittleEndian::read_u32(&last_block[offset_in_block..offset_in_block + 4]) as usize;
678            let message_data = &last_block[offset_in_block + 4..offset_in_block + 4 + message_len];
679            let transaction: Transaction =
680                lance_table::format::pb::Transaction::decode(message_data)?.try_into()?;
681
682            let metadata_cache = session.metadata_cache.for_dataset(uri);
683            let metadata_key = TransactionKey {
684                version: manifest_location.version,
685            };
686            metadata_cache
687                .insert_with_key(&metadata_key, Arc::new(transaction))
688                .await;
689        }
690
691        if manifest.should_use_legacy_format() {
692            populate_schema_dictionary(&mut manifest.schema, object_reader.as_ref()).await?;
693        }
694
695        Ok(manifest)
696    }
697
698    #[allow(clippy::too_many_arguments)]
699    fn checkout_manifest(
700        object_store: Arc<ObjectStore>,
701        base_path: Path,
702        uri: String,
703        manifest: Arc<Manifest>,
704        manifest_location: ManifestLocation,
705        session: Arc<Session>,
706        commit_handler: Arc<dyn CommitHandler>,
707        file_reader_options: Option<FileReaderOptions>,
708        store_params: Option<ObjectStoreParams>,
709        base_store_params: Option<Arc<HashMap<String, ObjectStoreParams>>>,
710    ) -> Result<Self> {
711        let refs = Refs::new(
712            object_store.clone(),
713            commit_handler.clone(),
714            BranchLocation {
715                path: base_path.clone(),
716                uri: uri.clone(),
717                branch: manifest.branch.clone(),
718            },
719        );
720        let metadata_cache = Arc::new(session.metadata_cache.for_dataset(&uri));
721        let index_cache = Arc::new(session.index_cache.for_dataset(&uri));
722        let fragment_bitmap = Arc::new(manifest.fragments.iter().map(|f| f.id as u32).collect());
723        Ok(Self {
724            object_store,
725            base: base_path,
726            uri,
727            manifest,
728            manifest_location,
729            commit_handler,
730            session,
731            refs,
732            fragment_bitmap,
733            metadata_cache,
734            index_cache,
735            file_reader_options,
736            store_params: store_params.map(Box::new),
737            base_store_params,
738        })
739    }
740
741    /// Write to or Create a [Dataset] with a stream of [RecordBatch]s.
742    ///
743    /// `dest` can be a `&str`, `object_store::path::Path` or `Arc<Dataset>`.
744    ///
745    /// Returns the newly created [`Dataset`].
746    /// Or Returns [Error] if the dataset already exists.
747    ///
748    pub async fn write(
749        batches: impl RecordBatchReader + Send + 'static,
750        dest: impl Into<WriteDestination<'_>>,
751        params: Option<WriteParams>,
752    ) -> Result<Self> {
753        let mut builder = InsertBuilder::new(dest);
754        if let Some(params) = &params {
755            builder = builder.with_params(params);
756        }
757        Box::pin(builder.execute_stream(Box::new(batches) as Box<dyn RecordBatchReader + Send>))
758            .await
759    }
760
761    /// Write into a namespace client-managed table with automatic credential vending.
762    ///
763    /// For CREATE mode, calls declare_table() to initialize the table.
764    /// For other modes, calls describe_table() and opens dataset with namespace client credentials.
765    ///
766    /// # Arguments
767    ///
768    /// * `batches` - The record batches to write
769    /// * `namespace_client` - The namespace client to use for table management
770    /// * `table_id` - The table identifier
771    /// * `params` - Write parameters
772    pub async fn write_into_namespace(
773        batches: impl RecordBatchReader + Send + 'static,
774        namespace_client: Arc<dyn LanceNamespace>,
775        table_id: Vec<String>,
776        mut params: Option<WriteParams>,
777    ) -> Result<Self> {
778        let mut write_params = params.take().unwrap_or_default();
779
780        match write_params.mode {
781            WriteMode::Create => {
782                let declare_request = DeclareTableRequest {
783                    id: Some(table_id.clone()),
784                    ..Default::default()
785                };
786                let response = namespace_client
787                    .declare_table(declare_request)
788                    .await
789                    .map_err(|e| Error::namespace_source(Box::new(e)))?;
790
791                let uri = response.location.ok_or_else(|| {
792                    Error::namespace_source(Box::new(std::io::Error::other(
793                        "Table location not found in declare_table response",
794                    )))
795                })?;
796
797                // Set up commit handler when managed_versioning is enabled
798                if response.managed_versioning == Some(true) {
799                    let external_store = LanceNamespaceExternalManifestStore::new(
800                        namespace_client.clone(),
801                        table_id.clone(),
802                    );
803                    let commit_handler: Arc<dyn CommitHandler> =
804                        Arc::new(ExternalManifestCommitHandler {
805                            external_manifest_store: Arc::new(external_store),
806                        });
807                    write_params.commit_handler = Some(commit_handler);
808                }
809
810                // Set initial credentials and provider from namespace_client
811                if let Some(namespace_storage_options) = response.storage_options {
812                    let provider: Arc<dyn StorageOptionsProvider> = Arc::new(
813                        LanceNamespaceStorageOptionsProvider::new(namespace_client, table_id),
814                    );
815
816                    // Merge namespace client storage options with any existing options
817                    let mut merged_options = write_params
818                        .store_params
819                        .as_ref()
820                        .and_then(|p| p.storage_options().cloned())
821                        .unwrap_or_default();
822                    merged_options.extend(namespace_storage_options);
823
824                    let accessor = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
825                        merged_options,
826                        provider,
827                    ));
828
829                    let existing_params = write_params.store_params.take().unwrap_or_default();
830                    write_params.store_params = Some(ObjectStoreParams {
831                        storage_options_accessor: Some(accessor),
832                        ..existing_params
833                    });
834                }
835
836                Self::write(batches, uri.as_str(), Some(write_params)).await
837            }
838            WriteMode::Append | WriteMode::Overwrite => {
839                let request = DescribeTableRequest {
840                    id: Some(table_id.clone()),
841                    ..Default::default()
842                };
843                let response = namespace_client
844                    .describe_table(request)
845                    .await
846                    .map_err(|e| Error::namespace_source(Box::new(e)))?;
847
848                let uri = response.location.ok_or_else(|| {
849                    Error::namespace_source(Box::new(std::io::Error::other(
850                        "Table location not found in describe_table response",
851                    )))
852                })?;
853
854                // Set up commit handler when managed_versioning is enabled
855                if response.managed_versioning == Some(true) {
856                    let external_store = LanceNamespaceExternalManifestStore::new(
857                        namespace_client.clone(),
858                        table_id.clone(),
859                    );
860                    let commit_handler: Arc<dyn CommitHandler> =
861                        Arc::new(ExternalManifestCommitHandler {
862                            external_manifest_store: Arc::new(external_store),
863                        });
864                    write_params.commit_handler = Some(commit_handler);
865                }
866
867                // Set initial credentials and provider from namespace_client
868                if let Some(namespace_storage_options) = response.storage_options {
869                    let provider: Arc<dyn StorageOptionsProvider> =
870                        Arc::new(LanceNamespaceStorageOptionsProvider::new(
871                            namespace_client.clone(),
872                            table_id.clone(),
873                        ));
874
875                    // Merge namespace client storage options with any existing options
876                    let mut merged_options = write_params
877                        .store_params
878                        .as_ref()
879                        .and_then(|p| p.storage_options().cloned())
880                        .unwrap_or_default();
881                    merged_options.extend(namespace_storage_options);
882
883                    let accessor = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
884                        merged_options,
885                        provider,
886                    ));
887
888                    let existing_params = write_params.store_params.take().unwrap_or_default();
889                    write_params.store_params = Some(ObjectStoreParams {
890                        storage_options_accessor: Some(accessor),
891                        ..existing_params
892                    });
893                }
894
895                // For APPEND/OVERWRITE modes, we must open the existing dataset first
896                // and pass it to InsertBuilder. If we pass just the URI, InsertBuilder
897                // assumes no dataset exists and converts the mode to CREATE.
898                let mut builder = DatasetBuilder::from_uri(uri.as_str());
899                if let Some(ref store_params) = write_params.store_params
900                    && let Some(accessor) = &store_params.storage_options_accessor
901                {
902                    builder = builder.with_storage_options_accessor(accessor.clone());
903                }
904                let dataset = Arc::new(builder.load().await?);
905
906                Self::write(batches, dataset, Some(write_params)).await
907            }
908        }
909    }
910
911    /// Append to existing [Dataset] with a stream of [RecordBatch]s
912    ///
913    /// Returns void result or Returns [Error]
914    pub async fn append(
915        &mut self,
916        batches: impl RecordBatchReader + Send + 'static,
917        params: Option<WriteParams>,
918    ) -> Result<()> {
919        let write_params = WriteParams {
920            mode: WriteMode::Append,
921            ..params.unwrap_or_default()
922        };
923
924        let new_dataset = InsertBuilder::new(WriteDestination::Dataset(Arc::new(self.clone())))
925            .with_params(&write_params)
926            .execute_stream(Box::new(batches) as Box<dyn RecordBatchReader + Send>)
927            .await?;
928
929        *self = new_dataset;
930
931        Ok(())
932    }
933
934    /// Get the fully qualified URI of this dataset.
935    pub fn uri(&self) -> &str {
936        &self.uri
937    }
938
939    pub fn branch_location(&self) -> BranchLocation {
940        BranchLocation {
941            path: self.base.clone(),
942            uri: self.uri.clone(),
943            branch: self.manifest.branch.clone(),
944        }
945    }
946
947    pub async fn branch_identifier(&self) -> Result<BranchIdentifier> {
948        self.refs
949            .branches()
950            .get_identifier(self.manifest.branch.as_deref())
951            .await
952    }
953
954    /// Get the full manifest of the dataset version.
955    pub fn manifest(&self) -> &Manifest {
956        &self.manifest
957    }
958
959    pub fn manifest_location(&self) -> &ManifestLocation {
960        &self.manifest_location
961    }
962
963    /// Create a [`delta::DatasetDeltaBuilder`] to explore changes between dataset versions.
964    ///
965    /// # Example
966    ///
967    /// ```
968    /// # use lance::{Dataset, Result};
969    /// # async fn example(dataset: &Dataset) -> Result<()> {
970    /// let delta = dataset.delta()
971    ///     .compared_against_version(5)
972    ///     .build()?;
973    /// let inserted = delta.get_inserted_rows().await?;
974    /// # Ok(())
975    /// # }
976    /// ```
977    pub fn delta(&self) -> delta::DatasetDeltaBuilder {
978        delta::DatasetDeltaBuilder::new(self.clone())
979    }
980
981    // TODO: Cache this
982    pub(crate) fn is_legacy_storage(&self) -> bool {
983        self.manifest
984            .data_storage_format
985            .lance_file_version()
986            .unwrap()
987            == LanceFileVersion::Legacy
988    }
989
990    pub async fn latest_manifest(&self) -> Result<(Arc<Manifest>, ManifestLocation)> {
991        let location = self
992            .commit_handler
993            .resolve_latest_location(&self.base, &self.object_store)
994            .await?;
995
996        // Check if manifest is in cache before reading from storage
997        let manifest_key = ManifestKey {
998            version: location.version,
999            e_tag: location.e_tag.as_deref(),
1000        };
1001        let cached_manifest = self.metadata_cache.get_with_key(&manifest_key).await;
1002        if let Some(cached_manifest) = cached_manifest {
1003            return Ok((cached_manifest, location));
1004        }
1005
1006        if self.already_checked_out(&location, self.manifest.branch.as_deref()) {
1007            return Ok((self.manifest.clone(), self.manifest_location.clone()));
1008        }
1009        let mut manifest = read_manifest(&self.object_store, &location.path, location.size).await?;
1010        if manifest.schema.has_dictionary_types() && manifest.should_use_legacy_format() {
1011            let reader = if let Some(size) = location.size {
1012                self.object_store
1013                    .open_with_size(&location.path, size as usize)
1014                    .await?
1015            } else {
1016                self.object_store.open(&location.path).await?
1017            };
1018            populate_schema_dictionary(&mut manifest.schema, reader.as_ref()).await?;
1019        }
1020        let manifest_arc = Arc::new(manifest);
1021        self.metadata_cache
1022            .insert_with_key(&manifest_key, manifest_arc.clone())
1023            .await;
1024        Ok((manifest_arc, location))
1025    }
1026
1027    /// Read the transaction file for this version of the dataset.
1028    ///
1029    /// If there was no transaction file written for this version of the dataset
1030    /// then this will return None.
1031    pub async fn read_transaction(&self) -> Result<Option<Transaction>> {
1032        let transaction_key = TransactionKey {
1033            version: self.manifest.version,
1034        };
1035        if let Some(transaction) = self.metadata_cache.get_with_key(&transaction_key).await {
1036            return Ok(Some((*transaction).clone()));
1037        }
1038
1039        // Prefer inline transaction from manifest when available
1040        let transaction = if let Some(pos) = self.manifest.transaction_section {
1041            let reader = if let Some(size) = self.manifest_location.size {
1042                self.object_store
1043                    .open_with_size(&self.manifest_location.path, size as usize)
1044                    .await?
1045            } else {
1046                self.object_store.open(&self.manifest_location.path).await?
1047            };
1048
1049            let tx: pb::Transaction = read_message(reader.as_ref(), pos).await?;
1050            Transaction::try_from(tx).map(Some)?
1051        } else if let Some(path) = &self.manifest.transaction_file {
1052            // Fallback: read external transaction file if present
1053            let path = self.transactions_dir().child(path.as_str());
1054            let data = self.object_store.inner.get(&path).await?.bytes().await?;
1055            let transaction = lance_table::format::pb::Transaction::decode(data)?;
1056            Transaction::try_from(transaction).map(Some)?
1057        } else {
1058            None
1059        };
1060
1061        if let Some(tx) = transaction.as_ref() {
1062            self.metadata_cache
1063                .insert_with_key(&transaction_key, Arc::new(tx.clone()))
1064                .await;
1065        }
1066        Ok(transaction)
1067    }
1068
1069    /// Read the transaction file for this version of the dataset.
1070    ///
1071    /// If there was no transaction file written for this version of the dataset
1072    /// then this will return None.
1073    pub async fn read_transaction_by_version(&self, version: u64) -> Result<Option<Transaction>> {
1074        let dataset_version = self.checkout_version(version).await?;
1075        dataset_version.read_transaction().await
1076    }
1077
1078    /// List transactions for the dataset, up to a maximum number.
1079    ///
1080    /// This method iterates through dataset versions, starting from the current version,
1081    /// and collects the transaction for each version. It stops when either `recent_transactions`
1082    /// is reached or there are no more versions.
1083    ///
1084    /// # Arguments
1085    ///
1086    /// * `recent_transactions` - Maximum number of transactions to return
1087    ///
1088    /// # Returns
1089    ///
1090    /// A vector of optional transactions. Each element corresponds to a version,
1091    /// and may be None if no transaction file exists for that version.
1092    pub async fn get_transactions(
1093        &self,
1094        recent_transactions: usize,
1095    ) -> Result<Vec<Option<Transaction>>> {
1096        let mut transactions = vec![];
1097        let mut dataset = self.clone();
1098
1099        loop {
1100            let transaction = dataset.read_transaction().await?;
1101            transactions.push(transaction);
1102
1103            if transactions.len() >= recent_transactions {
1104                break;
1105            } else {
1106                match dataset
1107                    .checkout_version(dataset.version().version - 1)
1108                    .await
1109                {
1110                    Ok(ds) => dataset = ds,
1111                    Err(Error::DatasetNotFound { .. }) => break,
1112                    Err(err) => return Err(err),
1113                }
1114            }
1115        }
1116
1117        Ok(transactions)
1118    }
1119
1120    /// Restore the currently checked out version of the dataset as the latest version.
1121    pub async fn restore(&mut self) -> Result<()> {
1122        let (latest_manifest, _) = self.latest_manifest().await?;
1123        let latest_version = latest_manifest.version;
1124
1125        let transaction = Transaction::new(
1126            latest_version,
1127            Operation::Restore {
1128                version: self.manifest.version,
1129            },
1130            None,
1131        );
1132
1133        self.apply_commit(transaction, &Default::default(), &Default::default())
1134            .await?;
1135
1136        Ok(())
1137    }
1138
1139    /// Removes old versions of the dataset from disk
1140    ///
1141    /// This function will remove all versions of the dataset that are older than the provided
1142    /// timestamp.  This function will not remove the current version of the dataset.
1143    ///
1144    /// Once a version is removed it can no longer be checked out or restored.  Any data unique
1145    /// to that version will be lost.
1146    ///
1147    /// # Arguments
1148    ///
1149    /// * `older_than` - Versions older than this will be deleted.
1150    /// * `delete_unverified` - If false (the default) then files will only be deleted if they
1151    ///                        are listed in at least one manifest.  Otherwise these files will
1152    ///                        be kept since they cannot be distinguished from an in-progress
1153    ///                        transaction.  Set to true to delete these files if you are sure
1154    ///                        there are no other in-progress dataset operations.
1155    ///
1156    /// # Returns
1157    ///
1158    /// * `RemovalStats` - Statistics about the removal operation
1159    #[instrument(level = "debug", skip(self))]
1160    pub fn cleanup_old_versions(
1161        &self,
1162        older_than: Duration,
1163        delete_unverified: Option<bool>,
1164        error_if_tagged_old_versions: Option<bool>,
1165    ) -> BoxFuture<'_, Result<RemovalStats>> {
1166        let mut builder = CleanupPolicyBuilder::default();
1167        builder = builder.before_timestamp(utc_now() - older_than);
1168        if let Some(v) = delete_unverified {
1169            builder = builder.delete_unverified(v);
1170        }
1171        if let Some(v) = error_if_tagged_old_versions {
1172            builder = builder.error_if_tagged_old_versions(v);
1173        }
1174
1175        self.cleanup_with_policy(builder.build())
1176    }
1177
1178    /// Removes old versions of the dataset from storage
1179    ///
1180    /// This function will remove all versions of the dataset that satisfies the given policy.
1181    /// This function will not remove the current version of the dataset.
1182    ///
1183    /// Once a version is removed it can no longer be checked out or restored.  Any data unique
1184    /// to that version will be lost.
1185    ///
1186    /// # Arguments
1187    ///
1188    /// * `policy` - `CleanupPolicy` determines the behaviour of cleanup.
1189    ///
1190    /// # Returns
1191    ///
1192    /// * `RemovalStats` - Statistics about the removal operation
1193    #[instrument(level = "debug", skip(self))]
1194    pub fn cleanup_with_policy(
1195        &self,
1196        policy: CleanupPolicy,
1197    ) -> BoxFuture<'_, Result<RemovalStats>> {
1198        info!(target: TRACE_DATASET_EVENTS, event=DATASET_CLEANING_EVENT, uri=&self.uri);
1199        cleanup::cleanup_old_versions(self, policy).boxed()
1200    }
1201
1202    #[allow(clippy::too_many_arguments)]
1203    async fn do_commit(
1204        base_uri: WriteDestination<'_>,
1205        operation: Operation,
1206        read_version: Option<u64>,
1207        store_params: Option<ObjectStoreParams>,
1208        commit_handler: Option<Arc<dyn CommitHandler>>,
1209        session: Arc<Session>,
1210        enable_v2_manifest_paths: bool,
1211        detached: bool,
1212    ) -> Result<Self> {
1213        let read_version = read_version.map_or_else(
1214            || match operation {
1215                Operation::Overwrite { .. } | Operation::Restore { .. } => Ok(0),
1216                _ => Err(Error::invalid_input(
1217                    "read_version must be specified for this operation",
1218                )),
1219            },
1220            Ok,
1221        )?;
1222
1223        let transaction = Transaction::new(read_version, operation, None);
1224
1225        let mut builder = CommitBuilder::new(base_uri)
1226            .enable_v2_manifest_paths(enable_v2_manifest_paths)
1227            .with_session(session)
1228            .with_detached(detached);
1229
1230        if let Some(store_params) = store_params {
1231            builder = builder.with_store_params(store_params);
1232        }
1233
1234        if let Some(commit_handler) = commit_handler {
1235            builder = builder.with_commit_handler(commit_handler);
1236        }
1237
1238        builder.execute(transaction).await
1239    }
1240
1241    /// Commit changes to the dataset
1242    ///
1243    /// This operation is not needed if you are using append/write/delete to manipulate the dataset.
1244    /// It is used to commit changes to the dataset that are made externally.  For example, a bulk
1245    /// import tool may import large amounts of new data and write the appropriate lance files
1246    /// directly instead of using the write function.
1247    ///
1248    /// This method can be used to commit this change to the dataset's manifest.  This method will
1249    /// not verify that the provided fragments exist and correct, that is the caller's responsibility.
1250    /// Some validation can be performed using the function
1251    /// [crate::dataset::transaction::validate_operation].
1252    ///
1253    /// If this commit is a change to an existing dataset then it will often need to be based on an
1254    /// existing version of the dataset.  For example, if this change is a `delete` operation then
1255    /// the caller will have read in the existing data (at some version) to determine which fragments
1256    /// need to be deleted.  The base version that the caller used should be supplied as the `read_version`
1257    /// parameter.  Some operations (e.g. Overwrite) do not depend on a previous version and `read_version`
1258    /// can be None.  An error will be returned if the `read_version` is needed for an operation and
1259    /// it is not specified.
1260    ///
1261    /// All operations except Overwrite will fail if the dataset does not already exist.
1262    ///
1263    /// # Arguments
1264    ///
1265    /// * `base_uri` - The base URI of the dataset
1266    /// * `operation` - A description of the change to commit
1267    /// * `read_version` - The version of the dataset that this change is based on
1268    /// * `store_params` Parameters controlling object store access to the manifest
1269    /// * `enable_v2_manifest_paths`: If set to true, and this is a new dataset, uses the new v2 manifest
1270    ///   paths. These allow constant-time lookups for the latest manifest on object storage.
1271    ///   This parameter has no effect on existing datasets. To migrate an existing
1272    ///   dataset, use the [`Self::migrate_manifest_paths_v2`] method. WARNING: turning
1273    ///   this on will make the dataset unreadable for older versions of Lance
1274    ///   (prior to 0.17.0). Default is False.
1275    pub async fn commit(
1276        dest: impl Into<WriteDestination<'_>>,
1277        operation: Operation,
1278        read_version: Option<u64>,
1279        store_params: Option<ObjectStoreParams>,
1280        commit_handler: Option<Arc<dyn CommitHandler>>,
1281        session: Arc<Session>,
1282        enable_v2_manifest_paths: bool,
1283    ) -> Result<Self> {
1284        Self::do_commit(
1285            dest.into(),
1286            operation,
1287            read_version,
1288            store_params,
1289            commit_handler,
1290            session,
1291            enable_v2_manifest_paths,
1292            /*detached=*/ false,
1293        )
1294        .await
1295    }
1296
1297    /// Commits changes exactly the same as [`Self::commit`] but the commit will
1298    /// not be associated with the dataset lineage.
1299    ///
1300    /// The commit will not show up in the dataset's history and will never be
1301    /// the latest version of the dataset.
1302    ///
1303    /// This can be used to stage changes or to handle "secondary" datasets whose
1304    /// lineage is tracked elsewhere.
1305    pub async fn commit_detached(
1306        dest: impl Into<WriteDestination<'_>>,
1307        operation: Operation,
1308        read_version: Option<u64>,
1309        store_params: Option<ObjectStoreParams>,
1310        commit_handler: Option<Arc<dyn CommitHandler>>,
1311        session: Arc<Session>,
1312        enable_v2_manifest_paths: bool,
1313    ) -> Result<Self> {
1314        Self::do_commit(
1315            dest.into(),
1316            operation,
1317            read_version,
1318            store_params,
1319            commit_handler,
1320            session,
1321            enable_v2_manifest_paths,
1322            /*detached=*/ true,
1323        )
1324        .await
1325    }
1326
1327    pub(crate) async fn apply_commit(
1328        &mut self,
1329        transaction: Transaction,
1330        write_config: &ManifestWriteConfig,
1331        commit_config: &CommitConfig,
1332    ) -> Result<()> {
1333        let (manifest, manifest_location) = commit_transaction(
1334            self,
1335            self.object_store(),
1336            self.commit_handler.as_ref(),
1337            &transaction,
1338            write_config,
1339            commit_config,
1340            self.manifest_location.naming_scheme,
1341            None,
1342        )
1343        .await?;
1344
1345        self.manifest = Arc::new(manifest);
1346        self.manifest_location = manifest_location;
1347        self.fragment_bitmap = Arc::new(
1348            self.manifest
1349                .fragments
1350                .iter()
1351                .map(|f| f.id as u32)
1352                .collect(),
1353        );
1354
1355        Ok(())
1356    }
1357
1358    /// Create a Scanner to scan the dataset.
1359    pub fn scan(&self) -> Scanner {
1360        Scanner::new(Arc::new(self.clone()))
1361    }
1362
1363    /// Count the number of rows in the dataset.
1364    ///
1365    /// It offers a fast path of counting rows by just computing via metadata.
1366    #[instrument(skip_all)]
1367    pub async fn count_rows(&self, filter: Option<String>) -> Result<usize> {
1368        // TODO: consolidate the count_rows into Scanner plan.
1369        if let Some(filter) = filter {
1370            let mut scanner = self.scan();
1371            scanner.filter(&filter)?;
1372            Ok(scanner
1373                .project::<String>(&[])?
1374                .with_row_id() // TODO: fix scan plan to not require row_id for count_rows.
1375                .count_rows()
1376                .await? as usize)
1377        } else {
1378            self.count_all_rows().await
1379        }
1380    }
1381
1382    pub(crate) async fn count_all_rows(&self) -> Result<usize> {
1383        let cnts = stream::iter(self.get_fragments())
1384            .map(|f| async move { f.count_rows(None).await })
1385            .buffer_unordered(16)
1386            .try_collect::<Vec<_>>()
1387            .await?;
1388        Ok(cnts.iter().sum())
1389    }
1390
1391    /// Take rows by indices.
1392    #[instrument(skip_all, fields(num_rows=row_indices.len()))]
1393    pub async fn take(
1394        &self,
1395        row_indices: &[u64],
1396        projection: impl Into<ProjectionRequest>,
1397    ) -> Result<RecordBatch> {
1398        take::take(self, row_indices, projection.into()).await
1399    }
1400
1401    /// Take Rows by the internal ROW ids.
1402    ///
1403    /// In Lance format, each row has a unique `u64` id, which is used to identify the row globally.
1404    ///
1405    /// ```rust
1406    /// # use std::sync::Arc;
1407    /// # use tokio::runtime::Runtime;
1408    /// # use arrow_array::{RecordBatch, RecordBatchIterator, Int64Array};
1409    /// # use arrow_schema::{Schema, Field, DataType};
1410    /// # use lance::dataset::{WriteParams, Dataset, ProjectionRequest};
1411    /// #
1412    /// # let mut rt = Runtime::new().unwrap();
1413    /// # rt.block_on(async {
1414    /// # let test_dir = tempfile::tempdir().unwrap();
1415    /// # let uri = test_dir.path().to_str().unwrap().to_string();
1416    /// #
1417    /// # let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)]));
1418    /// # let write_params = WriteParams::default();
1419    /// # let array = Arc::new(Int64Array::from_iter(0..128));
1420    /// # let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
1421    /// # let reader = RecordBatchIterator::new(
1422    /// #    vec![batch].into_iter().map(Ok), schema
1423    /// # );
1424    /// # let dataset = Dataset::write(reader, &uri, Some(write_params)).await.unwrap();
1425    /// #
1426    /// let schema = dataset.schema().clone();
1427    /// let row_ids = vec![0, 4, 7];
1428    /// let rows = dataset.take_rows(&row_ids, schema).await.unwrap();
1429    ///
1430    /// // We can have more fine-grained control over the projection, i.e., SQL projection.
1431    /// let projection = ProjectionRequest::from_sql([("identity", "id * 2")]);
1432    /// let rows = dataset.take_rows(&row_ids, projection).await.unwrap();
1433    /// # });
1434    /// ```
1435    pub async fn take_rows(
1436        &self,
1437        row_ids: &[u64],
1438        projection: impl Into<ProjectionRequest>,
1439    ) -> Result<RecordBatch> {
1440        Arc::new(self.clone())
1441            .take_builder(row_ids, projection)?
1442            .execute()
1443            .await
1444    }
1445
1446    pub fn take_builder(
1447        self: &Arc<Self>,
1448        row_ids: &[u64],
1449        projection: impl Into<ProjectionRequest>,
1450    ) -> Result<TakeBuilder> {
1451        TakeBuilder::try_new_from_ids(self.clone(), row_ids.to_vec(), projection.into())
1452    }
1453
1454    /// Take [BlobFile] by row IDs.
1455    pub async fn take_blobs(
1456        self: &Arc<Self>,
1457        row_ids: &[u64],
1458        column: impl AsRef<str>,
1459    ) -> Result<Vec<BlobFile>> {
1460        blob::take_blobs(self, row_ids, column.as_ref()).await
1461    }
1462
1463    /// Take [BlobFile] by row addresses.
1464    ///
1465    /// Row addresses are `u64` values encoding `(fragment_id << 32) | row_offset`.
1466    /// Use this method when you already have row addresses, for example from
1467    /// a scan with `with_row_address()`. For row IDs (stable identifiers), use
1468    /// [`Self::take_blobs`]. For row indices (offsets), use
1469    /// [`Self::take_blobs_by_indices`].
1470    pub async fn take_blobs_by_addresses(
1471        self: &Arc<Self>,
1472        row_addrs: &[u64],
1473        column: impl AsRef<str>,
1474    ) -> Result<Vec<BlobFile>> {
1475        blob::take_blobs_by_addresses(self, row_addrs, column.as_ref()).await
1476    }
1477
1478    /// Take [BlobFile] by row indices (offsets in the dataset).
1479    pub async fn take_blobs_by_indices(
1480        self: &Arc<Self>,
1481        row_indices: &[u64],
1482        column: impl AsRef<str>,
1483    ) -> Result<Vec<BlobFile>> {
1484        let fragments = self.get_fragments();
1485        let row_addrs = row_offsets_to_row_addresses(&fragments, row_indices).await?;
1486        blob::take_blobs_by_addresses(self, &row_addrs, column.as_ref()).await
1487    }
1488
1489    /// Create a planned blob reader for a blob column.
1490    ///
1491    /// This API complements [`Self::take_blobs`]. `take_blobs` returns
1492    /// [`BlobFile`] handles for caller-driven random access, while
1493    /// `read_blobs` builds a streaming read plan for sequential or batched blob
1494    /// retrieval.
1495    ///
1496    /// ```rust
1497    /// # use std::sync::Arc;
1498    /// # use futures::TryStreamExt;
1499    /// # use lance::dataset::Dataset;
1500    /// # use lance::Result;
1501    /// # async fn example(dataset: Arc<Dataset>) -> Result<()> {
1502    /// let blobs = dataset
1503    ///     .read_blobs("images")?
1504    ///     .with_row_indices(vec![0, 1, 2])
1505    ///     .execute()
1506    ///     .await?;
1507    /// # let _ = blobs;
1508    /// # Ok(())
1509    /// # }
1510    /// ```
1511    pub fn read_blobs(self: &Arc<Self>, column: impl AsRef<str>) -> Result<ReadBlobsBuilder> {
1512        let column = column.as_ref();
1513        let blob_field_id = blob::validate_blob_column(self, column)?;
1514        Ok(ReadBlobsBuilder::new(
1515            self.clone(),
1516            column.to_string(),
1517            blob_field_id,
1518        ))
1519    }
1520
1521    /// Get a stream of batches based on iterator of ranges of row numbers.
1522    ///
1523    /// This is an experimental API. It may change at any time.
1524    pub fn take_scan(
1525        &self,
1526        row_ranges: Pin<Box<dyn Stream<Item = Result<Range<u64>>> + Send>>,
1527        projection: Arc<Schema>,
1528        batch_readahead: usize,
1529    ) -> DatasetRecordBatchStream {
1530        take::take_scan(self, row_ranges, projection, batch_readahead)
1531    }
1532
1533    /// Randomly sample `n` rows from the dataset.
1534    ///
1535    /// If `fragment_ids` is provided, sampling is limited to rows from those
1536    /// fragments in the current dataset version.
1537    ///
1538    /// The returned rows are in row-id order (not random order), which allows
1539    /// the underlying take operation to use an efficient sorted code path.
1540    pub async fn sample(
1541        &self,
1542        n: usize,
1543        projection: &Schema,
1544        fragment_ids: Option<&[u32]>,
1545    ) -> Result<RecordBatch> {
1546        use rand::seq::IteratorRandom;
1547
1548        match fragment_ids {
1549            None => {
1550                let num_rows = self.count_rows(None).await?;
1551                let mut ids = (0..num_rows as u64).choose_multiple(&mut rand::rng(), n);
1552                ids.sort_unstable();
1553                self.take(&ids, projection.clone()).await
1554            }
1555            Some(fragment_ids) => {
1556                if fragment_ids.is_empty() {
1557                    return Err(Error::invalid_input(
1558                        "Dataset::sample does not accept an empty fragment_ids list".to_string(),
1559                    ));
1560                }
1561
1562                let selected_fragment_ids = fragment_ids.iter().copied().collect::<BTreeSet<_>>();
1563                let selected_fragments = self
1564                    .get_fragments()
1565                    .into_iter()
1566                    .filter(|fragment| selected_fragment_ids.contains(&(fragment.id() as u32)))
1567                    .collect::<Vec<_>>();
1568
1569                if selected_fragments.len() != selected_fragment_ids.len() {
1570                    let present_fragment_ids = selected_fragments
1571                        .iter()
1572                        .map(|fragment| fragment.id() as u32)
1573                        .collect::<HashSet<_>>();
1574                    let missing_fragment_ids = selected_fragment_ids
1575                        .into_iter()
1576                        .filter(|fragment_id| !present_fragment_ids.contains(fragment_id))
1577                        .collect::<Vec<_>>();
1578                    return Err(Error::invalid_input(format!(
1579                        "Dataset::sample received fragment ids that are not part of the current dataset version: {missing_fragment_ids:?}",
1580                    )));
1581                }
1582
1583                let num_rows = stream::iter(selected_fragments.iter().cloned())
1584                    .map(|fragment| async move { fragment.count_rows(None).await })
1585                    .buffer_unordered(16)
1586                    .try_fold(0_u64, |acc, rows| async move { Ok(acc + rows as u64) })
1587                    .await?;
1588
1589                let mut offsets = (0..num_rows).choose_multiple(&mut rand::rng(), n);
1590                offsets.sort_unstable();
1591
1592                let row_addrs = row_offsets_to_row_addresses(&selected_fragments, &offsets).await?;
1593                let dataset = Arc::new(self.clone());
1594                let projection = Arc::new(
1595                    ProjectionRequest::from(projection.clone())
1596                        .into_projection_plan(dataset.clone())?,
1597                );
1598                TakeBuilder::try_new_from_addresses(dataset, row_addrs, projection)?
1599                    .execute()
1600                    .await
1601            }
1602        }
1603    }
1604
1605    /// Delete rows based on a predicate.
1606    pub async fn delete(&mut self, predicate: &str) -> Result<write::delete::DeleteResult> {
1607        info!(target: TRACE_DATASET_EVENTS, event=DATASET_DELETING_EVENT, uri = &self.uri, predicate=predicate);
1608        write::delete::delete(self, predicate).await
1609    }
1610
1611    /// Truncate the dataset by deleting all rows.
1612    pub async fn truncate_table(&mut self) -> Result<()> {
1613        self.delete("true").await.map(|_| ())
1614    }
1615
1616    /// Add new base paths to the dataset.
1617    ///
1618    /// This method allows you to register additional storage locations (buckets)
1619    /// that can be used for future data writes. The base paths are added to the
1620    /// dataset's manifest and can be referenced by name in subsequent write operations.
1621    ///
1622    /// # Arguments
1623    ///
1624    /// * `new_bases` - A vector of `lance_table::format::BasePath` objects representing the new storage
1625    ///   locations to add. Each base path should have a unique name and path.
1626    ///
1627    /// # Returns
1628    ///
1629    /// Returns a new `Dataset` instance with the updated manifest containing the
1630    /// new base paths.
1631    pub async fn add_bases(
1632        self: &Arc<Self>,
1633        new_bases: Vec<lance_table::format::BasePath>,
1634        transaction_properties: Option<HashMap<String, String>>,
1635    ) -> Result<Self> {
1636        let operation = Operation::UpdateBases { new_bases };
1637
1638        let transaction = TransactionBuilder::new(self.manifest.version, operation)
1639            .transaction_properties(transaction_properties.map(Arc::new))
1640            .build();
1641
1642        let new_dataset = CommitBuilder::new(self.clone())
1643            .execute(transaction)
1644            .await?;
1645
1646        Ok(new_dataset)
1647    }
1648
1649    pub async fn count_deleted_rows(&self) -> Result<usize> {
1650        futures::stream::iter(self.get_fragments())
1651            .map(|f| async move { f.count_deletions().await })
1652            .buffer_unordered(self.object_store.io_parallelism())
1653            .try_fold(0, |acc, x| futures::future::ready(Ok(acc + x)))
1654            .await
1655    }
1656
1657    pub fn object_store(&self) -> &ObjectStore {
1658        &self.object_store
1659    }
1660
1661    /// Clone this dataset with a different object store binding.
1662    ///
1663    /// The returned dataset shares metadata, session state, and caches with the
1664    /// original dataset, but all subsequent operations on the returned dataset
1665    /// use the supplied object store.
1666    pub fn with_object_store(
1667        &self,
1668        object_store: Arc<ObjectStore>,
1669        store_params: Option<ObjectStoreParams>,
1670    ) -> Self {
1671        let mut cloned = self.clone();
1672        cloned.object_store = object_store;
1673        if let Some(store_params) = store_params {
1674            cloned.store_params = Some(Box::new(store_params));
1675        }
1676        cloned
1677    }
1678
1679    fn store_params_for_base(
1680        &self,
1681        base_path: Option<&lance_table::format::BasePath>,
1682    ) -> ObjectStoreParams {
1683        // Base-specific bindings are exact ObjectStoreParams keyed by
1684        // `BasePath.path`. If a base has no explicit binding then reads fall back
1685        // to the dataset-level default store params.
1686        base_path
1687            .and_then(|base_path| {
1688                self.base_store_params
1689                    .as_ref()
1690                    .and_then(|params| params.get(&base_path.path))
1691            })
1692            .cloned()
1693            .unwrap_or_else(|| self.store_params.as_deref().cloned().unwrap_or_default())
1694    }
1695
1696    /// Returns the initial storage options used when opening this dataset, if any.
1697    ///
1698    /// This returns the static initial options without triggering any refresh.
1699    /// For the latest refreshed options, use [`Self::latest_storage_options`].
1700    #[deprecated(since = "0.25.0", note = "Use initial_storage_options() instead")]
1701    pub fn storage_options(&self) -> Option<&HashMap<String, String>> {
1702        self.initial_storage_options()
1703    }
1704
1705    /// Returns the initial storage options without triggering any refresh.
1706    ///
1707    /// For the latest refreshed options, use [`Self::latest_storage_options`].
1708    pub fn initial_storage_options(&self) -> Option<&HashMap<String, String>> {
1709        self.store_params
1710            .as_ref()
1711            .and_then(|params| params.storage_options())
1712    }
1713
1714    /// Returns the storage options provider used when opening this dataset, if any.
1715    pub fn storage_options_provider(
1716        &self,
1717    ) -> Option<Arc<dyn lance_io::object_store::StorageOptionsProvider>> {
1718        self.store_params
1719            .as_ref()
1720            .and_then(|params| params.storage_options_accessor.as_ref())
1721            .and_then(|accessor| accessor.provider().cloned())
1722    }
1723
1724    /// Returns the unified storage options accessor for this dataset, if any.
1725    ///
1726    /// The accessor handles both static and dynamic storage options with automatic
1727    /// caching and refresh. Use [`StorageOptionsAccessor::get_storage_options`] to
1728    /// get the latest options.
1729    pub fn storage_options_accessor(&self) -> Option<Arc<StorageOptionsAccessor>> {
1730        self.store_params
1731            .as_ref()
1732            .and_then(|params| params.get_accessor())
1733    }
1734
1735    /// Returns the latest (possibly refreshed) storage options.
1736    ///
1737    /// If a dynamic storage options provider is configured, this will return
1738    /// the cached options if still valid, or fetch fresh options if expired.
1739    ///
1740    /// For the initial static options without refresh, use [`Self::storage_options`].
1741    ///
1742    /// # Returns
1743    ///
1744    /// - `Ok(Some(options))` - Storage options are available (static or refreshed)
1745    /// - `Ok(None)` - No storage options were configured for this dataset
1746    /// - `Err(...)` - Error occurred while fetching/refreshing options from provider
1747    pub async fn latest_storage_options(&self) -> Result<Option<StorageOptions>> {
1748        // First check if we have an accessor (handles both static and dynamic options)
1749        if let Some(accessor) = self.storage_options_accessor() {
1750            let options = accessor.get_storage_options().await?;
1751            return Ok(Some(options));
1752        }
1753
1754        // Fallback to initial storage options if no accessor
1755        Ok(self.initial_storage_options().cloned().map(StorageOptions))
1756    }
1757
1758    pub fn data_dir(&self) -> Path {
1759        self.base.child(DATA_DIR)
1760    }
1761
1762    pub fn indices_dir(&self) -> Path {
1763        self.base.child(INDICES_DIR)
1764    }
1765
1766    pub fn transactions_dir(&self) -> Path {
1767        self.base.child(TRANSACTIONS_DIR)
1768    }
1769
1770    pub fn deletions_dir(&self) -> Path {
1771        self.base.child(DELETIONS_DIR)
1772    }
1773
1774    pub fn versions_dir(&self) -> Path {
1775        self.base.child(VERSIONS_DIR)
1776    }
1777
1778    pub(crate) fn data_file_dir(&self, data_file: &DataFile) -> Result<Path> {
1779        self.data_file_dir_for_base(data_file.base_id)
1780    }
1781
1782    /// Create a [`DataFile`] by reading metadata from an existing lance file.
1783    ///
1784    /// This reads the file's schema and version information, matches columns to
1785    /// the dataset's schema to determine field IDs, and calculates column indices.
1786    /// This is useful for constructing `DataFile` metadata needed for operations
1787    /// like [`Operation::DataReplacement`].
1788    ///
1789    /// # Arguments
1790    ///
1791    /// * `path` - The path to the data file, relative to the dataset's data directory.
1792    /// * `base_id` - The base path ID if the file is outside the dataset directory.
1793    pub async fn create_data_file(&self, path: &str, base_id: Option<u32>) -> Result<DataFile> {
1794        let data_dir = self.data_file_dir_for_base(base_id)?;
1795        let filepath = data_dir.child(path);
1796
1797        // Get file size
1798        let file_size = self.object_store().size(&filepath).await?;
1799
1800        // Read file metadata
1801        let scheduler = ScanScheduler::new(
1802            self.object_store.clone(),
1803            SchedulerConfig::new(2 * 1024 * 1024 * 1024),
1804        );
1805        let file = scheduler
1806            .open_file(&filepath, &CachedFileSize::new(file_size))
1807            .await?;
1808        let file_metadata = FileReader::read_all_metadata(&file).await?;
1809
1810        let file_version = LanceFileVersion::try_from_major_minor(
1811            file_metadata.major_version as u32,
1812            file_metadata.minor_version as u32,
1813        )?;
1814
1815        // Get top-level column names from file schema in file order
1816        let column_names: Vec<&str> = file_metadata
1817            .file_schema
1818            .fields
1819            .iter()
1820            .map(|f| f.name.as_str())
1821            .collect();
1822
1823        // Project dataset schema by file column names to get dataset field IDs
1824        let projected_ds_schema = self.schema().project(&column_names)?;
1825
1826        // Walk both schemas in parallel to build fields and column_indices
1827        let is_structural = file_version >= LanceFileVersion::V2_1;
1828        let ds_fields: Vec<_> = projected_ds_schema.fields_pre_order().collect();
1829        let file_fields: Vec<_> = file_metadata.file_schema.fields_pre_order().collect();
1830
1831        if ds_fields.len() != file_fields.len() {
1832            return Err(Error::invalid_input(format!(
1833                "Schema mismatch: dataset projection has {} fields but file has {} fields",
1834                ds_fields.len(),
1835                file_fields.len()
1836            )));
1837        }
1838
1839        let mut fields = Vec::new();
1840        let mut column_indices = Vec::new();
1841        let mut curr_column_idx: i32 = 0;
1842        let mut packed_struct_fields_num: usize = 0;
1843
1844        for (ds_field, file_field) in ds_fields.iter().zip(file_fields.iter()) {
1845            if ds_field.name != file_field.name {
1846                return Err(Error::invalid_input(format!(
1847                    "Schema mismatch: expected field '{}' but file has '{}'",
1848                    ds_field.name, file_field.name
1849                )));
1850            }
1851
1852            if packed_struct_fields_num > 0 {
1853                packed_struct_fields_num -= 1;
1854                continue;
1855            }
1856
1857            if file_field.is_packed_struct() {
1858                fields.push(ds_field.id);
1859                column_indices.push(curr_column_idx);
1860                curr_column_idx += 1;
1861                packed_struct_fields_num = file_field.children.len();
1862            } else if file_field.children.is_empty() || !is_structural {
1863                fields.push(ds_field.id);
1864                column_indices.push(curr_column_idx);
1865                curr_column_idx += 1;
1866            }
1867        }
1868
1869        let file_size_nz = NonZero::new(file_size);
1870        Ok(DataFile::new(
1871            path,
1872            fields,
1873            column_indices,
1874            file_metadata.major_version as u32,
1875            file_metadata.minor_version as u32,
1876            file_size_nz,
1877            base_id,
1878        ))
1879    }
1880
1881    /// Resolve the data directory for a given base_id.
1882    ///
1883    /// If `base_id` is `None`, returns the default data directory.
1884    fn data_file_dir_for_base(&self, base_id: Option<u32>) -> Result<Path> {
1885        match base_id {
1886            Some(base_id) => {
1887                let base_path = self.manifest.base_paths.get(&base_id).ok_or_else(|| {
1888                    Error::invalid_input(format!("base_path id {} not found", base_id))
1889                })?;
1890                let path = base_path.extract_path(self.session.store_registry())?;
1891                if base_path.is_dataset_root {
1892                    Ok(path.child(DATA_DIR))
1893                } else {
1894                    Ok(path)
1895                }
1896            }
1897            None => Ok(self.base.child(DATA_DIR)),
1898        }
1899    }
1900
1901    /// Get the ObjectStore for a specific path based on base_id
1902    pub(crate) async fn object_store_for_base(&self, base_id: u32) -> Result<Arc<ObjectStore>> {
1903        let base_path = self.manifest.base_paths.get(&base_id).ok_or_else(|| {
1904            Error::invalid_input(format!("Dataset base path with ID {} not found", base_id))
1905        })?;
1906        let store_params = self.store_params_for_base(Some(base_path));
1907
1908        let (store, _) = ObjectStore::from_uri_and_params(
1909            self.session.store_registry(),
1910            &base_path.path,
1911            &store_params,
1912        )
1913        .await?;
1914
1915        Ok(store)
1916    }
1917
1918    pub(crate) fn dataset_dir_for_deletion(&self, deletion_file: &DeletionFile) -> Result<Path> {
1919        match deletion_file.base_id.as_ref() {
1920            Some(base_id) => {
1921                let base_paths = &self.manifest.base_paths;
1922                let base_path = base_paths.get(base_id).ok_or_else(|| {
1923                    Error::invalid_input(format!(
1924                        "base_path id {} not found for deletion_file {:?}",
1925                        base_id, deletion_file
1926                    ))
1927                })?;
1928
1929                if !base_path.is_dataset_root {
1930                    return Err(Error::internal(format!(
1931                        "base_path id {} is not a dataset root for deletion_file {:?}",
1932                        base_id, deletion_file
1933                    )));
1934                }
1935                base_path.extract_path(self.session.store_registry())
1936            }
1937            None => Ok(self.base.clone()),
1938        }
1939    }
1940
1941    /// Get the indices directory for a specific index, considering its base_id
1942    pub(crate) fn indice_files_dir(&self, index: &IndexMetadata) -> Result<Path> {
1943        match index.base_id.as_ref() {
1944            Some(base_id) => {
1945                let base_paths = &self.manifest.base_paths;
1946                let base_path = base_paths.get(base_id).ok_or_else(|| {
1947                    Error::invalid_input(format!(
1948                        "base_path id {} not found for index {}",
1949                        base_id, index.uuid
1950                    ))
1951                })?;
1952                let path = base_path.extract_path(self.session.store_registry())?;
1953                if base_path.is_dataset_root {
1954                    Ok(path.child(INDICES_DIR))
1955                } else {
1956                    // For non-dataset-root base paths, we assume the path already points to the indices directory
1957                    Ok(path)
1958                }
1959            }
1960            None => Ok(self.base.child(INDICES_DIR)),
1961        }
1962    }
1963
1964    pub fn session(&self) -> Arc<Session> {
1965        self.session.clone()
1966    }
1967
1968    /// Get the currently checked-out version id.
1969    ///
1970    /// This is a cheap accessor that reads the id directly from the loaded
1971    /// manifest without constructing the full [Version] summary.
1972    pub fn version_id(&self) -> u64 {
1973        self.manifest.version
1974    }
1975
1976    /// Get the currently checked-out version details.
1977    ///
1978    /// This constructs a full [Version], including summary metadata derived
1979    /// from the loaded manifest fragments.
1980    pub fn version(&self) -> Version {
1981        Version::from(self.manifest.as_ref())
1982    }
1983
1984    /// Get the number of entries currently in the index cache.
1985    pub async fn index_cache_entry_count(&self) -> usize {
1986        self.session.index_cache.size().await
1987    }
1988
1989    /// Get cache hit ratio.
1990    pub async fn index_cache_hit_rate(&self) -> f32 {
1991        let stats = self.session.index_cache_stats().await;
1992        stats.hit_ratio()
1993    }
1994
1995    pub fn cache_size_bytes(&self) -> u64 {
1996        self.session.deep_size_of() as u64
1997    }
1998
1999    /// Get all versions.
2000    pub async fn versions(&self) -> Result<Vec<Version>> {
2001        let mut versions: Vec<Version> = self
2002            .commit_handler
2003            .list_manifest_locations(&self.base, &self.object_store, false)
2004            .try_filter_map(|location| async move {
2005                match read_manifest(&self.object_store, &location.path, location.size).await {
2006                    Ok(manifest) => Ok(Some(Version::from(&manifest))),
2007                    Err(e) => Err(e),
2008                }
2009            })
2010            .try_collect()
2011            .await?;
2012
2013        // TODO: this API should support pagination
2014        versions.sort_by_key(|v| v.version);
2015
2016        Ok(versions)
2017    }
2018
2019    /// List all detached manifest locations.
2020    ///
2021    /// Detached manifests are versions that are not part of the main version history.
2022    /// They are created by `commit_detached` and can be used for staging changes.
2023    ///
2024    /// To read transaction properties from a detached manifest:
2025    /// ```ignore
2026    /// let detached = dataset.list_detached_manifests().await?;
2027    /// for location in detached {
2028    ///     let ds = dataset.checkout_version(location.version).await?;
2029    ///     let tx = ds.read_transaction().await?;
2030    ///     // Access tx.transaction_properties
2031    /// }
2032    /// ```
2033    pub async fn list_detached_manifests(&self) -> Result<Vec<ManifestLocation>> {
2034        self.commit_handler
2035            .list_detached_manifest_locations(&self.base, &self.object_store)
2036            .try_collect()
2037            .await
2038    }
2039
2040    /// Get the latest version of the dataset
2041    /// This is meant to be a fast path for checking if a dataset has changed. This is why
2042    /// we don't return the full version struct.
2043    pub async fn latest_version_id(&self) -> Result<u64> {
2044        Ok(self
2045            .commit_handler
2046            .resolve_latest_location(&self.base, &self.object_store)
2047            .await?
2048            .version)
2049    }
2050
2051    pub fn count_fragments(&self) -> usize {
2052        self.manifest.fragments.len()
2053    }
2054
2055    /// Get the schema of the dataset
2056    pub fn schema(&self) -> &Schema {
2057        &self.manifest.schema
2058    }
2059
2060    /// Similar to [Self::schema], but only returns fields that are not marked as blob columns
2061    /// Creates a new empty projection into the dataset schema
2062    pub fn empty_projection(self: &Arc<Self>) -> Projection {
2063        Projection::empty(self.clone())
2064    }
2065
2066    /// Creates a projection that includes all columns in the dataset
2067    pub fn full_projection(self: &Arc<Self>) -> Projection {
2068        Projection::full(self.clone())
2069    }
2070
2071    /// Get fragments.
2072    pub fn get_fragments(&self) -> Vec<FileFragment> {
2073        let dataset = Arc::new(self.clone());
2074        self.manifest
2075            .fragments
2076            .iter()
2077            .map(|f| FileFragment::new(dataset.clone(), f.clone()))
2078            .collect()
2079    }
2080
2081    /// Iterate over manifest fragments without allocating [`FileFragment`] wrappers.
2082    pub fn iter_fragments(&self) -> impl Iterator<Item = &Fragment> {
2083        self.manifest.fragments.iter()
2084    }
2085
2086    pub fn get_fragment(&self, fragment_id: usize) -> Option<FileFragment> {
2087        let dataset = Arc::new(self.clone());
2088        let fragment = self
2089            .manifest
2090            .fragments
2091            .iter()
2092            .find(|f| f.id == fragment_id as u64)?;
2093        Some(FileFragment::new(dataset, fragment.clone()))
2094    }
2095
2096    pub fn fragments(&self) -> &Arc<Vec<Fragment>> {
2097        &self.manifest.fragments
2098    }
2099
2100    // Gets a filtered list of fragments from ids in O(N) time instead of using
2101    // `get_fragment` which would require O(N^2) time.
2102    pub fn get_frags_from_ordered_ids(&self, ordered_ids: &[u32]) -> Vec<Option<FileFragment>> {
2103        let mut fragments = Vec::with_capacity(ordered_ids.len());
2104        let mut id_iter = ordered_ids.iter();
2105        let mut id = id_iter.next();
2106        // This field is just used to assert the ids are in order
2107        let mut last_id: i64 = -1;
2108        for frag in self.manifest.fragments.iter() {
2109            let mut the_id = if let Some(id) = id { *id } else { break };
2110            // Assert the given ids are, in fact, in order
2111            assert!(the_id as i64 > last_id);
2112            // For any IDs we've passed we can assume that no fragment exists any longer
2113            // with that ID.
2114            while the_id < frag.id as u32 {
2115                fragments.push(None);
2116                last_id = the_id as i64;
2117                id = id_iter.next();
2118                the_id = if let Some(id) = id { *id } else { break };
2119            }
2120
2121            if the_id == frag.id as u32 {
2122                fragments.push(Some(FileFragment::new(
2123                    Arc::new(self.clone()),
2124                    frag.clone(),
2125                )));
2126                last_id = the_id as i64;
2127                id = id_iter.next();
2128            }
2129        }
2130        fragments
2131    }
2132
2133    // This method filters deleted items from `addr_or_ids` using `addrs` as a reference
2134    async fn filter_addr_or_ids(&self, addr_or_ids: &[u64], addrs: &[u64]) -> Result<Vec<u64>> {
2135        if addrs.is_empty() {
2136            return Ok(Vec::new());
2137        }
2138
2139        let mut perm = permutation::sort(addrs);
2140        // First we sort the addrs, then we transform from Vec<u64> to Vec<Option<u64>> and then
2141        // we un-sort and use the None values to filter `addr_or_ids`
2142        let sorted_addrs = perm.apply_slice(addrs);
2143
2144        // Only collect deletion vectors for the fragments referenced by the given addrs
2145        let referenced_frag_ids = sorted_addrs
2146            .iter()
2147            .map(|addr| RowAddress::from(*addr).fragment_id())
2148            .dedup()
2149            .collect::<Vec<_>>();
2150        let frags = self.get_frags_from_ordered_ids(&referenced_frag_ids);
2151        let dv_futs = frags
2152            .iter()
2153            .map(|frag| {
2154                if let Some(frag) = frag {
2155                    frag.get_deletion_vector().boxed()
2156                } else {
2157                    std::future::ready(Ok(None)).boxed()
2158                }
2159            })
2160            .collect::<Vec<_>>();
2161        let dvs = stream::iter(dv_futs)
2162            .buffered(self.object_store.io_parallelism())
2163            .try_collect::<Vec<_>>()
2164            .await?;
2165
2166        // Iterate through the sorted addresses and sorted fragments (and sorted deletion vectors)
2167        // and filter out addresses that have been deleted
2168        let mut filtered_sorted_addrs = Vec::with_capacity(sorted_addrs.len());
2169        let mut sorted_addr_iter = sorted_addrs.into_iter().map(RowAddress::from);
2170        let mut next_addr = sorted_addr_iter.next().unwrap();
2171        let mut exhausted = false;
2172
2173        for frag_dv in frags.iter().zip(dvs).zip(referenced_frag_ids.iter()) {
2174            let ((frag, dv), frag_id) = frag_dv;
2175            if frag.is_some() {
2176                // Frag exists
2177                if let Some(dv) = dv.as_ref() {
2178                    // Deletion vector exists, scan DV
2179                    for deleted in dv.to_sorted_iter() {
2180                        while next_addr.fragment_id() == *frag_id
2181                            && next_addr.row_offset() < deleted
2182                        {
2183                            filtered_sorted_addrs.push(Some(u64::from(next_addr)));
2184                            if let Some(next) = sorted_addr_iter.next() {
2185                                next_addr = next;
2186                            } else {
2187                                exhausted = true;
2188                                break;
2189                            }
2190                        }
2191                        if exhausted {
2192                            break;
2193                        }
2194                        if next_addr.fragment_id() != *frag_id {
2195                            break;
2196                        }
2197                        if next_addr.row_offset() == deleted {
2198                            filtered_sorted_addrs.push(None);
2199                            if let Some(next) = sorted_addr_iter.next() {
2200                                next_addr = next;
2201                            } else {
2202                                exhausted = true;
2203                                break;
2204                            }
2205                        }
2206                    }
2207                }
2208                if exhausted {
2209                    break;
2210                }
2211                // Either no deletion vector, or we've exhausted it, keep everything else
2212                // in this frag
2213                while next_addr.fragment_id() == *frag_id {
2214                    filtered_sorted_addrs.push(Some(u64::from(next_addr)));
2215                    if let Some(next) = sorted_addr_iter.next() {
2216                        next_addr = next;
2217                    } else {
2218                        break;
2219                    }
2220                }
2221            } else {
2222                // Frag doesn't exist (possibly deleted), delete all items
2223                while next_addr.fragment_id() == *frag_id {
2224                    filtered_sorted_addrs.push(None);
2225                    if let Some(next) = sorted_addr_iter.next() {
2226                        next_addr = next;
2227                    } else {
2228                        break;
2229                    }
2230                }
2231            }
2232        }
2233
2234        // filtered_sorted_ids is now a Vec with the same size as sorted_addrs, but with None
2235        // values where the corresponding address was deleted.  We now need to un-sort it and
2236        // filter out the deleted addresses.
2237        perm.apply_inv_slice_in_place(&mut filtered_sorted_addrs);
2238        Ok(addr_or_ids
2239            .iter()
2240            .zip(filtered_sorted_addrs)
2241            .filter_map(|(addr_or_id, maybe_addr)| maybe_addr.map(|_| *addr_or_id))
2242            .collect())
2243    }
2244
2245    pub(crate) async fn filter_deleted_ids(&self, ids: &[u64]) -> Result<Vec<u64>> {
2246        let addresses = if let Some(row_id_index) = get_row_id_index(self).await? {
2247            let addresses = ids
2248                .iter()
2249                .filter_map(|id| row_id_index.get(*id).map(|address| address.into()))
2250                .collect::<Vec<_>>();
2251            Cow::Owned(addresses)
2252        } else {
2253            Cow::Borrowed(ids)
2254        };
2255
2256        self.filter_addr_or_ids(ids, &addresses).await
2257    }
2258
2259    /// Gets the number of files that are so small they don't even have a full
2260    /// group. These are considered too small because reading many of them is
2261    /// much less efficient than reading a single file because the separate files
2262    /// split up what would otherwise be single IO requests into multiple.
2263    pub async fn num_small_files(&self, max_rows_per_group: usize) -> usize {
2264        futures::stream::iter(self.get_fragments())
2265            .map(|f| async move { f.physical_rows().await })
2266            .buffered(self.object_store.io_parallelism())
2267            .try_filter(|row_count| futures::future::ready(*row_count < max_rows_per_group))
2268            .count()
2269            .await
2270    }
2271
2272    pub async fn validate(&self) -> Result<()> {
2273        // All fragments have unique ids
2274        let id_counts =
2275            self.manifest
2276                .fragments
2277                .iter()
2278                .map(|f| f.id)
2279                .fold(HashMap::new(), |mut acc, id| {
2280                    *acc.entry(id).or_insert(0) += 1;
2281                    acc
2282                });
2283        for (id, count) in id_counts {
2284            if count > 1 {
2285                return Err(Error::corrupt_file(
2286                    self.base.clone(),
2287                    format!(
2288                        "Duplicate fragment id {} found in dataset {:?}",
2289                        id, self.base
2290                    ),
2291                ));
2292            }
2293        }
2294
2295        // Fragments are sorted in increasing fragment id order
2296        self.manifest
2297            .fragments
2298            .iter()
2299            .map(|f| f.id)
2300            .try_fold(0, |prev, id| {
2301                if id < prev {
2302                    Err(Error::corrupt_file(self.base.clone(), format!(
2303                        "Fragment ids are not sorted in increasing fragment-id order. Found {} after {} in dataset {:?}",
2304                        id, prev, self.base
2305                    )))
2306                } else {
2307                    Ok(id)
2308                }
2309            })?;
2310
2311        // All fragments have equal lengths
2312        futures::stream::iter(self.get_fragments())
2313            .map(|f| async move { f.validate().await })
2314            .buffer_unordered(self.object_store.io_parallelism())
2315            .try_collect::<Vec<()>>()
2316            .await?;
2317
2318        // Validate indices
2319        let indices = self.load_indices().await?;
2320        self.validate_indices(&indices)?;
2321
2322        Ok(())
2323    }
2324
2325    fn validate_indices(&self, indices: &[IndexMetadata]) -> Result<()> {
2326        // Make sure there are no duplicate ids
2327        let mut index_ids = HashSet::new();
2328        for index in indices.iter() {
2329            if !index_ids.insert(&index.uuid) {
2330                return Err(Error::corrupt_file(
2331                    self.manifest_location.path.clone(),
2332                    format!(
2333                        "Duplicate index id {} found in dataset {:?}",
2334                        &index.uuid, self.base
2335                    ),
2336                ));
2337            }
2338        }
2339
2340        // For each index name, make sure there is no overlap in fragment bitmaps
2341        if let Err(err) = detect_overlapping_fragments(indices) {
2342            let mut message = "Overlapping fragments detected in dataset.".to_string();
2343            for (index_name, overlapping_frags) in err.bad_indices {
2344                message.push_str(&format!(
2345                    "\nIndex {:?} has overlapping fragments: {:?}",
2346                    index_name, overlapping_frags
2347                ));
2348            }
2349            return Err(Error::corrupt_file(
2350                self.manifest_location.path.clone(),
2351                message,
2352            ));
2353        };
2354
2355        Ok(())
2356    }
2357
2358    /// Migrate the dataset to use the new manifest path scheme.
2359    ///
2360    /// This function will rename all V1 manifests to [ManifestNamingScheme::V2].
2361    /// These paths provide more efficient opening of datasets with many versions
2362    /// on object stores.
2363    ///
2364    /// This function is idempotent, and can be run multiple times without
2365    /// changing the state of the object store.
2366    ///
2367    /// However, it should not be run while other concurrent operations are happening.
2368    /// And it should also run until completion before resuming other operations.
2369    ///
2370    /// ```rust
2371    /// # use lance::dataset::Dataset;
2372    /// # use lance_table::io::commit::ManifestNamingScheme;
2373    /// # use lance_datagen::{array, RowCount, BatchCount};
2374    /// # use arrow_array::types::Int32Type;
2375    /// # use lance::dataset::write::WriteParams;
2376    /// # let data = lance_datagen::gen_batch()
2377    /// #  .col("key", array::step::<Int32Type>())
2378    /// #  .into_reader_rows(RowCount::from(10), BatchCount::from(1));
2379    /// # let fut = async {
2380    /// # let params = WriteParams {
2381    /// #     enable_v2_manifest_paths: false,
2382    /// #     ..Default::default()
2383    /// # };
2384    /// let mut dataset = Dataset::write(data, "memory://test", Some(params)).await.unwrap();
2385    /// assert_eq!(dataset.manifest_location().naming_scheme, ManifestNamingScheme::V1);
2386    ///
2387    /// dataset.migrate_manifest_paths_v2().await.unwrap();
2388    /// assert_eq!(dataset.manifest_location().naming_scheme, ManifestNamingScheme::V2);
2389    /// # };
2390    /// # tokio::runtime::Runtime::new().unwrap().block_on(fut);
2391    /// ```
2392    pub async fn migrate_manifest_paths_v2(&mut self) -> Result<()> {
2393        migrate_scheme_to_v2(self.object_store(), &self.base).await?;
2394        // We need to re-open.
2395        let latest_version = self.latest_version_id().await?;
2396        *self = self.checkout_version(latest_version).await?;
2397        Ok(())
2398    }
2399
2400    /// Shallow clone the target version into a new dataset at target_path.
2401    /// 'target_path': the uri string to clone the dataset into.
2402    /// 'version': the version cloned from, could be a version number or tag.
2403    /// 'store_params': the object store params to use for the new dataset.
2404    pub async fn shallow_clone(
2405        &mut self,
2406        target_path: &str,
2407        version: impl Into<refs::Ref>,
2408        store_params: Option<ObjectStoreParams>,
2409    ) -> Result<Self> {
2410        let (ref_name, version_number) = self.resolve_reference(version.into()).await?;
2411        let clone_op = Operation::Clone {
2412            is_shallow: true,
2413            ref_name,
2414            ref_version: version_number,
2415            ref_path: self.uri.clone(),
2416            branch_name: None,
2417        };
2418        let transaction = Transaction::new(version_number, clone_op, None);
2419
2420        let builder = CommitBuilder::new(WriteDestination::Uri(target_path))
2421            .with_store_params(
2422                store_params.unwrap_or(self.store_params.as_deref().cloned().unwrap_or_default()),
2423            )
2424            .with_object_store(Arc::new(self.object_store().clone()))
2425            .with_commit_handler(self.commit_handler.clone())
2426            .with_storage_format(self.manifest.data_storage_format.lance_file_version()?);
2427        builder.execute(transaction).await
2428    }
2429
2430    /// Deep clone the target version into a new dataset at target_path.
2431    /// This performs a server-side copy of all relevant dataset files (data files,
2432    /// deletion files, and any external row-id files) into the target dataset
2433    /// without loading data into memory.
2434    ///
2435    /// Parameters:
2436    /// - `target_path`: the URI string to clone the dataset into.
2437    /// - `version`: the version cloned from, could be a version number, branch head, or tag.
2438    /// - `store_params`: the object store params to use for the new dataset.
2439    pub async fn deep_clone(
2440        &mut self,
2441        target_path: &str,
2442        version: impl Into<refs::Ref>,
2443        store_params: Option<ObjectStoreParams>,
2444    ) -> Result<Self> {
2445        use futures::StreamExt;
2446
2447        // Resolve source dataset and its manifest using checkout_version
2448        let src_ds = self.checkout_version(version).await?;
2449        let src_paths = src_ds.collect_paths().await?;
2450
2451        // Prepare target object store and base path
2452        let (target_store, target_base) = ObjectStore::from_uri_and_params(
2453            self.session.store_registry(),
2454            target_path,
2455            &store_params.clone().unwrap_or_default(),
2456        )
2457        .await?;
2458
2459        // Prevent cloning into an existing target dataset
2460        if self
2461            .commit_handler
2462            .resolve_latest_location(&target_base, &target_store)
2463            .await
2464            .is_ok()
2465        {
2466            return Err(Error::dataset_already_exists(target_path.to_string()));
2467        }
2468
2469        let build_absolute_path = |relative_path: &str, base: &Path| -> Path {
2470            let mut path = base.clone();
2471            for seg in relative_path.split('/') {
2472                if !seg.is_empty() {
2473                    path = path.child(seg);
2474                }
2475            }
2476            path
2477        };
2478
2479        // TODO: Leverage object store bulk copy for efficient deep_clone
2480        //
2481        // All cloud storage providers support batch copy APIs that would provide significant
2482        // performance improvements. We use single file copy before we have upstream support.
2483        //
2484        // Tracked by: https://github.com/lance-format/lance/issues/5435
2485        let io_parallelism = self.object_store.io_parallelism();
2486        let copy_futures = src_paths
2487            .iter()
2488            .map(|(relative_path, base)| {
2489                let store = Arc::clone(&target_store);
2490                let src_path = build_absolute_path(relative_path, base);
2491                let target_path = build_absolute_path(relative_path, &target_base);
2492                async move { store.copy(&src_path, &target_path).await.map(|_| ()) }
2493            })
2494            .collect::<Vec<_>>();
2495
2496        futures::stream::iter(copy_futures)
2497            .buffer_unordered(io_parallelism)
2498            .collect::<Vec<_>>()
2499            .await
2500            .into_iter()
2501            .collect::<Result<Vec<_>>>()?;
2502
2503        // Record a Clone operation and commit via CommitBuilder
2504        let ref_name = src_ds.manifest.branch.clone();
2505        let ref_version = src_ds.manifest_location.version;
2506        let clone_op = Operation::Clone {
2507            is_shallow: false,
2508            ref_name,
2509            ref_version,
2510            ref_path: src_ds.uri().to_string(),
2511            branch_name: None,
2512        };
2513        let txn = Transaction::new(ref_version, clone_op, None);
2514        let builder = CommitBuilder::new(WriteDestination::Uri(target_path))
2515            .with_store_params(store_params.clone().unwrap_or_default())
2516            .with_object_store(target_store.clone())
2517            .with_commit_handler(self.commit_handler.clone())
2518            .with_storage_format(self.manifest.data_storage_format.lance_file_version()?);
2519        let new_ds = builder.execute(txn).await?;
2520        Ok(new_ds)
2521    }
2522
2523    async fn resolve_reference(&self, reference: refs::Ref) -> Result<(Option<String>, u64)> {
2524        match reference {
2525            refs::Ref::Version(branch, version_number) => {
2526                if let Some(version_number) = version_number {
2527                    Ok((branch, version_number))
2528                } else {
2529                    let branch_location = self.branch_location().find_branch(branch.as_deref())?;
2530                    let version_number = self
2531                        .commit_handler
2532                        .resolve_latest_location(&branch_location.path, &self.object_store)
2533                        .await?
2534                        .version;
2535                    Ok((branch, version_number))
2536                }
2537            }
2538            refs::Ref::VersionNumber(version_number) => {
2539                Ok((self.manifest.branch.clone(), version_number))
2540            }
2541            refs::Ref::Tag(tag_name) => {
2542                let tag_contents = self.tags().get(tag_name.as_str()).await?;
2543                Ok((tag_contents.branch, tag_contents.version))
2544            }
2545        }
2546    }
2547
2548    /// Collect all (relative_path, path) of the dataset files.
2549    async fn collect_paths(&self) -> Result<Vec<(String, Path)>> {
2550        let mut file_paths: Vec<(String, Path)> = Vec::new();
2551        for fragment in self.manifest.fragments.iter() {
2552            if let Some(RowIdMeta::External(external_file)) = &fragment.row_id_meta {
2553                return Err(Error::internal(format!(
2554                    "External row_id_meta is not supported yet. external file path: {}",
2555                    external_file.path
2556                )));
2557            }
2558            for data_file in fragment.files.iter() {
2559                let base_root = if let Some(base_id) = data_file.base_id {
2560                    let base_path =
2561                        self.manifest.base_paths.get(&base_id).ok_or_else(|| {
2562                            Error::internal(format!("base_id {} not found", base_id))
2563                        })?;
2564                    Path::parse(base_path.path.as_str())?
2565                } else {
2566                    self.base.clone()
2567                };
2568                file_paths.push((
2569                    format!("{}/{}", DATA_DIR, data_file.path.clone()),
2570                    base_root,
2571                ));
2572            }
2573            if let Some(deletion_file) = &fragment.deletion_file {
2574                let base_root = if let Some(base_id) = deletion_file.base_id {
2575                    let base_path =
2576                        self.manifest.base_paths.get(&base_id).ok_or_else(|| {
2577                            Error::internal(format!("base_id {} not found", base_id))
2578                        })?;
2579                    Path::parse(base_path.path.as_str())?
2580                } else {
2581                    self.base.clone()
2582                };
2583                file_paths.push((
2584                    relative_deletion_file_path(fragment.id, deletion_file),
2585                    base_root,
2586                ));
2587            }
2588        }
2589
2590        let indices = read_manifest_indexes(
2591            self.object_store.as_ref(),
2592            &self.manifest_location,
2593            &self.manifest,
2594        )
2595        .await?;
2596
2597        for index in &indices {
2598            let base_root = if let Some(base_id) = index.base_id {
2599                let base_path = self
2600                    .manifest
2601                    .base_paths
2602                    .get(&base_id)
2603                    .ok_or_else(|| Error::internal(format!("base_id {} not found", base_id)))?;
2604                Path::parse(base_path.path.as_str())?
2605            } else {
2606                self.base.clone()
2607            };
2608            let index_root = base_root.child(INDICES_DIR).child(index.uuid.to_string());
2609            let mut stream = self.object_store.read_dir_all(&index_root, None);
2610            while let Some(meta) = stream.next().await.transpose()? {
2611                if let Some(filename) = meta.location.filename() {
2612                    file_paths.push((
2613                        format!("{}/{}/{}", INDICES_DIR, index.uuid, filename),
2614                        base_root.clone(),
2615                    ));
2616                }
2617            }
2618        }
2619        Ok(file_paths)
2620    }
2621
2622    /// Run a SQL query against the dataset.
2623    /// The underlying SQL engine is DataFusion.
2624    /// Please refer to the DataFusion documentation for supported SQL syntax.
2625    pub fn sql(&self, sql: &str) -> SqlQueryBuilder {
2626        SqlQueryBuilder::new(self.clone(), sql)
2627    }
2628
2629    /// Returns true if Lance supports writing this datatype with nulls.
2630    pub(crate) fn lance_supports_nulls(&self, datatype: &DataType) -> bool {
2631        match self
2632            .manifest()
2633            .data_storage_format
2634            .lance_file_version()
2635            .unwrap_or(LanceFileVersion::Legacy)
2636            .resolve()
2637        {
2638            LanceFileVersion::Legacy => matches!(
2639                datatype,
2640                DataType::Utf8
2641                    | DataType::LargeUtf8
2642                    | DataType::Binary
2643                    | DataType::List(_)
2644                    | DataType::FixedSizeBinary(_)
2645                    | DataType::FixedSizeList(_, _)
2646            ),
2647            LanceFileVersion::V2_0 => !matches!(datatype, DataType::Struct(..)),
2648            _ => true,
2649        }
2650    }
2651}
2652
2653pub(crate) struct NewTransactionResult<'a> {
2654    pub dataset: BoxFuture<'a, Result<Dataset>>,
2655    pub new_transactions: BoxStream<'a, Result<(u64, Arc<Transaction>)>>,
2656}
2657
2658pub(crate) fn load_new_transactions(dataset: &Dataset) -> NewTransactionResult<'_> {
2659    // Re-use the same list call for getting the latest manifest and the metadata
2660    // for all manifests in between.
2661    let io_parallelism = dataset.object_store().io_parallelism();
2662    let latest_version = dataset.manifest.version;
2663    let locations = dataset
2664        .commit_handler
2665        .list_manifest_locations(&dataset.base, dataset.object_store(), true)
2666        .try_take_while(move |location| {
2667            futures::future::ready(Ok(location.version > latest_version))
2668        });
2669
2670    // Will send the latest manifest via a channel.
2671    let (latest_tx, latest_rx) = tokio::sync::oneshot::channel();
2672    let mut latest_tx = Some(latest_tx);
2673
2674    let manifests = locations
2675        .map_ok(move |location| {
2676            let latest_tx = latest_tx.take();
2677            async move {
2678                let manifest_key = ManifestKey {
2679                    version: location.version,
2680                    e_tag: location.e_tag.as_deref(),
2681                };
2682                let manifest = if let Some(cached) =
2683                    dataset.metadata_cache.get_with_key(&manifest_key).await
2684                {
2685                    cached
2686                } else {
2687                    let loaded = Arc::new(
2688                        Dataset::load_manifest(
2689                            dataset.object_store(),
2690                            &location,
2691                            &dataset.uri,
2692                            dataset.session.as_ref(),
2693                        )
2694                        .await?,
2695                    );
2696                    dataset
2697                        .metadata_cache
2698                        .insert_with_key(&manifest_key, loaded.clone())
2699                        .await;
2700                    loaded
2701                };
2702
2703                if let Some(latest_tx) = latest_tx {
2704                    // We ignore the error, since we don't care if the receiver is dropped.
2705                    let _ = latest_tx.send((manifest.clone(), location.clone()));
2706                }
2707
2708                Ok((manifest, location))
2709            }
2710        })
2711        .try_buffer_unordered(io_parallelism / 2);
2712    let transactions = manifests
2713        .map_ok(move |(manifest, location)| async move {
2714            let manifest_copy = manifest.clone();
2715            let tx_key = TransactionKey {
2716                version: manifest.version,
2717            };
2718            let transaction =
2719                if let Some(cached) = dataset.metadata_cache.get_with_key(&tx_key).await {
2720                    cached
2721                } else {
2722                    let dataset_version = Dataset::checkout_manifest(
2723                        dataset.object_store.clone(),
2724                        dataset.base.clone(),
2725                        dataset.uri.clone(),
2726                        manifest_copy.clone(),
2727                        location,
2728                        dataset.session(),
2729                        dataset.commit_handler.clone(),
2730                        dataset.file_reader_options.clone(),
2731                        dataset.store_params.as_deref().cloned(),
2732                        dataset.base_store_params.clone(),
2733                    )?;
2734                    let loaded =
2735                        Arc::new(dataset_version.read_transaction().await?.ok_or_else(|| {
2736                            Error::internal(format!(
2737                                "Dataset version {} does not have a transaction file",
2738                                manifest_copy.version
2739                            ))
2740                        })?);
2741                    dataset
2742                        .metadata_cache
2743                        .insert_with_key(&tx_key, loaded.clone())
2744                        .await;
2745                    loaded
2746                };
2747            Ok((manifest.version, transaction))
2748        })
2749        .try_buffer_unordered(io_parallelism / 2);
2750
2751    let dataset = async move {
2752        if let Ok((latest_manifest, location)) = latest_rx.await {
2753            // If we got the latest manifest, we can checkout the dataset.
2754            Dataset::checkout_manifest(
2755                dataset.object_store.clone(),
2756                dataset.base.clone(),
2757                dataset.uri.clone(),
2758                latest_manifest,
2759                location,
2760                dataset.session(),
2761                dataset.commit_handler.clone(),
2762                dataset.file_reader_options.clone(),
2763                dataset.store_params.as_deref().cloned(),
2764                dataset.base_store_params.clone(),
2765            )
2766        } else {
2767            // If we didn't get the latest manifest, we can still return the dataset
2768            // with the current manifest.
2769            Ok(dataset.clone())
2770        }
2771    }
2772    .boxed();
2773
2774    let new_transactions = transactions.boxed();
2775
2776    NewTransactionResult {
2777        dataset,
2778        new_transactions,
2779    }
2780}
2781
2782/// # Schema Evolution
2783///
2784/// Lance datasets support evolving the schema. Several operations are
2785/// supported that mirror common SQL operations:
2786///
2787/// - [Self::add_columns()]: Add new columns to the dataset, similar to `ALTER TABLE ADD COLUMN`.
2788/// - [Self::drop_columns()]: Drop columns from the dataset, similar to `ALTER TABLE DROP COLUMN`.
2789/// - [Self::alter_columns()]: Modify columns in the dataset, changing their name, type, or nullability.
2790///   Similar to `ALTER TABLE ALTER COLUMN`.
2791///
2792/// In addition, one operation is unique to Lance: [`merge`](Self::merge). This
2793/// operation allows inserting precomputed data into the dataset.
2794///
2795/// Because these operations change the schema of the dataset, they will conflict
2796/// with most other concurrent operations. Therefore, they should be performed
2797/// when no other write operations are being run.
2798impl Dataset {
2799    /// Append new columns to the dataset.
2800    pub async fn add_columns(
2801        &mut self,
2802        transforms: NewColumnTransform,
2803        read_columns: Option<Vec<String>>,
2804        batch_size: Option<u32>,
2805    ) -> Result<()> {
2806        schema_evolution::add_columns(self, transforms, read_columns, batch_size).await
2807    }
2808
2809    /// Modify columns in the dataset, changing their name, type, or nullability.
2810    ///
2811    /// If only changing the name or nullability of a column, this is a zero-copy
2812    /// operation and any indices will be preserved. If changing the type of a
2813    /// column, the data for that column will be rewritten and any indices will
2814    /// be dropped. The old column data will not be immediately deleted. To remove
2815    /// it, call [optimize::compact_files()] and then
2816    /// [cleanup::cleanup_old_versions()] on the dataset.
2817    pub async fn alter_columns(&mut self, alterations: &[ColumnAlteration]) -> Result<()> {
2818        schema_evolution::alter_columns(self, alterations).await
2819    }
2820
2821    /// Remove columns from the dataset.
2822    ///
2823    /// This is a metadata-only operation and does not remove the data from the
2824    /// underlying storage. In order to remove the data, you must subsequently
2825    /// call [optimize::compact_files()] to rewrite the data without the removed columns and
2826    /// then call [cleanup::cleanup_old_versions()] to remove the old files.
2827    pub async fn drop_columns(&mut self, columns: &[&str]) -> Result<()> {
2828        info!(target: TRACE_DATASET_EVENTS, event=DATASET_DROPPING_COLUMN_EVENT, uri = &self.uri, columns = columns.join(","));
2829        schema_evolution::drop_columns(self, columns).await
2830    }
2831
2832    /// Drop columns from the dataset and return updated dataset. Note that this
2833    /// is a zero-copy operation and column is not physically removed from the
2834    /// dataset.
2835    /// Parameters:
2836    /// - `columns`: the list of column names to drop.
2837    #[deprecated(since = "0.9.12", note = "Please use `drop_columns` instead.")]
2838    pub async fn drop(&mut self, columns: &[&str]) -> Result<()> {
2839        self.drop_columns(columns).await
2840    }
2841
2842    async fn merge_impl(
2843        &mut self,
2844        stream: Box<dyn RecordBatchReader + Send>,
2845        left_on: &str,
2846        right_on: &str,
2847    ) -> Result<()> {
2848        // Sanity check.
2849        if self.schema().field(left_on).is_none() && left_on != ROW_ID && left_on != ROW_ADDR {
2850            return Err(Error::invalid_input(format!(
2851                "Column {} does not exist in the left side dataset",
2852                left_on
2853            )));
2854        };
2855        let right_schema = stream.schema();
2856        if right_schema.field_with_name(right_on).is_err() {
2857            return Err(Error::invalid_input(format!(
2858                "Column {} does not exist in the right side dataset",
2859                right_on
2860            )));
2861        };
2862        for field in right_schema.fields() {
2863            if field.name() == right_on {
2864                // right_on is allowed to exist in the dataset, since it may be
2865                // the same as left_on.
2866                continue;
2867            }
2868            if self.schema().field(field.name()).is_some() {
2869                return Err(Error::invalid_input(format!(
2870                    "Column {} exists in both sides of the dataset",
2871                    field.name()
2872                )));
2873            }
2874        }
2875
2876        // Hash join
2877        let joiner = Arc::new(HashJoiner::try_new(stream, right_on).await?);
2878        // Final schema is union of current schema, plus the RHS schema without
2879        // the right_on key.
2880        let mut new_schema: Schema = self.schema().merge(joiner.out_schema().as_ref())?;
2881        new_schema.set_field_id(Some(self.manifest.max_field_id()));
2882
2883        // Write new data file to each fragment. Parallelism is done over columns,
2884        // so no parallelism done at this level.
2885        let updated_fragments: Vec<Fragment> = stream::iter(self.get_fragments())
2886            .then(|f| {
2887                let joiner = joiner.clone();
2888                async move { f.merge(left_on, &joiner).await.map(|f| f.metadata) }
2889            })
2890            .try_collect::<Vec<_>>()
2891            .await?;
2892
2893        let transaction = Transaction::new(
2894            self.manifest.version,
2895            Operation::Merge {
2896                fragments: updated_fragments,
2897                schema: new_schema,
2898            },
2899            None,
2900        );
2901
2902        self.apply_commit(transaction, &Default::default(), &Default::default())
2903            .await?;
2904
2905        Ok(())
2906    }
2907
2908    /// Merge this dataset with another arrow Table / Dataset, and returns a new version of dataset.
2909    ///
2910    /// Parameters:
2911    ///
2912    /// - `stream`: the stream of [`RecordBatch`] to merge.
2913    /// - `left_on`: the column name to join on the left side (self).
2914    /// - `right_on`: the column name to join on the right side (stream).
2915    ///
2916    /// Returns: a new version of dataset.
2917    ///
2918    /// It performs a left-join on the two datasets.
2919    pub async fn merge(
2920        &mut self,
2921        stream: impl RecordBatchReader + Send + 'static,
2922        left_on: &str,
2923        right_on: &str,
2924    ) -> Result<()> {
2925        let stream = Box::new(stream);
2926        self.merge_impl(stream, left_on, right_on).await
2927    }
2928
2929    /// Merge a distributed scalar index into a single root artifact and report
2930    /// progress via the supplied callback.
2931    pub async fn merge_index_metadata(
2932        &self,
2933        index_uuid: &str,
2934        index_type: IndexType,
2935        batch_readhead: Option<usize>,
2936        progress: Arc<dyn IndexBuildProgress>,
2937    ) -> Result<()> {
2938        let store = LanceIndexStore::from_dataset_for_new(self, index_uuid)?;
2939        let index_dir = self.indices_dir().child(index_uuid);
2940        match index_type {
2941            IndexType::Inverted => {
2942                // Call merge_index_files function for inverted index
2943                lance_index::scalar::inverted::builder::merge_index_files(
2944                    self.object_store(),
2945                    &index_dir,
2946                    Arc::new(store),
2947                    progress,
2948                )
2949                .await
2950            }
2951            IndexType::BTree => {
2952                // Call merge_index_files function for btree index
2953                lance_index::scalar::btree::merge_index_files(
2954                    self.object_store(),
2955                    &index_dir,
2956                    Arc::new(store),
2957                    batch_readhead,
2958                    progress,
2959                )
2960                .await
2961            }
2962            IndexType::IvfFlat | IndexType::IvfPq | IndexType::IvfSq | IndexType::Vector => {
2963                Err(Error::invalid_input(
2964                    "Vector distributed indexing no longer supports merge_index_metadata; \
2965                     build segments, optionally merge groups with merge_existing_index_segments(...), \
2966                     and commit with commit_existing_index_segments(...)"
2967                        .to_string(),
2968                ))
2969            }
2970            _ => Err(Error::invalid_input_source(Box::new(std::io::Error::new(
2971                std::io::ErrorKind::InvalidInput,
2972                format!("Unsupported index type (patched): {}", index_type),
2973            )))),
2974        }
2975    }
2976}
2977
2978/// # Dataset metadata APIs
2979///
2980/// There are four kinds of metadata on datasets:
2981///
2982///  - **Schema metadata**: metadata about the data itself.
2983///  - **Field metadata**: metadata about the dataset itself.
2984///  - **Dataset metadata**: metadata about the dataset. For example, this could
2985///    store a created_at date.
2986///  - **Dataset config**: configuration values controlling how engines should
2987///    manage the dataset. This configures things like auto-cleanup.
2988///
2989/// You can get
2990impl Dataset {
2991    /// Get dataset metadata.
2992    pub fn metadata(&self) -> &HashMap<String, String> {
2993        &self.manifest.table_metadata
2994    }
2995
2996    /// Get the dataset config from manifest
2997    pub fn config(&self) -> &HashMap<String, String> {
2998        &self.manifest.config
2999    }
3000
3001    /// Delete keys from the config.
3002    #[deprecated(
3003        note = "Use the new update_config(values, replace) method - pass None values to delete keys"
3004    )]
3005    pub async fn delete_config_keys(&mut self, delete_keys: &[&str]) -> Result<()> {
3006        let updates = delete_keys.iter().map(|key| (*key, None));
3007        self.update_config(updates).await?;
3008        Ok(())
3009    }
3010
3011    /// Update table metadata.
3012    ///
3013    /// Pass `None` for a value to remove that key.
3014    ///
3015    /// Use `.replace()` to replace the entire metadata map instead of merging.
3016    ///
3017    /// Returns the updated metadata map after the operation.
3018    ///
3019    /// ```
3020    /// # use lance::{Dataset, Result};
3021    /// # use lance::dataset::transaction::UpdateMapEntry;
3022    /// # async fn test_update_metadata(dataset: &mut Dataset) -> Result<()> {
3023    /// // Update single key
3024    /// dataset.update_metadata([("key", "value")]).await?;
3025    ///
3026    /// // Remove a key
3027    /// dataset.update_metadata([("to_delete", None)]).await?;
3028    ///
3029    /// // Clear all metadata
3030    /// dataset.update_metadata([] as [UpdateMapEntry; 0]).replace().await?;
3031    ///
3032    /// // Replace full metadata
3033    /// dataset.update_metadata([("k1", "v1"), ("k2", "v2")]).replace().await?;
3034    /// # Ok(())
3035    /// # }
3036    /// ```
3037    pub fn update_metadata(
3038        &mut self,
3039        values: impl IntoIterator<Item = impl Into<UpdateMapEntry>>,
3040    ) -> metadata::UpdateMetadataBuilder<'_> {
3041        metadata::UpdateMetadataBuilder::new(self, values, metadata::MetadataType::TableMetadata)
3042    }
3043
3044    /// Update config.
3045    ///
3046    /// Pass `None` for a value to remove that key.
3047    ///
3048    /// Use `.replace()` to replace the entire config map instead of merging.
3049    ///
3050    /// Returns the updated config map after the operation.
3051    ///
3052    /// ```
3053    /// # use lance::{Dataset, Result};
3054    /// # use lance::dataset::transaction::UpdateMapEntry;
3055    /// # async fn test_update_config(dataset: &mut Dataset) -> Result<()> {
3056    /// // Update single key
3057    /// dataset.update_config([("key", "value")]).await?;
3058    ///
3059    /// // Remove a key
3060    /// dataset.update_config([("to_delete", None)]).await?;
3061    ///
3062    /// // Clear all config
3063    /// dataset.update_config([] as [UpdateMapEntry; 0]).replace().await?;
3064    ///
3065    /// // Replace full config
3066    /// dataset.update_config([("k1", "v1"), ("k2", "v2")]).replace().await?;
3067    /// # Ok(())
3068    /// # }
3069    /// ```
3070    pub fn update_config(
3071        &mut self,
3072        values: impl IntoIterator<Item = impl Into<UpdateMapEntry>>,
3073    ) -> metadata::UpdateMetadataBuilder<'_> {
3074        metadata::UpdateMetadataBuilder::new(self, values, metadata::MetadataType::Config)
3075    }
3076
3077    /// Update schema metadata.
3078    ///
3079    /// Pass `None` for a value to remove that key.
3080    ///
3081    /// Use `.replace()` to replace the entire schema metadata map instead of merging.
3082    ///
3083    /// Returns the updated schema metadata map after the operation.
3084    ///
3085    /// ```
3086    /// # use lance::{Dataset, Result};
3087    /// # use lance::dataset::transaction::UpdateMapEntry;
3088    /// # async fn test_update_schema_metadata(dataset: &mut Dataset) -> Result<()> {
3089    /// // Update single key
3090    /// dataset.update_schema_metadata([("key", "value")]).await?;
3091    ///
3092    /// // Remove a key
3093    /// dataset.update_schema_metadata([("to_delete", None)]).await?;
3094    ///
3095    /// // Clear all schema metadata
3096    /// dataset.update_schema_metadata([] as [UpdateMapEntry; 0]).replace().await?;
3097    ///
3098    /// // Replace full schema metadata
3099    /// dataset.update_schema_metadata([("k1", "v1"), ("k2", "v2")]).replace().await?;
3100    /// # Ok(())
3101    /// # }
3102    /// ```
3103    pub fn update_schema_metadata(
3104        &mut self,
3105        values: impl IntoIterator<Item = impl Into<UpdateMapEntry>>,
3106    ) -> metadata::UpdateMetadataBuilder<'_> {
3107        metadata::UpdateMetadataBuilder::new(self, values, metadata::MetadataType::SchemaMetadata)
3108    }
3109
3110    /// Update schema metadata
3111    #[deprecated(note = "Use the new update_schema_metadata(values).replace() instead")]
3112    pub async fn replace_schema_metadata(
3113        &mut self,
3114        new_values: impl IntoIterator<Item = (String, String)>,
3115    ) -> Result<()> {
3116        let new_values = new_values
3117            .into_iter()
3118            .map(|(k, v)| (k, Some(v)))
3119            .collect::<HashMap<_, _>>();
3120        self.update_schema_metadata(new_values).replace().await?;
3121        Ok(())
3122    }
3123
3124    /// Update field metadata
3125    ///
3126    /// ```
3127    /// # use lance::{Dataset, Result};
3128    /// # use lance::dataset::transaction::UpdateMapEntry;
3129    /// # async fn test_update_field_metadata(dataset: &mut Dataset) -> Result<()> {
3130    /// // Update metadata by field path
3131    /// dataset.update_field_metadata()
3132    ///     .update("path.to_field", [("key", "value")])?
3133    ///     .await?;
3134    ///
3135    /// // Update metadata by field id
3136    /// dataset.update_field_metadata()
3137    ///     .update(12, [("key", "value")])?
3138    ///     .await?;
3139    ///
3140    /// // Clear field metadata
3141    /// dataset.update_field_metadata()
3142    ///     .replace("path.to_field", [] as [UpdateMapEntry; 0])?
3143    ///     .replace(12, [] as [UpdateMapEntry; 0])?
3144    ///     .await?;
3145    ///
3146    /// // Replace field metadata
3147    /// dataset.update_field_metadata()
3148    ///     .replace("field_name", [("k1", "v1"), ("k2", "v2")])?
3149    ///     .await?;
3150    /// # Ok(())
3151    /// # }
3152    /// ```
3153    pub fn update_field_metadata(&mut self) -> UpdateFieldMetadataBuilder<'_> {
3154        UpdateFieldMetadataBuilder::new(self)
3155    }
3156
3157    /// Update field metadata
3158    pub async fn replace_field_metadata(
3159        &mut self,
3160        new_values: impl IntoIterator<Item = (u32, HashMap<String, String>)>,
3161    ) -> Result<()> {
3162        let new_values = new_values.into_iter().collect::<HashMap<_, _>>();
3163        let field_metadata_updates = new_values
3164            .into_iter()
3165            .map(|(field_id, metadata)| {
3166                (
3167                    field_id as i32,
3168                    translate_schema_metadata_updates(&metadata),
3169                )
3170            })
3171            .collect();
3172        metadata::execute_metadata_update(
3173            self,
3174            Operation::UpdateConfig {
3175                config_updates: None,
3176                table_metadata_updates: None,
3177                schema_metadata_updates: None,
3178                field_metadata_updates,
3179            },
3180        )
3181        .await
3182    }
3183}
3184
3185#[async_trait::async_trait]
3186impl DatasetTakeRows for Dataset {
3187    fn schema(&self) -> &Schema {
3188        Self::schema(self)
3189    }
3190
3191    async fn take_rows(&self, row_ids: &[u64], projection: &Schema) -> Result<RecordBatch> {
3192        Self::take_rows(self, row_ids, projection.clone()).await
3193    }
3194}
3195
3196#[derive(Debug)]
3197pub(crate) struct ManifestWriteConfig {
3198    auto_set_feature_flags: bool,              // default true
3199    timestamp: Option<SystemTime>,             // default None
3200    use_stable_row_ids: bool,                  // default false
3201    use_legacy_format: Option<bool>,           // default None
3202    storage_format: Option<DataStorageFormat>, // default None
3203    disable_transaction_file: bool,            // default false
3204}
3205
3206impl Default for ManifestWriteConfig {
3207    fn default() -> Self {
3208        Self {
3209            auto_set_feature_flags: true,
3210            timestamp: None,
3211            use_stable_row_ids: false,
3212            disable_transaction_file: false,
3213            use_legacy_format: None,
3214            storage_format: None,
3215        }
3216    }
3217}
3218
3219impl ManifestWriteConfig {
3220    pub fn disable_transaction_file(&self) -> bool {
3221        self.disable_transaction_file
3222    }
3223}
3224
3225/// Commit a manifest file and create a copy at the latest manifest path.
3226#[allow(clippy::too_many_arguments)]
3227pub(crate) async fn write_manifest_file(
3228    object_store: &ObjectStore,
3229    commit_handler: &dyn CommitHandler,
3230    base_path: &Path,
3231    manifest: &mut Manifest,
3232    indices: Option<Vec<IndexMetadata>>,
3233    config: &ManifestWriteConfig,
3234    naming_scheme: ManifestNamingScheme,
3235    mut transaction: Option<&Transaction>,
3236) -> std::result::Result<ManifestLocation, CommitError> {
3237    if config.auto_set_feature_flags {
3238        apply_feature_flags(
3239            manifest,
3240            config.use_stable_row_ids,
3241            config.disable_transaction_file,
3242        )?;
3243    }
3244
3245    manifest.set_timestamp(timestamp_to_nanos(config.timestamp));
3246
3247    manifest.update_max_fragment_id();
3248
3249    commit_handler
3250        .commit(
3251            manifest,
3252            indices,
3253            base_path,
3254            object_store,
3255            write_manifest_file_to_path,
3256            naming_scheme,
3257            transaction.take().map(|tx| tx.into()),
3258        )
3259        .await
3260}
3261
3262impl Projectable for Dataset {
3263    fn schema(&self) -> &Schema {
3264        self.schema()
3265    }
3266}
3267
3268#[cfg(test)]
3269mod tests;