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 byteorder::{ByteOrder, LittleEndian};
9use chrono::{Duration, prelude::*};
10use futures::future::BoxFuture;
11use futures::stream::{self, BoxStream, StreamExt, TryStreamExt};
12use futures::{FutureExt, Stream};
13use lance_core::deepsize::DeepSizeOf;
14
15use crate::dataset::metadata::UpdateFieldMetadataBuilder;
16use crate::dataset::transaction::translate_schema_metadata_updates;
17use crate::index::DatasetIndexExt;
18use crate::session::caches::{DSMetadataCache, ManifestKey, TransactionKey};
19use crate::session::index_caches::DSIndexCache;
20use itertools::Itertools;
21use lance_core::ROW_ADDR;
22use lance_core::datatypes::{OnMissing, OnTypeMismatch, Projectable, Projection};
23use lance_core::traits::DatasetTakeRows;
24use lance_core::utils::address::RowAddress;
25use lance_core::utils::tracing::{
26    DATASET_DELETING_EVENT, DATASET_DROPPING_COLUMN_EVENT, TRACE_DATASET_EVENTS,
27};
28use lance_datafusion::projection::ProjectionPlan;
29use lance_file::reader::{FileReader, FileReaderOptions};
30use lance_file::versions as file_versions;
31use lance_index::{IndexType, progress::IndexBuildProgress};
32use lance_io::object_store::{
33    ChainedWrappingObjectStore, LanceNamespaceStorageOptionsProvider, ObjectStore,
34    ObjectStoreParams, StorageOptions, StorageOptionsAccessor, StorageOptionsProvider,
35    WrappingObjectStore,
36};
37use lance_io::scheduler::{ScanScheduler, SchedulerConfig};
38use lance_io::traits::{WriteExt, Writer};
39use lance_io::utils::{
40    CachedFileSize, read_last_block, read_message, read_metadata_offset, read_struct,
41};
42use lance_namespace::LanceNamespace;
43use lance_table::format::{
44    DataFile, DataStorageFormat, DeletionFile, Fragment, IndexMetadata, MAGIC, Manifest,
45    ManifestBuildConfig, RowIdMeta, pb, populate_manifest_schema_dictionaries,
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::ObjectStoreExt;
56use object_store::path::Path;
57use prost::Message;
58use roaring::RoaringBitmap;
59use rowids::get_row_id_index;
60use serde::{Deserialize, Serialize};
61use std::borrow::Cow;
62use std::collections::{BTreeMap, HashMap, HashSet};
63use std::fmt::Debug;
64use std::num::NonZero;
65use std::ops::Range;
66use std::pin::Pin;
67use std::sync::Arc;
68use tracing::{info, instrument};
69
70pub(crate) mod blob;
71pub(crate) mod branch_location;
72pub mod builder;
73pub mod cleanup;
74pub mod delta;
75pub mod files;
76pub mod fragment;
77mod hash_joiner;
78pub mod index;
79pub mod mem_wal;
80mod metadata;
81pub mod optimize;
82pub(crate) mod overlay;
83pub mod progress;
84pub mod refs;
85pub mod rowids;
86pub mod scanner;
87mod schema_evolution;
88pub mod sql;
89pub mod statistics;
90mod take;
91/// Transaction definitions for updating datasets
92///
93/// Prior to creating a new manifest, a transaction must be created representing
94/// the changes being made to the dataset. By representing them as incremental
95/// changes, we can detect whether concurrent operations are compatible with
96/// one another. We can also rebuild manifests when retrying committing a
97/// manifest.
98///
99/// The definitions live in [`lance_table::transaction`]: building a manifest from
100/// a transaction reads and writes only table metadata, so it belongs at the table
101/// layer. This module re-exports them at the path callers have always used.
102///
103/// For more details please refer to the
104/// [Transaction Specification](https://lance.org/format/table/transaction/#transaction-types).
105pub mod transaction {
106    pub use lance_table::transaction::{
107        DataOverlayGroup, DataReplacementGroup, Operation, ReadVersionState, RewriteGroup,
108        RewrittenIndex, Transaction, TransactionBuilder, UpdateMap, UpdateMapEntry, UpdateMode,
109        UpdatedFragmentOffsets, translate_config_updates, translate_schema_metadata_updates,
110        validate_operation,
111    };
112}
113pub mod udtf;
114pub mod updater;
115mod utils;
116pub(crate) mod versions;
117pub mod write;
118
119pub(crate) use take::row_offsets_to_row_addresses;
120
121use self::builder::DatasetBuilder;
122use self::cleanup::RemovalStats;
123use self::fragment::FileFragment;
124use self::refs::Refs;
125use self::scanner::{DatasetRecordBatchStream, Scanner};
126use self::statistics::DatasetStatistics;
127use self::transaction::{Operation, Transaction, TransactionBuilder, UpdateMapEntry};
128use self::write::{cleanup_data_fragments, write_fragments_internal};
129use crate::dataset::branch_location::BranchLocation;
130use crate::dataset::cleanup::{CleanupOperation, CleanupPolicy, CleanupPolicyBuilder};
131use crate::dataset::refs::{BranchContents, BranchIdentifier, Branches, Tags};
132use crate::dataset::sql::SqlQueryBuilder;
133use crate::datatypes::Schema;
134use crate::index::retain_supported_indices;
135use crate::io::commit::{
136    DEFAULT_COMMIT_RETRY_TIMEOUT, commit_detached_transaction, commit_new_dataset,
137    commit_transaction, detect_overlapping_fragments,
138};
139use crate::session::Session;
140use crate::utils::temporal::{SystemTime, timestamp_to_nanos, utc_now};
141use crate::{Error, Result};
142pub use blob::{
143    BlobFile, BlobRangeRequest, BlobReadRange, ReadBlob, ReadBlobRange, ReadBlobRangesBuilder,
144    ReadBlobRangesStream, ReadBlobsBuilder, ReadBlobsStream,
145};
146use hash_joiner::HashJoiner;
147pub use lance_core::ROW_ID;
148use lance_core::box_error;
149use lance_index::scalar::lance_format::LanceIndexStore;
150use lance_namespace::models::{DeclareTableRequest, DescribeTableRequest};
151use lance_table::feature_flags::{apply_feature_flags, can_read_dataset};
152use lance_table::io::deletion::{DELETIONS_DIR, relative_deletion_file_path};
153use lance_table::rowids::{RowIdSequence, write_row_ids};
154pub use schema_evolution::{
155    BatchInfo, BatchUDF, ColumnAlteration, NewColumnTransform, UDFCheckpointStore,
156};
157pub use take::TakeBuilder;
158use uuid::Uuid;
159pub use write::merge_insert::{
160    MergeInsertBuilder, MergeInsertJob, MergeInsertWriteMode, MergeStats, UncommittedMergeInsert,
161    WhenMatched, WhenNotMatched, WhenNotMatchedBySource,
162};
163
164use crate::dataset::index::LanceIndexStoreExt;
165pub use write::update::{UpdateBuilder, UpdateJob};
166#[allow(deprecated)]
167pub use write::{
168    AutoCleanupParams, CommitBuilder, DEFAULT_COMMIT_TIMEOUT, DeleteBuilder, DeleteResult,
169    ExternalBlobMode, InsertBuilder, UncommittedDelete, WriteDestination, WriteMode, WriteParams,
170    WriteProgressFn, WriteStats, write_fragments,
171};
172
173pub(crate) const INDICES_DIR: &str = "_indices";
174pub(crate) const DATA_DIR: &str = "data";
175pub(crate) const TRANSACTIONS_DIR: &str = "_transactions";
176
177// We default to 6GB for the index cache, since indices are often large but
178// worth caching.
179pub const DEFAULT_INDEX_CACHE_SIZE: usize = 6 * 1024 * 1024 * 1024;
180// Default to 1 GiB for the metadata cache. Column metadata can be like 40MB,
181// so this should be enough for a few hundred columns. Other metadata is much
182// smaller.
183pub const DEFAULT_METADATA_CACHE_SIZE: usize = 1024 * 1024 * 1024;
184
185/// Lance Dataset
186#[derive(Clone)]
187pub struct Dataset {
188    /// The primary dataset object store. Use [`Self::object_store`] when
189    /// resolving files that may carry a base id.
190    pub(crate) object_store: Arc<ObjectStore>,
191    pub(crate) commit_handler: Arc<dyn CommitHandler>,
192    /// Uri of the dataset.
193    ///
194    /// On cloud storage, we can not use [Dataset::base] to build the full uri because the
195    /// `bucket` is swallowed in the inner [ObjectStore].
196    uri: String,
197    pub(crate) base: Path,
198    pub manifest: Arc<Manifest>,
199    // Path for the manifest that is loaded. Used to get additional information,
200    // such as the index metadata.
201    pub(crate) manifest_location: ManifestLocation,
202    pub(crate) session: Arc<Session>,
203    pub refs: Refs,
204
205    // Bitmap of fragment ids in this dataset.
206    pub(crate) fragment_bitmap: Arc<RoaringBitmap>,
207
208    // These are references to session caches, but with the dataset URI as a prefix.
209    pub(crate) index_cache: Arc<DSIndexCache>,
210    pub(crate) metadata_cache: Arc<DSMetadataCache>,
211
212    /// File reader options to use when reading data files.
213    pub(crate) file_reader_options: Option<FileReaderOptions>,
214
215    /// Object store parameters used when opening this dataset.
216    /// These are used when creating object stores for additional base paths.
217    pub(crate) store_params: Option<Box<ObjectStoreParams>>,
218    /// Optional runtime-only object store parameters keyed by base path URI.
219    pub(crate) base_store_params: Option<Arc<HashMap<String, ObjectStoreParams>>>,
220    /// Object stores for additional base paths, normally shared across clones.
221    /// Applying new object store wrappers starts a fresh cache scope.
222    pub(crate) base_object_stores: BaseObjectStores,
223}
224
225/// The `OnceCell` coalesces concurrent first resolutions into one build.
226pub(crate) type BaseObjectStores =
227    Arc<std::sync::Mutex<HashMap<u32, Arc<tokio::sync::OnceCell<Arc<ObjectStore>>>>>>;
228
229impl std::fmt::Debug for Dataset {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("Dataset")
232            .field("uri", &self.uri)
233            .field("base", &self.base)
234            .field("version", &self.manifest.version)
235            .field("cache_num_items", &self.session.approx_num_items())
236            .field("base_store_params", &self.base_store_params.is_some())
237            .finish()
238    }
239}
240
241/// Dataset Version
242#[derive(Deserialize, Serialize, Debug)]
243pub struct Version {
244    /// version number
245    pub version: u64,
246
247    /// Timestamp of dataset creation in UTC.
248    pub timestamp: DateTime<Utc>,
249
250    /// Key-value pairs of metadata.
251    pub metadata: BTreeMap<String, String>,
252}
253
254/// A lightweight reference to an attached dataset version, which could be used to uniquely identify a version.
255#[non_exhaustive]
256#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
257pub struct VersionRef {
258    /// Version number within the current branch's history.
259    pub version: u64,
260}
261
262/// Convert Manifest to Data Version.
263impl From<&Manifest> for Version {
264    fn from(m: &Manifest) -> Self {
265        Self {
266            version: m.version,
267            timestamp: m.timestamp(),
268            metadata: m.summary().into(),
269        }
270    }
271}
272
273/// The transaction that produced a version of the dataset, along with the
274/// version's commit timestamp.
275///
276/// Returned by [`Dataset::read_version_transaction`], which reads this
277/// information directly from storage without checking out the version.
278#[derive(Debug, Clone)]
279pub struct VersionTransaction {
280    /// Version number.
281    pub version: u64,
282
283    /// Timestamp the version was committed, in UTC.
284    pub timestamp: DateTime<Utc>,
285
286    /// The transaction that produced this version, if one was recorded.
287    pub transaction: Option<Transaction>,
288}
289
290/// Customize read behavior of a dataset.
291#[derive(Clone, Debug)]
292pub struct ReadParams {
293    /// Size of the index cache in bytes. This cache stores index data in memory
294    /// for faster lookups. The default is 6 GiB.
295    pub index_cache_size_bytes: usize,
296
297    /// Size of the metadata cache in bytes. This cache stores metadata in memory
298    /// for faster open table and scans. The default is 1 GiB.
299    pub metadata_cache_size_bytes: usize,
300
301    /// If present, dataset will use this shared [`Session`] instead creating a new one.
302    ///
303    /// This is useful for sharing the same session across multiple datasets.
304    pub session: Option<Arc<Session>>,
305
306    pub store_options: Option<ObjectStoreParams>,
307
308    /// If present, dataset will use this to resolve the latest version
309    ///
310    /// Lance needs to be able to make atomic updates to the manifest.  This involves
311    /// coordination between readers and writers and we can usually rely on the filesystem
312    /// to do this coordination for us.
313    ///
314    /// Some file systems (e.g. S3) do not support atomic operations.  In this case, for
315    /// safety, we recommend an external commit mechanism (such as dynamodb) and, on the
316    /// read path, we need to reach out to that external mechanism to figure out the latest
317    /// version of the dataset.
318    ///
319    /// If this is not set then a default behavior is chosen that is appropriate for the
320    /// filesystem.
321    ///
322    /// If a custom object store is provided (via store_params.object_store) then this
323    /// must also be provided.
324    pub commit_handler: Option<Arc<dyn CommitHandler>>,
325
326    /// File reader options to use when reading data files.
327    ///
328    /// This allows control over features like caching repetition indices and validation.
329    /// Options set here act as dataset-level defaults and can be overridden on a
330    /// per-scan basis via [`Scanner::batch_size_bytes`](crate::dataset::scanner::Scanner::batch_size_bytes) or
331    /// [`Scanner::with_file_reader_options`](crate::dataset::scanner::Scanner::with_file_reader_options).
332    pub file_reader_options: Option<FileReaderOptions>,
333}
334
335impl ReadParams {
336    /// Set the cache size for indices. Set to zero, to disable the cache.
337    #[deprecated(
338        since = "0.30.0",
339        note = "Use `index_cache_size_bytes` instead, which accepts a size in bytes."
340    )]
341    pub fn index_cache_size(&mut self, cache_size: usize) -> &mut Self {
342        let assumed_entry_size = 20 * 1024 * 1024; // 20 MiB per entry
343        self.index_cache_size_bytes = cache_size * assumed_entry_size;
344        self
345    }
346
347    pub fn index_cache_size_bytes(&mut self, cache_size: usize) -> &mut Self {
348        self.index_cache_size_bytes = cache_size;
349        self
350    }
351
352    /// Set the cache size for the file metadata. Set to zero to disable this cache.
353    #[deprecated(
354        since = "0.30.0",
355        note = "Use `metadata_cache_size_bytes` instead, which accepts a size in bytes."
356    )]
357    pub fn metadata_cache_size(&mut self, cache_size: usize) -> &mut Self {
358        let assumed_entry_size = 10 * 1024 * 1024; // 10 MiB per entry
359        self.metadata_cache_size_bytes = cache_size * assumed_entry_size;
360        self
361    }
362
363    /// Set the cache size for the file metadata in bytes.
364    pub fn metadata_cache_size_bytes(&mut self, cache_size: usize) -> &mut Self {
365        self.metadata_cache_size_bytes = cache_size;
366        self
367    }
368
369    /// Set a shared session for the datasets.
370    pub fn session(&mut self, session: Arc<Session>) -> &mut Self {
371        self.session = Some(session);
372        self
373    }
374
375    /// Use the explicit locking to resolve the latest version
376    pub fn set_commit_lock<T: CommitLock + Send + Sync + 'static>(&mut self, lock: Arc<T>) {
377        self.commit_handler = Some(Arc::new(lock));
378    }
379
380    /// Set the file reader options.
381    pub fn file_reader_options(&mut self, options: FileReaderOptions) -> &mut Self {
382        self.file_reader_options = Some(options);
383        self
384    }
385}
386
387impl Default for ReadParams {
388    fn default() -> Self {
389        Self {
390            index_cache_size_bytes: DEFAULT_INDEX_CACHE_SIZE,
391            metadata_cache_size_bytes: DEFAULT_METADATA_CACHE_SIZE,
392            session: None,
393            store_options: None,
394            commit_handler: None,
395            file_reader_options: None,
396        }
397    }
398}
399
400#[derive(Debug, Clone)]
401pub enum ProjectionRequest {
402    Schema(Arc<Schema>),
403    Sql(Vec<(String, String)>),
404}
405
406impl ProjectionRequest {
407    pub fn from_columns(
408        columns: impl IntoIterator<Item = impl AsRef<str>>,
409        dataset_schema: &Schema,
410    ) -> Self {
411        let columns = columns
412            .into_iter()
413            .map(|s| s.as_ref().to_string())
414            .collect::<Vec<_>>();
415
416        let schema = dataset_schema
417            .project_preserve_system_columns(&columns)
418            .unwrap();
419        Self::Schema(Arc::new(schema))
420    }
421
422    pub fn from_schema(schema: Schema) -> Self {
423        Self::Schema(Arc::new(schema))
424    }
425
426    /// Provide a list of projection with SQL transform.
427    ///
428    /// # Parameters
429    /// - `columns`: A list of tuples where the first element is resulted column name and the second
430    ///   element is the SQL expression.
431    pub fn from_sql(
432        columns: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
433    ) -> Self {
434        Self::Sql(
435            columns
436                .into_iter()
437                .map(|(a, b)| (a.into(), b.into()))
438                .collect(),
439        )
440    }
441
442    pub fn into_projection_plan(self, dataset: Arc<Dataset>) -> Result<ProjectionPlan> {
443        match self {
444            Self::Schema(schema) => {
445                // The schema might contain system columns (_rowid, _rowaddr) which are not
446                // in the dataset schema. We handle these specially in ProjectionPlan::from_schema.
447                let system_columns_present = schema
448                    .fields
449                    .iter()
450                    .any(|f| lance_core::is_system_column(&f.name));
451
452                if system_columns_present {
453                    // If system columns are present, we can't use project_by_schema directly
454                    // Just pass the schema to ProjectionPlan::from_schema which handles it
455                    ProjectionPlan::from_schema(dataset, schema.as_ref())
456                } else {
457                    // No system columns, use normal path with validation
458                    let projection = dataset.schema().project_by_schema(
459                        schema.as_ref(),
460                        OnMissing::Error,
461                        OnTypeMismatch::Error,
462                    )?;
463                    ProjectionPlan::from_schema(dataset, &projection)
464                }
465            }
466            Self::Sql(columns) => ProjectionPlan::from_expressions(dataset, &columns),
467        }
468    }
469}
470
471impl From<Arc<Schema>> for ProjectionRequest {
472    fn from(schema: Arc<Schema>) -> Self {
473        Self::Schema(schema)
474    }
475}
476
477impl From<Schema> for ProjectionRequest {
478    fn from(schema: Schema) -> Self {
479        Self::from(Arc::new(schema))
480    }
481}
482
483impl Dataset {
484    /// Open an existing dataset.
485    ///
486    /// See also [DatasetBuilder].
487    #[instrument]
488    pub async fn open(uri: &str) -> Result<Self> {
489        DatasetBuilder::from_uri(uri).load().await
490    }
491
492    /// Check out a dataset version with a ref
493    pub async fn checkout_version(&self, version: impl Into<refs::Ref>) -> Result<Self> {
494        let reference: refs::Ref = version.into();
495        match reference {
496            refs::Ref::Version(branch, version_number) => {
497                self.checkout_by_ref(version_number, branch.as_deref())
498                    .await
499            }
500            refs::Ref::VersionNumber(version_number) => {
501                self.checkout_by_ref(Some(version_number), self.manifest.branch.as_deref())
502                    .await
503            }
504            refs::Ref::Tag(tag_name) => {
505                let tag_contents = self.tags().get(tag_name.as_str()).await?;
506                self.checkout_by_ref(Some(tag_contents.version), tag_contents.branch.as_deref())
507                    .await
508            }
509        }
510    }
511
512    pub fn tags(&self) -> Tags<'_> {
513        self.refs.tags()
514    }
515
516    /// A handle for cheap, index-derived statistics about this dataset (e.g. a
517    /// column's global value range) that never scan data.
518    pub fn statistics(&self) -> DatasetStatistics<'_> {
519        DatasetStatistics::new(self)
520    }
521
522    pub fn branches(&self) -> Branches<'_> {
523        self.refs.branches()
524    }
525
526    /// Check out the latest version of the dataset
527    pub async fn checkout_latest(&mut self) -> Result<()> {
528        let (manifest, manifest_location) = self.latest_manifest().await?;
529        self.set_manifest(manifest, manifest_location);
530        Ok(())
531    }
532
533    /// Replace the manifest, refreshing derived state. Base stores are kept
534    /// when `base_paths` is unchanged.
535    fn set_manifest(&mut self, manifest: Arc<Manifest>, manifest_location: ManifestLocation) {
536        if manifest.base_paths != self.manifest.base_paths {
537            self.base_object_stores = Default::default();
538        }
539        self.manifest = manifest;
540        self.manifest_location = manifest_location;
541        self.fragment_bitmap = Arc::new(
542            self.manifest
543                .fragments
544                .iter()
545                .map(|f| f.id as u32)
546                .collect(),
547        );
548    }
549
550    /// Check out the latest version of the branch
551    pub async fn checkout_branch(&self, branch: &str) -> Result<Self> {
552        self.checkout_by_ref(None, Some(branch)).await
553    }
554
555    /// This is a two-phase operation:
556    /// - Create the branch dataset by shallow cloning.
557    /// - Create the branch metadata (a.k.a. `BranchContents`).
558    ///
559    /// These two phases are not atomic. We consider `BranchContents` as the source of truth
560    /// for the branch.
561    ///
562    /// The cleanup procedure should:
563    /// - Clean up zombie branch datasets that have no related `BranchContents`.
564    /// - Delete broken `BranchContents` entries that have no related branch dataset.
565    ///
566    /// If `create_branch` stops at phase 1, it may leave a zombie branch dataset,
567    /// which can be cleaned up later. Such a zombie dataset may cause a branch creation
568    /// failure if we use the same name to `create_branch`. In that case, you need to call
569    /// `force_delete_branch` to interactively clean up the zombie dataset.
570    pub async fn create_branch(
571        &mut self,
572        branch: &str,
573        version: impl Into<refs::Ref>,
574        store_params: Option<ObjectStoreParams>,
575    ) -> Result<Self> {
576        let (source_branch, version_number) = self.resolve_reference(version.into()).await?;
577        let branch_location = self.branch_location().find_branch(Some(branch))?;
578        let source_location = self
579            .branch_location()
580            .find_branch(source_branch.as_deref())?;
581        let clone_op = Operation::Clone {
582            is_shallow: true,
583            ref_name: source_branch.clone(),
584            ref_version: version_number,
585            ref_path: source_location.uri,
586            branch_name: Some(branch.to_string()),
587        };
588        let transaction = Transaction::new(version_number, clone_op, None);
589
590        let builder = CommitBuilder::new(WriteDestination::Uri(branch_location.uri.as_str()))
591            // Fall back to the dataset's own store params
592            .with_store_params(
593                store_params.unwrap_or(self.store_params.as_deref().cloned().unwrap_or_default()),
594            )
595            .with_object_store(Arc::new(self.object_store.as_ref().clone()))
596            .with_commit_handler(self.commit_handler.clone())
597            .with_exact_storage_format(self.manifest.data_storage_format.lance_file_format());
598        let dataset = builder.execute(transaction).await?;
599
600        // Create BranchContents after shallow_clone
601        self.branches()
602            .create(branch, version_number, source_branch.as_deref())
603            .await?;
604        Ok(dataset)
605    }
606
607    pub async fn delete_branch(&mut self, branch: &str) -> Result<()> {
608        self.branches().delete(branch, false).await
609    }
610
611    /// Delete the branch even if the BranchContents is not found.
612    /// This could be useful when we have zombie branches and want to clean them up immediately.
613    pub async fn force_delete_branch(&mut self, branch: &str) -> Result<()> {
614        self.branches().delete(branch, true).await
615    }
616
617    pub async fn list_branches(&self) -> Result<HashMap<String, BranchContents>> {
618        self.branches().list().await
619    }
620
621    fn already_checked_out(&self, location: &ManifestLocation, branch_name: Option<&str>) -> bool {
622        // The ETag is an opaque object-generation token, not a content hash.
623        // Comparing the token still prevents reusing this Dataset's manifest
624        // after the physical object was replaced, for example by a recent
625        // drop/recreate at the same URI and version.
626        self.manifest.branch.as_deref() == branch_name
627            && self.manifest.version == location.version
628            && self.manifest_location.naming_scheme == location.naming_scheme
629            && location.e_tag.as_ref().is_some_and(|e_tag| {
630                self.manifest_location
631                    .e_tag
632                    .as_ref()
633                    .is_some_and(|current_e_tag| e_tag == current_e_tag)
634            })
635    }
636
637    async fn checkout_by_ref(
638        &self,
639        version_number: Option<u64>,
640        branch: Option<&str>,
641    ) -> Result<Self> {
642        // Reject malformed names at the boundary (mirroring the branch CRUD
643        // paths) so they fail as InvalidRef instead of tripping the wrong-chain
644        // check below
645        if let Some(branch_name) = branch
646            && !Branches::is_main_branch(branch)
647        {
648            refs::check_valid_branch(branch_name)?;
649        }
650
651        let new_location = self.branch_location().find_branch(branch)?;
652
653        let manifest_location = if let Some(version_number) = version_number {
654            self.commit_handler
655                .resolve_version_location(
656                    &new_location.path,
657                    version_number,
658                    &self.object_store.inner,
659                )
660                .await?
661        } else {
662            self.commit_handler
663                .resolve_latest_location(&new_location.path, &self.object_store)
664                .await?
665        };
666
667        if self.already_checked_out(&manifest_location, branch) {
668            return Ok(self.clone());
669        }
670
671        let manifest = Self::get_manifest(
672            self.object_store.as_ref(),
673            &manifest_location,
674            &new_location.uri,
675            self.session.as_ref(),
676        )
677        .await?;
678
679        // The resolved manifest must belong to the requested branch. A mismatch
680        // means the commit handler resolved against a different chain (for
681        // example an external manifest store that ignores branch-qualified
682        // paths); error loudly rather than hand back another branch's data.
683        let requested_branch = branch.and_then(refs::standardize_branch);
684        if manifest.branch.as_deref() != requested_branch.as_deref() {
685            return Err(Error::internal(format!(
686                "checkout of branch '{}' at version {} resolved a manifest belonging to branch '{}'",
687                refs::normalize_branch(branch),
688                manifest.version,
689                refs::normalize_branch(manifest.branch.as_deref()),
690            )));
691        }
692
693        Self::checkout_manifest(
694            self.object_store.clone(),
695            new_location.path,
696            new_location.uri,
697            manifest,
698            manifest_location,
699            self.session.clone(),
700            self.commit_handler.clone(),
701            self.file_reader_options.clone(),
702            self.store_params.as_deref().cloned(),
703            self.base_store_params.clone(),
704        )
705    }
706
707    pub(crate) async fn load_manifest(
708        object_store: &ObjectStore,
709        manifest_location: &ManifestLocation,
710        uri: &str,
711        session: &Session,
712    ) -> Result<Manifest> {
713        let object_reader = if let Some(size) = manifest_location.size {
714            object_store
715                .open_with_size(&manifest_location.path, size as usize)
716                .await
717        } else {
718            object_store.open(&manifest_location.path).await
719        };
720        let object_reader = object_reader.map_err(|e| match &e {
721            Error::NotFound { uri, .. } => Error::dataset_not_found(uri.clone(), box_error(e)),
722            _ => e,
723        })?;
724
725        let last_block =
726            read_last_block(object_reader.as_ref())
727                .await
728                .map_err(|err| match err {
729                    object_store::Error::NotFound { path, source } => {
730                        Error::dataset_not_found(path, source)
731                    }
732                    _ => Error::io_source(err.into()),
733                })?;
734
735        // A stale cached size yields a bogus footer offset. Detect it (the block
736        // lacks the trailing magic) and retry with the true size, like
737        // read_manifest.
738        if manifest_location.size.is_some() && !last_block.ends_with(MAGIC) {
739            let manifest_location = ManifestLocation {
740                size: None,
741                ..manifest_location.clone()
742            };
743            return Box::pin(Self::load_manifest(
744                object_store,
745                &manifest_location,
746                uri,
747                session,
748            ))
749            .await;
750        }
751
752        let offset = read_metadata_offset(&last_block)?;
753
754        // If manifest is in the last block, we can decode directly from memory.
755        let manifest_size = object_reader.size().await?;
756        let mut manifest = if manifest_size - offset <= last_block.len() {
757            let manifest_len = manifest_size - offset;
758            let offset_in_block = last_block.len() - manifest_len;
759            let message_len =
760                LittleEndian::read_u32(&last_block[offset_in_block..offset_in_block + 4]) as usize;
761            let message_data = &last_block[offset_in_block + 4..offset_in_block + 4 + message_len];
762            Manifest::try_from(lance_table::format::pb::Manifest::decode(message_data)?)
763        } else {
764            read_struct(object_reader.as_ref(), offset).await
765        }?;
766
767        if !can_read_dataset(manifest.reader_feature_flags) {
768            let message = format!(
769                "This dataset cannot be read by this version of Lance. \
770                 Please upgrade Lance to read this dataset.\n Flags: {}",
771                manifest.reader_feature_flags
772            );
773            return Err(Error::not_supported_source(message.into()));
774        }
775
776        // If indices were also in the last block, we can take the opportunity to
777        // decode them now and cache them.
778        if let Some(index_offset) = manifest.index_section
779            && manifest_size - index_offset <= last_block.len()
780        {
781            let offset_in_block = last_block.len() - (manifest_size - index_offset);
782            let message_len =
783                LittleEndian::read_u32(&last_block[offset_in_block..offset_in_block + 4]) as usize;
784            let message_data = &last_block[offset_in_block + 4..offset_in_block + 4 + message_len];
785            let section = lance_table::format::pb::IndexSection::decode(message_data)?;
786            let mut indices: Vec<IndexMetadata> = section
787                .indices
788                .into_iter()
789                .map(IndexMetadata::try_from)
790                .collect::<Result<Vec<_>>>()?;
791            retain_supported_indices(&mut indices);
792            let ds_index_cache = session.index_cache.for_dataset(uri);
793            let metadata_key = crate::session::index_caches::IndexMetadataKey {
794                version: manifest_location.version,
795                store_identity: &object_store.store_prefix,
796            };
797            ds_index_cache
798                .insert_with_key(&metadata_key, Arc::new(indices))
799                .await;
800        }
801
802        // If transaction is also in the last block, we can take the opportunity to
803        // decode them now and cache them.
804        if let Some(transaction_offset) = manifest.transaction_section
805            && manifest_size - transaction_offset <= last_block.len()
806        {
807            let offset_in_block = last_block.len() - (manifest_size - transaction_offset);
808            let message_len =
809                LittleEndian::read_u32(&last_block[offset_in_block..offset_in_block + 4]) as usize;
810            let message_data = &last_block[offset_in_block + 4..offset_in_block + 4 + message_len];
811            let transaction: Transaction =
812                lance_table::format::pb::Transaction::decode(message_data)?.try_into()?;
813
814            let metadata_cache = session.metadata_cache.for_dataset(uri);
815            let metadata_key = TransactionKey {
816                version: manifest_location.version,
817            };
818            metadata_cache
819                .insert_with_key(&metadata_key, Arc::new(transaction))
820                .await;
821        }
822
823        populate_manifest_schema_dictionaries(&mut manifest, object_reader.as_ref()).await?;
824
825        Ok(manifest)
826    }
827
828    /// Fetch the manifest for `manifest_location` from the session metadata
829    /// cache, loading and caching it on a miss.
830    pub(crate) async fn get_manifest(
831        object_store: &ObjectStore,
832        manifest_location: &ManifestLocation,
833        uri: &str,
834        session: &Session,
835    ) -> Result<Arc<Manifest>> {
836        if manifest_location.size.is_none() {
837            return Ok(Arc::new(
838                Self::load_manifest(object_store, manifest_location, uri, session).await?,
839            ));
840        }
841        let metadata_cache = session.metadata_cache.for_dataset(uri);
842        let manifest_key = ManifestKey {
843            version: manifest_location.version,
844            e_tag: manifest_location.e_tag.as_deref(),
845        };
846        if let Some(cached) = metadata_cache.get_with_key(&manifest_key).await {
847            return Ok(cached);
848        }
849        let loaded =
850            Arc::new(Self::load_manifest(object_store, manifest_location, uri, session).await?);
851        metadata_cache
852            .insert_with_key(&manifest_key, loaded.clone())
853            .await;
854        Ok(loaded)
855    }
856
857    #[allow(clippy::too_many_arguments)]
858    fn checkout_manifest(
859        object_store: Arc<ObjectStore>,
860        base_path: Path,
861        uri: String,
862        manifest: Arc<Manifest>,
863        manifest_location: ManifestLocation,
864        session: Arc<Session>,
865        commit_handler: Arc<dyn CommitHandler>,
866        file_reader_options: Option<FileReaderOptions>,
867        store_params: Option<ObjectStoreParams>,
868        base_store_params: Option<Arc<HashMap<String, ObjectStoreParams>>>,
869    ) -> Result<Self> {
870        let refs = Refs::new(
871            object_store.clone(),
872            commit_handler.clone(),
873            BranchLocation {
874                path: base_path.clone(),
875                uri: uri.clone(),
876                branch: manifest.branch.clone(),
877            },
878        );
879        let metadata_cache = Arc::new(session.metadata_cache.for_dataset(&uri));
880        let index_cache = Arc::new(session.index_cache.for_dataset(&uri));
881        let fragment_bitmap = Arc::new(manifest.fragments.iter().map(|f| f.id as u32).collect());
882        write::log_unregistered_base_scoped_options(
883            store_params.as_ref(),
884            &manifest.base_paths,
885            log::Level::Debug,
886        );
887        Ok(Self {
888            object_store,
889            base: base_path,
890            uri,
891            manifest,
892            manifest_location,
893            commit_handler,
894            session,
895            refs,
896            fragment_bitmap,
897            metadata_cache,
898            index_cache,
899            file_reader_options,
900            store_params: store_params.map(Box::new),
901            base_store_params,
902            base_object_stores: Default::default(),
903        })
904    }
905
906    /// Write to or Create a [Dataset] with a stream of [RecordBatch]s.
907    ///
908    /// `dest` can be a `&str`, `object_store::path::Path` or `Arc<Dataset>`.
909    ///
910    /// Returns the newly created [`Dataset`].
911    /// Or Returns [Error] if the dataset already exists.
912    ///
913    pub async fn write(
914        batches: impl RecordBatchReader + Send + 'static,
915        dest: impl Into<WriteDestination<'_>>,
916        params: Option<WriteParams>,
917    ) -> Result<Self> {
918        let mut builder = InsertBuilder::new(dest);
919        if let Some(params) = &params {
920            builder = builder.with_params(params);
921        }
922        Box::pin(builder.execute_stream(Box::new(batches) as Box<dyn RecordBatchReader + Send>))
923            .await
924    }
925
926    /// Write into a namespace client-managed table with automatic credential vending.
927    ///
928    /// For CREATE mode, calls declare_table() to initialize the table.
929    /// For other modes, calls describe_table() and opens dataset with namespace client credentials.
930    ///
931    /// # Arguments
932    ///
933    /// * `batches` - The record batches to write
934    /// * `namespace_client` - The namespace client to use for table management
935    /// * `table_id` - The table identifier
936    /// * `params` - Write parameters
937    pub async fn write_into_namespace(
938        batches: impl RecordBatchReader + Send + 'static,
939        namespace_client: Arc<dyn LanceNamespace>,
940        table_id: Vec<String>,
941        params: Option<WriteParams>,
942    ) -> Result<Self> {
943        Self::write_into_namespace_impl(batches, namespace_client, table_id, None, params).await
944    }
945
946    /// Write into a branch of a namespace client-managed table.
947    ///
948    /// Behaves like [`write_into_namespace`](Self::write_into_namespace), but APPEND and
949    /// OVERWRITE open and commit against `branch` instead of main. CREATE is rejected,
950    /// since a branch forks from an existing version.
951    pub async fn write_into_namespace_on_branch(
952        batches: impl RecordBatchReader + Send + 'static,
953        namespace_client: Arc<dyn LanceNamespace>,
954        table_id: Vec<String>,
955        branch: &str,
956        params: Option<WriteParams>,
957    ) -> Result<Self> {
958        Self::write_into_namespace_impl(
959            batches,
960            namespace_client,
961            table_id,
962            Some(branch.to_string()),
963            params,
964        )
965        .await
966    }
967
968    async fn write_into_namespace_impl(
969        batches: impl RecordBatchReader + Send + 'static,
970        namespace_client: Arc<dyn LanceNamespace>,
971        table_id: Vec<String>,
972        branch: Option<String>,
973        mut params: Option<WriteParams>,
974    ) -> Result<Self> {
975        let mut write_params = params.take().unwrap_or_default();
976
977        match write_params.mode {
978            WriteMode::Create => {
979                if branch.is_some() {
980                    return Err(Error::not_supported_source(
981                        "cannot create a table on a branch; create on main first, then branch it"
982                            .into(),
983                    ));
984                }
985                let declare_request = DeclareTableRequest {
986                    id: Some(table_id.clone()),
987                    ..Default::default()
988                };
989                let response = namespace_client
990                    .declare_table(declare_request)
991                    .await
992                    .map_err(|e| Error::namespace_source(Box::new(e)))?;
993
994                let uri = response.location.ok_or_else(|| {
995                    Error::namespace_source(Box::new(std::io::Error::other(
996                        "Table location not found in declare_table response",
997                    )))
998                })?;
999
1000                // Set up commit handler when managed_versioning is enabled
1001                if response.managed_versioning == Some(true) {
1002                    // The store derives the branch a request targets from the
1003                    // base path it is handed, resolved against the table root.
1004                    let external_store = LanceNamespaceExternalManifestStore::for_table_uri(
1005                        namespace_client.clone(),
1006                        table_id.clone(),
1007                        &uri,
1008                    )?;
1009                    let commit_handler: Arc<dyn CommitHandler> =
1010                        Arc::new(ExternalManifestCommitHandler {
1011                            external_manifest_store: Arc::new(external_store),
1012                        });
1013                    write_params.commit_handler = Some(commit_handler);
1014                }
1015
1016                // Set initial credentials and provider from namespace_client
1017                if let Some(namespace_storage_options) = response.storage_options {
1018                    let provider: Arc<dyn StorageOptionsProvider> = Arc::new(
1019                        LanceNamespaceStorageOptionsProvider::new(namespace_client, table_id),
1020                    );
1021
1022                    // Merge namespace client storage options with any existing options
1023                    let mut merged_options = write_params
1024                        .store_params
1025                        .as_ref()
1026                        .and_then(|p| p.storage_options().cloned())
1027                        .unwrap_or_default();
1028                    merged_options.extend(namespace_storage_options);
1029
1030                    let accessor = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
1031                        merged_options,
1032                        provider,
1033                    ));
1034
1035                    let existing_params = write_params.store_params.take().unwrap_or_default();
1036                    write_params.store_params = Some(ObjectStoreParams {
1037                        storage_options_accessor: Some(accessor),
1038                        ..existing_params
1039                    });
1040                }
1041
1042                Self::write(batches, uri.as_str(), Some(write_params)).await
1043            }
1044            WriteMode::Append | WriteMode::Overwrite => {
1045                let request = DescribeTableRequest {
1046                    id: Some(table_id.clone()),
1047                    ..Default::default()
1048                };
1049                let response = namespace_client
1050                    .describe_table(request)
1051                    .await
1052                    .map_err(|e| Error::namespace_source(Box::new(e)))?;
1053
1054                let uri = response.location.ok_or_else(|| {
1055                    Error::namespace_source(Box::new(std::io::Error::other(
1056                        "Table location not found in describe_table response",
1057                    )))
1058                })?;
1059
1060                // Set up commit handler when managed_versioning is enabled.
1061                // It must ride on the dataset opened below: InsertBuilder
1062                // commits through the destination dataset's handler and does
1063                // not consult write params for Dataset destinations.
1064                let commit_handler: Option<Arc<dyn CommitHandler>> =
1065                    if response.managed_versioning == Some(true) {
1066                        // The store derives the branch a request targets from the
1067                        // base path it is handed, resolved against the table root.
1068                        let external_store = LanceNamespaceExternalManifestStore::for_table_uri(
1069                            namespace_client.clone(),
1070                            table_id.clone(),
1071                            uri.as_str(),
1072                        )?;
1073                        Some(Arc::new(ExternalManifestCommitHandler {
1074                            external_manifest_store: Arc::new(external_store),
1075                        }))
1076                    } else {
1077                        None
1078                    };
1079
1080                // Set initial credentials and provider from namespace_client
1081                if let Some(namespace_storage_options) = response.storage_options {
1082                    let provider: Arc<dyn StorageOptionsProvider> =
1083                        Arc::new(LanceNamespaceStorageOptionsProvider::new(
1084                            namespace_client.clone(),
1085                            table_id.clone(),
1086                        ));
1087
1088                    // Merge namespace client storage options with any existing options
1089                    let mut merged_options = write_params
1090                        .store_params
1091                        .as_ref()
1092                        .and_then(|p| p.storage_options().cloned())
1093                        .unwrap_or_default();
1094                    merged_options.extend(namespace_storage_options);
1095
1096                    let accessor = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
1097                        merged_options,
1098                        provider,
1099                    ));
1100
1101                    let existing_params = write_params.store_params.take().unwrap_or_default();
1102                    write_params.store_params = Some(ObjectStoreParams {
1103                        storage_options_accessor: Some(accessor),
1104                        ..existing_params
1105                    });
1106                }
1107
1108                // For APPEND/OVERWRITE modes, we must open the existing dataset first
1109                // and pass it to InsertBuilder. If we pass just the URI, InsertBuilder
1110                // assumes no dataset exists and converts the mode to CREATE.
1111                let mut builder = DatasetBuilder::from_uri(uri.as_str());
1112                if let Some(ref store_params) = write_params.store_params
1113                    && let Some(accessor) = &store_params.storage_options_accessor
1114                {
1115                    builder = builder.with_storage_options_accessor(accessor.clone());
1116                }
1117                if let Some(commit_handler) = commit_handler {
1118                    builder = builder.with_commit_handler(commit_handler);
1119                }
1120                if let Some(branch) = &branch {
1121                    builder = builder.with_branch(branch, None);
1122                }
1123                let dataset = Arc::new(builder.load().await?);
1124
1125                Self::write(batches, dataset, Some(write_params)).await
1126            }
1127        }
1128    }
1129
1130    /// Append to existing [Dataset] with a stream of [RecordBatch]s
1131    ///
1132    /// Returns void result or Returns [Error]
1133    pub async fn append(
1134        &mut self,
1135        batches: impl RecordBatchReader + Send + 'static,
1136        params: Option<WriteParams>,
1137    ) -> Result<()> {
1138        let write_params = WriteParams {
1139            mode: WriteMode::Append,
1140            ..params.unwrap_or_default()
1141        };
1142
1143        let new_dataset = InsertBuilder::new(WriteDestination::Dataset(Arc::new(self.clone())))
1144            .with_params(&write_params)
1145            .execute_stream(Box::new(batches) as Box<dyn RecordBatchReader + Send>)
1146            .await?;
1147
1148        *self = new_dataset;
1149
1150        Ok(())
1151    }
1152
1153    /// Get the fully qualified URI of this dataset.
1154    pub fn uri(&self) -> &str {
1155        &self.uri
1156    }
1157
1158    pub fn branch_location(&self) -> BranchLocation {
1159        BranchLocation {
1160            path: self.base.clone(),
1161            uri: self.uri.clone(),
1162            branch: self.manifest.branch.clone(),
1163        }
1164    }
1165
1166    pub async fn branch_identifier(&self) -> Result<BranchIdentifier> {
1167        self.refs
1168            .branches()
1169            .get_identifier(self.manifest.branch.as_deref())
1170            .await
1171    }
1172
1173    /// Get the full manifest of the dataset version.
1174    pub fn manifest(&self) -> &Manifest {
1175        &self.manifest
1176    }
1177
1178    pub fn manifest_location(&self) -> &ManifestLocation {
1179        &self.manifest_location
1180    }
1181
1182    /// Create a [`delta::DatasetDeltaBuilder`] to explore changes between dataset versions.
1183    ///
1184    /// # Example
1185    ///
1186    /// ```
1187    /// # use lance::{Dataset, Result};
1188    /// # async fn example(dataset: &Dataset) -> Result<()> {
1189    /// let delta = dataset.delta()
1190    ///     .compared_against_version(5)
1191    ///     .build()?;
1192    /// let inserted = delta.get_inserted_rows().await?;
1193    /// # Ok(())
1194    /// # }
1195    /// ```
1196    pub fn delta(&self) -> delta::DatasetDeltaBuilder {
1197        delta::DatasetDeltaBuilder::new(self.clone())
1198    }
1199
1200    pub async fn latest_manifest(&self) -> Result<(Arc<Manifest>, ManifestLocation)> {
1201        let location = self
1202            .commit_handler
1203            .resolve_latest_location(&self.base, &self.object_store)
1204            .await?;
1205
1206        // Check if manifest is in cache before reading from storage
1207        let manifest_key = ManifestKey {
1208            version: location.version,
1209            e_tag: location.e_tag.as_deref(),
1210        };
1211        let cached_manifest = self.metadata_cache.get_with_key(&manifest_key).await;
1212        if let Some(cached_manifest) = cached_manifest {
1213            return Ok((cached_manifest, location));
1214        }
1215
1216        if self.already_checked_out(&location, self.manifest.branch.as_deref()) {
1217            return Ok((self.manifest.clone(), self.manifest_location.clone()));
1218        }
1219        let mut manifest = read_manifest(&self.object_store, &location.path, location.size).await?;
1220        if manifest.schema.has_dictionary_types() {
1221            let reader = if let Some(size) = location.size {
1222                self.object_store
1223                    .open_with_size(&location.path, size as usize)
1224                    .await?
1225            } else {
1226                self.object_store.open(&location.path).await?
1227            };
1228            populate_manifest_schema_dictionaries(&mut manifest, reader.as_ref()).await?;
1229        }
1230        let manifest_arc = Arc::new(manifest);
1231        self.metadata_cache
1232            .insert_with_key(&manifest_key, manifest_arc.clone())
1233            .await;
1234        Ok((manifest_arc, location))
1235    }
1236
1237    /// Read the transaction file for this version of the dataset.
1238    ///
1239    /// If there was no transaction file written for this version of the dataset
1240    /// then this will return None.
1241    pub async fn read_transaction(&self) -> Result<Option<Transaction>> {
1242        let transaction_key = TransactionKey {
1243            version: self.manifest.version,
1244        };
1245        if let Some(transaction) = self.metadata_cache.get_with_key(&transaction_key).await {
1246            return Ok(Some((*transaction).clone()));
1247        }
1248
1249        let transaction = self
1250            .read_transaction_from_storage(&self.manifest, &self.manifest_location)
1251            .await?;
1252
1253        if let Some(tx) = transaction.as_ref() {
1254            self.metadata_cache
1255                .insert_with_key(&transaction_key, Arc::new(tx.clone()))
1256                .await;
1257        }
1258        Ok(transaction)
1259    }
1260
1261    /// Read the transaction recorded by `manifest` directly from storage,
1262    /// without consulting or populating any session cache.
1263    async fn read_transaction_from_storage(
1264        &self,
1265        manifest: &Manifest,
1266        manifest_location: &ManifestLocation,
1267    ) -> Result<Option<Transaction>> {
1268        // Prefer inline transaction from manifest when available
1269        if let Some(pos) = manifest.transaction_section {
1270            let reader = match manifest_location.size {
1271                Some(size) => {
1272                    self.object_store
1273                        .open_with_size(&manifest_location.path, size as usize)
1274                        .await?
1275                }
1276                None => self.object_store.open(&manifest_location.path).await?,
1277            };
1278
1279            // A concurrent overwrite can leave the listed size too small; retry
1280            // once with the true size.
1281            let tx: pb::Transaction = match read_message(reader.as_ref(), pos).await {
1282                Err(e)
1283                    if manifest_location.size.is_some()
1284                        && e.to_string().contains("file size is too small") =>
1285                {
1286                    let reader = self.object_store.open(&manifest_location.path).await?;
1287                    read_message(reader.as_ref(), pos).await?
1288                }
1289                other => other?,
1290            };
1291            Transaction::try_from(tx).map(Some)
1292        } else if let Some(path) = &manifest.transaction_file {
1293            // Fallback: read external transaction file if present
1294            let path = self.transactions_dir().join(path.as_str());
1295            let data = self.object_store.inner.get(&path).await?.bytes().await?;
1296            let transaction = lance_table::format::pb::Transaction::decode(data)?;
1297            Transaction::try_from(transaction).map(Some)
1298        } else {
1299            Ok(None)
1300        }
1301    }
1302
1303    /// Read the transaction (if any) and commit timestamp of a version of the
1304    /// dataset. `version` is a version number on this dataset's current branch.
1305    ///
1306    /// Reads the version's manifest transiently: no historical `Dataset` is
1307    /// constructed, no `IndexSection` is decoded, and no session cache is read
1308    /// or written, so scanning many historical versions does not fill the
1309    /// shared caches.
1310    ///
1311    /// Returns an error if the version does not exist (for example, if it has
1312    /// been cleaned up).
1313    ///
1314    /// # Example
1315    ///
1316    /// ```
1317    /// # use lance::{Dataset, Result};
1318    /// # async fn example(dataset: &Dataset) -> Result<()> {
1319    /// let record = dataset.read_version_transaction(5).await?;
1320    /// let committed_at = record.timestamp;
1321    /// let operation = record.transaction.as_ref().map(|t| t.operation.name());
1322    /// # Ok(())
1323    /// # }
1324    /// ```
1325    pub async fn read_version_transaction(&self, version: u64) -> Result<VersionTransaction> {
1326        // Resolve against this dataset's current branch.
1327        let manifest_location = self
1328            .commit_handler
1329            .resolve_version_location(&self.base, version, &self.object_store.inner)
1330            .await?;
1331
1332        // Keep the DatasetNotFound variant callers expect for a missing version.
1333        let manifest = read_manifest(
1334            &self.object_store,
1335            &manifest_location.path,
1336            manifest_location.size,
1337        )
1338        .await
1339        .map_err(|e| match &e {
1340            Error::NotFound { uri, .. } => Error::dataset_not_found(uri.clone(), box_error(e)),
1341            _ => e,
1342        })?;
1343
1344        // The resolved manifest must belong to this dataset's branch. A
1345        // mismatch means the commit handler resolved against a different chain
1346        // (for example an external manifest store that ignores
1347        // branch-qualified paths); error loudly rather than hand back another
1348        // branch's transaction.
1349        if manifest.branch != self.manifest.branch {
1350            return Err(Error::internal(format!(
1351                "reading version {} on branch '{}' resolved a manifest belonging to branch '{}'",
1352                version,
1353                refs::normalize_branch(self.manifest.branch.as_deref()),
1354                refs::normalize_branch(manifest.branch.as_deref()),
1355            )));
1356        }
1357
1358        let transaction = self
1359            .read_transaction_from_storage(&manifest, &manifest_location)
1360            .await?;
1361
1362        Ok(VersionTransaction {
1363            version: manifest.version,
1364            timestamp: manifest.timestamp(),
1365            transaction,
1366        })
1367    }
1368
1369    /// Read the transaction file for this version of the dataset.
1370    ///
1371    /// If there was no transaction file written for this version of the dataset
1372    /// then this will return None.
1373    ///
1374    /// Does not populate the session caches; see
1375    /// [`Self::read_version_transaction`].
1376    ///
1377    /// # Example
1378    ///
1379    /// ```
1380    /// # use lance::{Dataset, Result};
1381    /// # async fn example(dataset: &Dataset) -> Result<()> {
1382    /// let transaction = dataset.read_transaction_by_version(5).await?;
1383    /// let operation = transaction.as_ref().map(|t| t.operation.name());
1384    /// # Ok(())
1385    /// # }
1386    /// ```
1387    pub async fn read_transaction_by_version(&self, version: u64) -> Result<Option<Transaction>> {
1388        Ok(self.read_version_transaction(version).await?.transaction)
1389    }
1390
1391    /// List transactions for the dataset, up to a maximum number.
1392    ///
1393    /// This method iterates through dataset versions, starting from the current version,
1394    /// and collects the transaction for each version. It stops when either `recent_transactions`
1395    /// is reached or there are no more versions.
1396    ///
1397    /// # Arguments
1398    ///
1399    /// * `recent_transactions` - Maximum number of transactions to return
1400    ///
1401    /// # Returns
1402    ///
1403    /// A vector of optional transactions. Each element corresponds to a version,
1404    /// and may be None if no transaction file exists for that version.
1405    pub async fn get_transactions(
1406        &self,
1407        recent_transactions: usize,
1408    ) -> Result<Vec<Option<Transaction>>> {
1409        let mut transactions = vec![];
1410        let mut dataset = self.clone();
1411
1412        loop {
1413            let transaction = dataset.read_transaction().await?;
1414            transactions.push(transaction);
1415
1416            if transactions.len() >= recent_transactions {
1417                break;
1418            } else {
1419                match dataset
1420                    .checkout_version(dataset.version().version - 1)
1421                    .await
1422                {
1423                    Ok(ds) => dataset = ds,
1424                    Err(Error::DatasetNotFound { .. }) => break,
1425                    Err(err) => return Err(err),
1426                }
1427            }
1428        }
1429
1430        Ok(transactions)
1431    }
1432
1433    /// Restore the currently checked out version of the dataset as the latest version.
1434    pub async fn restore(&mut self) -> Result<()> {
1435        let (latest_manifest, _) = self.latest_manifest().await?;
1436        let latest_version = latest_manifest.version;
1437
1438        let transaction = Transaction::new(
1439            latest_version,
1440            Operation::Restore {
1441                version: self.manifest.version,
1442            },
1443            None,
1444        );
1445
1446        self.apply_commit(transaction, &Default::default(), &Default::default())
1447            .await?;
1448
1449        Ok(())
1450    }
1451
1452    /// Removes old versions of the dataset from disk
1453    ///
1454    /// This function will remove all versions of the dataset that are older than the provided
1455    /// timestamp.  This function will not remove the current version of the dataset.
1456    ///
1457    /// Once a version is removed it can no longer be checked out or restored.  Any data unique
1458    /// to that version will be lost.
1459    ///
1460    /// # Arguments
1461    ///
1462    /// * `older_than` - Versions older than this will be deleted.
1463    /// * `delete_unverified` - If false (the default) then files will only be deleted if they
1464    ///                        are listed in at least one manifest.  Otherwise these files will
1465    ///                        be kept since they cannot be distinguished from an in-progress
1466    ///                        transaction.  Set to true to delete these files if you are sure
1467    ///                        there are no other in-progress dataset operations.
1468    ///
1469    /// # Returns
1470    ///
1471    /// * `RemovalStats` - Statistics about the removal operation
1472    #[instrument(level = "debug", skip(self))]
1473    pub fn cleanup_old_versions(
1474        &self,
1475        older_than: Duration,
1476        delete_unverified: Option<bool>,
1477        error_if_tagged_old_versions: Option<bool>,
1478    ) -> BoxFuture<'_, Result<RemovalStats>> {
1479        let mut builder = CleanupPolicyBuilder::default();
1480        builder = builder.before_timestamp(utc_now() - older_than);
1481        if let Some(v) = delete_unverified {
1482            builder = builder.delete_unverified(v);
1483        }
1484        if let Some(v) = error_if_tagged_old_versions {
1485            builder = builder.error_if_tagged_old_versions(v);
1486        }
1487
1488        self.cleanup_with_policy(builder.build())
1489    }
1490
1491    /// Removes old versions of the dataset from storage
1492    ///
1493    /// This function will remove all versions of the dataset that satisfies the given policy.
1494    /// This function will not remove the current version of the dataset.
1495    ///
1496    /// Once a version is removed it can no longer be checked out or restored.  Any data unique
1497    /// to that version will be lost.
1498    ///
1499    /// # Arguments
1500    ///
1501    /// * `policy` - `CleanupPolicy` determines the behaviour of cleanup.
1502    ///
1503    /// # Returns
1504    ///
1505    /// * `RemovalStats` - Statistics about the removal operation
1506    #[instrument(level = "debug", skip(self))]
1507    pub fn cleanup_with_policy(
1508        &self,
1509        policy: CleanupPolicy,
1510    ) -> BoxFuture<'_, Result<RemovalStats>> {
1511        async move { self.cleanup(policy).execute().await }.boxed()
1512    }
1513
1514    /// Creates a cleanup operation for this dataset.
1515    ///
1516    /// The returned operation can be explained without deleting files, or
1517    /// executed to re-evaluate the current dataset state and remove files.
1518    pub fn cleanup(&self, policy: CleanupPolicy) -> CleanupOperation<'_> {
1519        CleanupOperation::new(self, policy)
1520    }
1521
1522    #[allow(clippy::too_many_arguments)]
1523    async fn do_commit(
1524        base_uri: WriteDestination<'_>,
1525        operation: Operation,
1526        read_version: Option<u64>,
1527        store_params: Option<ObjectStoreParams>,
1528        commit_handler: Option<Arc<dyn CommitHandler>>,
1529        session: Arc<Session>,
1530        enable_v2_manifest_paths: bool,
1531        detached: bool,
1532    ) -> Result<Self> {
1533        let read_version = read_version.map_or_else(
1534            || match operation {
1535                Operation::Overwrite { .. } | Operation::Restore { .. } => Ok(0),
1536                _ => Err(Error::invalid_input(
1537                    "read_version must be specified for this operation",
1538                )),
1539            },
1540            Ok,
1541        )?;
1542
1543        let transaction = Transaction::new(read_version, operation, None);
1544
1545        let mut builder = CommitBuilder::new(base_uri)
1546            .enable_v2_manifest_paths(enable_v2_manifest_paths)
1547            .with_session(session)
1548            .with_detached(detached);
1549
1550        if let Some(store_params) = store_params {
1551            builder = builder.with_store_params(store_params);
1552        }
1553
1554        if let Some(commit_handler) = commit_handler {
1555            builder = builder.with_commit_handler(commit_handler);
1556        }
1557
1558        builder.execute(transaction).await
1559    }
1560
1561    /// Commit changes to the dataset
1562    ///
1563    /// This operation is not needed if you are using append/write/delete to manipulate the dataset.
1564    /// It is used to commit changes to the dataset that are made externally.  For example, a bulk
1565    /// import tool may import large amounts of new data and write the appropriate lance files
1566    /// directly instead of using the write function.
1567    ///
1568    /// This method can be used to commit this change to the dataset's manifest.  This method will
1569    /// not verify that the provided fragments exist and correct, that is the caller's responsibility.
1570    /// Some validation can be performed using the function
1571    /// [crate::dataset::transaction::validate_operation].
1572    ///
1573    /// If this commit is a change to an existing dataset then it will often need to be based on an
1574    /// existing version of the dataset.  For example, if this change is a `delete` operation then
1575    /// the caller will have read in the existing data (at some version) to determine which fragments
1576    /// need to be deleted.  The base version that the caller used should be supplied as the `read_version`
1577    /// parameter.  Some operations (e.g. Overwrite) do not depend on a previous version and `read_version`
1578    /// can be None.  An error will be returned if the `read_version` is needed for an operation and
1579    /// it is not specified.
1580    ///
1581    /// All operations except Overwrite will fail if the dataset does not already exist.
1582    ///
1583    /// # Arguments
1584    ///
1585    /// * `base_uri` - The base URI of the dataset
1586    /// * `operation` - A description of the change to commit
1587    /// * `read_version` - The version of the dataset that this change is based on
1588    /// * `store_params` Parameters controlling object store access to the manifest
1589    /// * `enable_v2_manifest_paths`: If set to true, and this is a new dataset, uses the new v2 manifest
1590    ///   paths. These allow constant-time lookups for the latest manifest on object storage.
1591    ///   This parameter has no effect on existing datasets. To migrate an existing
1592    ///   dataset, use the [`Self::migrate_manifest_paths_v2`] method. WARNING: turning
1593    ///   this on will make the dataset unreadable for older versions of Lance
1594    ///   (prior to 0.17.0). Default is False.
1595    pub async fn commit(
1596        dest: impl Into<WriteDestination<'_>>,
1597        operation: Operation,
1598        read_version: Option<u64>,
1599        store_params: Option<ObjectStoreParams>,
1600        commit_handler: Option<Arc<dyn CommitHandler>>,
1601        session: Arc<Session>,
1602        enable_v2_manifest_paths: bool,
1603    ) -> Result<Self> {
1604        Self::do_commit(
1605            dest.into(),
1606            operation,
1607            read_version,
1608            store_params,
1609            commit_handler,
1610            session,
1611            enable_v2_manifest_paths,
1612            /*detached=*/ false,
1613        )
1614        .await
1615    }
1616
1617    /// Commits changes exactly the same as [`Self::commit`] but the commit will
1618    /// not be associated with the dataset lineage.
1619    ///
1620    /// The commit will not show up in the dataset's history and will never be
1621    /// the latest version of the dataset.
1622    ///
1623    /// This can be used to stage changes or to handle "secondary" datasets whose
1624    /// lineage is tracked elsewhere.
1625    pub async fn commit_detached(
1626        dest: impl Into<WriteDestination<'_>>,
1627        operation: Operation,
1628        read_version: Option<u64>,
1629        store_params: Option<ObjectStoreParams>,
1630        commit_handler: Option<Arc<dyn CommitHandler>>,
1631        session: Arc<Session>,
1632        enable_v2_manifest_paths: bool,
1633    ) -> Result<Self> {
1634        Self::do_commit(
1635            dest.into(),
1636            operation,
1637            read_version,
1638            store_params,
1639            commit_handler,
1640            session,
1641            enable_v2_manifest_paths,
1642            /*detached=*/ true,
1643        )
1644        .await
1645    }
1646
1647    pub(crate) async fn apply_commit(
1648        &mut self,
1649        transaction: Transaction,
1650        write_config: &ManifestWriteConfig,
1651        commit_config: &CommitConfig,
1652    ) -> Result<()> {
1653        let (manifest, manifest_location) = commit_transaction(
1654            self,
1655            self.object_store.as_ref(),
1656            self.commit_handler.as_ref(),
1657            &transaction,
1658            write_config,
1659            commit_config,
1660            DEFAULT_COMMIT_RETRY_TIMEOUT,
1661            self.manifest_location.naming_scheme,
1662            None,
1663        )
1664        .await?;
1665
1666        self.set_manifest(Arc::new(manifest), manifest_location);
1667
1668        Ok(())
1669    }
1670
1671    /// Create a Scanner to scan the dataset.
1672    pub fn scan(&self) -> Scanner {
1673        Scanner::new(Arc::new(self.clone()))
1674    }
1675
1676    /// Count the number of rows in the dataset.
1677    ///
1678    /// It offers a fast path of counting rows by just computing via metadata.
1679    #[instrument(skip_all)]
1680    pub async fn count_rows(&self, filter: Option<String>) -> Result<usize> {
1681        // TODO: consolidate the count_rows into Scanner plan.
1682        if let Some(filter) = filter {
1683            let mut scanner = self.scan();
1684            scanner.filter(&filter)?;
1685            Ok(scanner
1686                .project::<String>(&[])?
1687                .with_row_id() // TODO: fix scan plan to not require row_id for count_rows.
1688                .count_rows()
1689                .await? as usize)
1690        } else {
1691            self.count_all_rows().await
1692        }
1693    }
1694
1695    pub(crate) async fn count_all_rows(&self) -> Result<usize> {
1696        let cnts = stream::iter(self.get_fragments())
1697            .map(|f| async move { f.count_rows(None).await })
1698            .buffer_unordered(16)
1699            .try_collect::<Vec<_>>()
1700            .await?;
1701        Ok(cnts.iter().sum())
1702    }
1703
1704    /// Take rows by indices.
1705    #[instrument(skip_all, fields(num_rows=row_indices.len()))]
1706    pub async fn take(
1707        &self,
1708        row_indices: &[u64],
1709        projection: impl Into<ProjectionRequest>,
1710    ) -> Result<RecordBatch> {
1711        take::take(self, row_indices, projection.into()).await
1712    }
1713
1714    /// Take Rows by the internal ROW ids.
1715    ///
1716    /// In Lance format, each row has a unique `u64` id, which is used to identify the row globally.
1717    ///
1718    /// ```rust
1719    /// # use std::sync::Arc;
1720    /// # use tokio::runtime::Runtime;
1721    /// # use arrow_array::{RecordBatch, RecordBatchIterator, Int64Array};
1722    /// # use arrow_schema::{Schema, Field, DataType};
1723    /// # use lance::dataset::{WriteParams, Dataset, ProjectionRequest};
1724    /// #
1725    /// # let mut rt = Runtime::new().unwrap();
1726    /// # rt.block_on(async {
1727    /// # let test_dir = tempfile::tempdir().unwrap();
1728    /// # let uri = test_dir.path().to_str().unwrap().to_string();
1729    /// #
1730    /// # let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)]));
1731    /// # let write_params = WriteParams::default();
1732    /// # let array = Arc::new(Int64Array::from_iter(0..128));
1733    /// # let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
1734    /// # let reader = RecordBatchIterator::new(
1735    /// #    vec![batch].into_iter().map(Ok), schema
1736    /// # );
1737    /// # let dataset = Dataset::write(reader, &uri, Some(write_params)).await.unwrap();
1738    /// #
1739    /// let schema = dataset.schema().clone();
1740    /// let row_ids = vec![0, 4, 7];
1741    /// let rows = dataset.take_rows(&row_ids, schema).await.unwrap();
1742    ///
1743    /// // We can have more fine-grained control over the projection, i.e., SQL projection.
1744    /// let projection = ProjectionRequest::from_sql([("identity", "id * 2")]);
1745    /// let rows = dataset.take_rows(&row_ids, projection).await.unwrap();
1746    /// # });
1747    /// ```
1748    pub async fn take_rows(
1749        &self,
1750        row_ids: &[u64],
1751        projection: impl Into<ProjectionRequest>,
1752    ) -> Result<RecordBatch> {
1753        Arc::new(self.clone())
1754            .take_builder(row_ids, projection)?
1755            .execute()
1756            .await
1757    }
1758
1759    pub fn take_builder(
1760        self: &Arc<Self>,
1761        row_ids: &[u64],
1762        projection: impl Into<ProjectionRequest>,
1763    ) -> Result<TakeBuilder> {
1764        TakeBuilder::try_new_from_ids(self.clone(), row_ids.to_vec(), projection.into())
1765    }
1766
1767    /// Take [BlobFile] by row IDs.
1768    ///
1769    /// The returned vector has one element per row ID. Null blob values are
1770    /// represented as `None`; valid empty blobs return a `BlobFile` with size
1771    /// zero.
1772    ///
1773    /// ```
1774    /// # use std::sync::Arc;
1775    /// # use lance::dataset::Dataset;
1776    /// # use lance::Result;
1777    /// # async fn example(dataset: Arc<Dataset>) -> Result<()> {
1778    /// let blobs = dataset.take_blobs(&[42], "images").await?;
1779    /// match &blobs[0] {
1780    ///     None => { /* The selected blob is null. */ }
1781    ///     Some(blob) if blob.size() == 0 => { /* The selected blob is valid but empty. */ }
1782    ///     Some(blob) => { let _size = blob.size(); }
1783    /// }
1784    /// # Ok(())
1785    /// # }
1786    /// ```
1787    pub async fn take_blobs(
1788        self: &Arc<Self>,
1789        row_ids: &[u64],
1790        column: impl AsRef<str>,
1791    ) -> Result<Vec<Option<BlobFile>>> {
1792        blob::take_blobs(self, row_ids, column.as_ref()).await
1793    }
1794
1795    /// Take [BlobFile] by row addresses.
1796    ///
1797    /// Row addresses are `u64` values encoding `(fragment_id << 32) | row_offset`.
1798    /// Use this method when you already have row addresses, for example from
1799    /// a scan with `with_row_address()`. For row IDs (stable identifiers), use
1800    /// [`Self::take_blobs`]. For row indices (offsets), use
1801    /// [`Self::take_blobs_by_indices`]. The result has the same null and empty
1802    /// blob representation as [`Self::take_blobs`].
1803    ///
1804    /// ```
1805    /// # use std::sync::Arc;
1806    /// # use lance::dataset::Dataset;
1807    /// # use lance::Result;
1808    /// # async fn example(dataset: Arc<Dataset>, row_address: u64) -> Result<()> {
1809    /// let blobs = dataset
1810    ///     .take_blobs_by_addresses(&[row_address], "images")
1811    ///     .await?;
1812    /// match &blobs[0] {
1813    ///     None => { /* The selected blob is null. */ }
1814    ///     Some(blob) if blob.size() == 0 => { /* The selected blob is valid but empty. */ }
1815    ///     Some(blob) => { let _size = blob.size(); }
1816    /// }
1817    /// # Ok(())
1818    /// # }
1819    /// ```
1820    pub async fn take_blobs_by_addresses(
1821        self: &Arc<Self>,
1822        row_addrs: &[u64],
1823        column: impl AsRef<str>,
1824    ) -> Result<Vec<Option<BlobFile>>> {
1825        blob::take_blobs_by_addresses(self, row_addrs, column.as_ref()).await
1826    }
1827
1828    /// Take [BlobFile] by row indices (offsets in the dataset).
1829    ///
1830    /// The result has the same null and empty blob representation as
1831    /// [`Self::take_blobs`].
1832    ///
1833    /// ```
1834    /// # use std::sync::Arc;
1835    /// # use lance::dataset::Dataset;
1836    /// # use lance::Result;
1837    /// # async fn example(dataset: Arc<Dataset>) -> Result<()> {
1838    /// let blobs = dataset.take_blobs_by_indices(&[0], "images").await?;
1839    /// match &blobs[0] {
1840    ///     None => { /* The selected blob is null. */ }
1841    ///     Some(blob) if blob.size() == 0 => { /* The selected blob is valid but empty. */ }
1842    ///     Some(blob) => { let _size = blob.size(); }
1843    /// }
1844    /// # Ok(())
1845    /// # }
1846    /// ```
1847    pub async fn take_blobs_by_indices(
1848        self: &Arc<Self>,
1849        row_indices: &[u64],
1850        column: impl AsRef<str>,
1851    ) -> Result<Vec<Option<BlobFile>>> {
1852        let fragments = self.get_fragments();
1853        let row_addrs = row_offsets_to_row_addresses(&fragments, row_indices).await?;
1854        blob::take_blobs_by_addresses(self, &row_addrs, column.as_ref()).await
1855    }
1856
1857    /// Create a planned blob reader for a blob column.
1858    ///
1859    /// This API complements [`Self::take_blobs`]. `take_blobs` returns
1860    /// [`BlobFile`] handles for caller-driven random access, while
1861    /// `read_blobs` builds a streaming read plan for sequential or batched blob
1862    /// retrieval. Every selected row produces one result: null blob values have
1863    /// `ReadBlob::data` set to `None`, while valid empty blobs contain an empty
1864    /// buffer.
1865    ///
1866    /// ```rust
1867    /// # use std::sync::Arc;
1868    /// # use futures::TryStreamExt;
1869    /// # use lance::dataset::Dataset;
1870    /// # use lance::Result;
1871    /// # async fn example(dataset: Arc<Dataset>) -> Result<()> {
1872    /// let blobs = dataset
1873    ///     .read_blobs("images")?
1874    ///     .with_row_indices(vec![0, 1, 2])
1875    ///     .execute()
1876    ///     .await?;
1877    /// # let _ = blobs;
1878    /// # Ok(())
1879    /// # }
1880    /// ```
1881    pub fn read_blobs(self: &Arc<Self>, column: impl AsRef<str>) -> Result<ReadBlobsBuilder> {
1882        let column = column.as_ref();
1883        let blob_field_id = blob::validate_blob_column(self, column)?;
1884        Ok(ReadBlobsBuilder::new(
1885            self.clone(),
1886            column.to_string(),
1887            blob_field_id,
1888        ))
1889    }
1890
1891    /// Create a planned reader for row-specific blob-local byte ranges.
1892    ///
1893    /// Each [`BlobRangeRequest`] contains both its row selector and byte range,
1894    /// so requests can be repeated or reordered without coordinating parallel
1895    /// selector and range lists. Every request produces one result. A null blob
1896    /// has `ReadBlobRange::data` set to `None`; an empty range on a non-null blob
1897    /// contains an empty buffer.
1898    ///
1899    /// ```rust
1900    /// # use std::sync::Arc;
1901    /// # use lance::dataset::{BlobRangeRequest, Dataset};
1902    /// # use lance::Result;
1903    /// # async fn example(dataset: Arc<Dataset>) -> Result<()> {
1904    /// let ranges = dataset
1905    ///     .read_blob_ranges("images")?
1906    ///     .with_row_indices([
1907    ///         BlobRangeRequest::new(7, 0, 1024),
1908    ///         BlobRangeRequest::new(7, 4096, 1024),
1909    ///     ])
1910    ///     .execute()
1911    ///     .await?;
1912    /// # let _ = ranges;
1913    /// # Ok(())
1914    /// # }
1915    /// ```
1916    pub fn read_blob_ranges(
1917        self: &Arc<Self>,
1918        column: impl AsRef<str>,
1919    ) -> Result<ReadBlobRangesBuilder> {
1920        Ok(ReadBlobRangesBuilder::new(self.read_blobs(column)?))
1921    }
1922
1923    /// Get a stream of batches based on iterator of ranges of row numbers.
1924    ///
1925    /// This is an experimental API. It may change at any time.
1926    pub fn take_scan(
1927        &self,
1928        row_ranges: Pin<Box<dyn Stream<Item = Result<Range<u64>>> + Send>>,
1929        projection: Arc<Schema>,
1930        batch_readahead: usize,
1931    ) -> DatasetRecordBatchStream {
1932        take::take_scan(self, row_ranges, projection, batch_readahead)
1933    }
1934
1935    /// Randomly sample `n` rows from the dataset.
1936    ///
1937    /// If `fragment_ids` is provided, sampling is limited to rows from those
1938    /// fragments in the current dataset version.
1939    ///
1940    /// The returned rows are in row-id order (not random order), which allows
1941    /// the underlying take operation to use an efficient sorted code path.
1942    pub async fn sample(
1943        &self,
1944        n: usize,
1945        projection: &Schema,
1946        fragment_ids: Option<&[u32]>,
1947    ) -> Result<RecordBatch> {
1948        use rand::seq::IteratorRandom;
1949
1950        match fragment_ids {
1951            None => {
1952                let num_rows = self.count_rows(None).await?;
1953                let mut ids = (0..num_rows as u64).choose_multiple(&mut rand::rng(), n);
1954                ids.sort_unstable();
1955                self.take(&ids, projection.clone()).await
1956            }
1957            Some(fragment_ids) => {
1958                if fragment_ids.is_empty() {
1959                    return Err(Error::invalid_input(
1960                        "Dataset::sample does not accept an empty fragment_ids list".to_string(),
1961                    ));
1962                }
1963
1964                let selected_fragments = self.get_fragments_from_ids(fragment_ids)?;
1965
1966                let num_rows = stream::iter(selected_fragments.iter().cloned())
1967                    .map(|fragment| async move { fragment.count_rows(None).await })
1968                    .buffer_unordered(16)
1969                    .try_fold(0_u64, |acc, rows| async move { Ok(acc + rows as u64) })
1970                    .await?;
1971
1972                let mut offsets = (0..num_rows).choose_multiple(&mut rand::rng(), n);
1973                offsets.sort_unstable();
1974
1975                let row_addrs = row_offsets_to_row_addresses(&selected_fragments, &offsets).await?;
1976                let dataset = Arc::new(self.clone());
1977                let projection = Arc::new(
1978                    ProjectionRequest::from(projection.clone())
1979                        .into_projection_plan(dataset.clone())?,
1980                );
1981                TakeBuilder::try_new_from_addresses(dataset, row_addrs, projection)?
1982                    .execute()
1983                    .await
1984            }
1985        }
1986    }
1987
1988    /// Delete rows based on a predicate.
1989    pub async fn delete(&mut self, predicate: &str) -> Result<write::delete::DeleteResult> {
1990        info!(target: TRACE_DATASET_EVENTS, event=DATASET_DELETING_EVENT, uri = &self.uri, predicate=predicate);
1991        write::delete::delete(self, predicate).await
1992    }
1993
1994    /// Truncate the dataset by deleting all rows.
1995    pub async fn truncate_table(&mut self) -> Result<()> {
1996        self.delete("true").await.map(|_| ())
1997    }
1998
1999    /// Add new base paths to the dataset.
2000    ///
2001    /// This method allows you to register additional storage locations (buckets)
2002    /// that can be used for future data writes. The base paths are added to the
2003    /// dataset's manifest and can be referenced by name in subsequent write operations.
2004    ///
2005    /// # Arguments
2006    ///
2007    /// * `new_bases` - A vector of `lance_table::format::BasePath` objects representing the new storage
2008    ///   locations to add. Each base path should have a unique name and path.
2009    ///
2010    /// # Returns
2011    ///
2012    /// Returns a new `Dataset` instance with the updated manifest containing the
2013    /// new base paths.
2014    pub async fn add_bases(
2015        self: &Arc<Self>,
2016        new_bases: Vec<lance_table::format::BasePath>,
2017        transaction_properties: Option<HashMap<String, String>>,
2018    ) -> Result<Self> {
2019        let operation = Operation::UpdateBases { new_bases };
2020
2021        let transaction = TransactionBuilder::new(self.manifest.version, operation)
2022            .transaction_properties(transaction_properties.map(Arc::new))
2023            .build();
2024
2025        let new_dataset = CommitBuilder::new(self.clone())
2026            .execute(transaction)
2027            .await?;
2028
2029        Ok(new_dataset)
2030    }
2031
2032    pub async fn count_deleted_rows(&self) -> Result<usize> {
2033        futures::stream::iter(self.get_fragments())
2034            .map(|f| async move { f.count_deletions().await })
2035            .buffer_unordered(self.object_store.io_parallelism())
2036            .try_fold(0, |acc, x| futures::future::ready(Ok(acc + x)))
2037            .await
2038    }
2039
2040    /// Clone this dataset with a different object store binding.
2041    ///
2042    /// The returned dataset shares metadata, session state, and caches with the
2043    /// original dataset, but all subsequent operations on the returned dataset
2044    /// use the supplied object store.
2045    pub fn with_object_store(
2046        &self,
2047        object_store: Arc<ObjectStore>,
2048        store_params: Option<ObjectStoreParams>,
2049    ) -> Self {
2050        let mut cloned = self.clone();
2051        cloned.object_store = object_store;
2052        cloned.base_object_stores = Default::default();
2053        if let Some(store_params) = store_params {
2054            cloned.store_params = Some(Box::new(store_params));
2055        }
2056        cloned
2057    }
2058
2059    /// Clone this dataset with extra object store wrappers applied to all read stores.
2060    ///
2061    /// The returned dataset uses the wrappers for the already-open primary object
2062    /// store and appends the same wrappers to the dataset-level and base-specific
2063    /// object store params used when additional base stores are opened later.
2064    pub fn with_object_store_wrappers(
2065        &self,
2066        wrappers: impl IntoIterator<Item = Arc<dyn WrappingObjectStore>>,
2067    ) -> Self {
2068        let wrappers = wrappers.into_iter().collect::<Vec<_>>();
2069        if wrappers.is_empty() {
2070            return self.clone();
2071        }
2072
2073        let mut cloned = self.clone();
2074        // Each wrapper application defines a new store lifetime. Keep base
2075        // stores alive within the derived dataset without sharing stateful
2076        // provider layers (such as an AIMD throttle) with other scopes.
2077        cloned.base_object_stores = Default::default();
2078        let mut object_store = self.object_store.as_ref().clone();
2079        for wrapper in &wrappers {
2080            object_store.inner =
2081                wrapper.wrap(&object_store.store_prefix, object_store.inner.clone());
2082        }
2083        cloned.object_store = Arc::new(object_store);
2084        cloned.refs = Refs::new(
2085            cloned.object_store.clone(),
2086            cloned.commit_handler.clone(),
2087            cloned.branch_location(),
2088        );
2089
2090        let store_params = self.store_params.as_deref().cloned().unwrap_or_default();
2091        cloned.store_params = Some(Box::new(Self::append_object_store_wrappers(
2092            store_params,
2093            &wrappers,
2094        )));
2095        cloned.base_store_params = self.base_store_params.as_ref().map(|base_store_params| {
2096            Arc::new(
2097                base_store_params
2098                    .iter()
2099                    .map(|(base_path, store_params)| {
2100                        (
2101                            base_path.clone(),
2102                            Self::append_object_store_wrappers(store_params.clone(), &wrappers),
2103                        )
2104                    })
2105                    .collect(),
2106            )
2107        });
2108        cloned
2109    }
2110
2111    fn append_object_store_wrappers(
2112        mut store_params: ObjectStoreParams,
2113        wrappers: &[Arc<dyn WrappingObjectStore>],
2114    ) -> ObjectStoreParams {
2115        let mut all_wrappers = Vec::with_capacity(
2116            store_params.object_store_wrapper.as_ref().map_or(0, |_| 1) + wrappers.len(),
2117        );
2118        if let Some(wrapper) = store_params.object_store_wrapper.take() {
2119            all_wrappers.push(wrapper);
2120        }
2121        all_wrappers.extend(wrappers.iter().cloned());
2122        store_params.object_store_wrapper = match all_wrappers.len() {
2123            0 => None,
2124            1 => all_wrappers.pop(),
2125            _ => Some(Arc::new(ChainedWrappingObjectStore::new(all_wrappers))),
2126        };
2127        store_params
2128    }
2129
2130    pub(crate) fn store_params_for_base(
2131        &self,
2132        base_path: Option<&lance_table::format::BasePath>,
2133    ) -> ObjectStoreParams {
2134        // Base-specific bindings are exact ObjectStoreParams keyed by
2135        // `BasePath.path` and are used as-is. Otherwise the dataset-level
2136        // default params are resolved for the base scope: `base_<id>.<key>`
2137        // storage options overlay the shared defaults for that base.
2138        if let Some(params) = base_path.and_then(|base_path| {
2139            self.base_store_params
2140                .as_ref()
2141                .and_then(|params| params.get(&base_path.path))
2142        }) {
2143            return params.clone();
2144        }
2145        let default_params = self.store_params.as_deref().cloned().unwrap_or_default();
2146        match default_params.scoped_to_base(base_path.map(|base_path| base_path.id)) {
2147            Cow::Owned(scoped_params) => scoped_params,
2148            Cow::Borrowed(_) => default_params,
2149        }
2150    }
2151
2152    /// Returns the initial storage options used when opening this dataset, if any.
2153    ///
2154    /// This returns the static initial options without triggering any refresh.
2155    /// For the latest refreshed options, use [`Self::latest_storage_options`].
2156    #[deprecated(since = "0.25.0", note = "Use initial_storage_options() instead")]
2157    pub fn storage_options(&self) -> Option<&HashMap<String, String>> {
2158        self.initial_storage_options()
2159    }
2160
2161    /// Returns the initial storage options without triggering any refresh.
2162    ///
2163    /// For the latest refreshed options, use [`Self::latest_storage_options`].
2164    pub fn initial_storage_options(&self) -> Option<&HashMap<String, String>> {
2165        self.store_params
2166            .as_ref()
2167            .and_then(|params| params.storage_options())
2168    }
2169
2170    /// Returns the storage options provider used when opening this dataset, if any.
2171    pub fn storage_options_provider(
2172        &self,
2173    ) -> Option<Arc<dyn lance_io::object_store::StorageOptionsProvider>> {
2174        self.store_params
2175            .as_ref()
2176            .and_then(|params| params.storage_options_accessor.as_ref())
2177            .and_then(|accessor| accessor.provider().cloned())
2178    }
2179
2180    /// Returns the unified storage options accessor for this dataset, if any.
2181    ///
2182    /// The accessor handles both static and dynamic storage options with automatic
2183    /// caching and refresh. Use [`StorageOptionsAccessor::get_storage_options`] to
2184    /// get the latest options.
2185    pub fn storage_options_accessor(&self) -> Option<Arc<StorageOptionsAccessor>> {
2186        self.store_params
2187            .as_ref()
2188            .and_then(|params| params.get_accessor())
2189    }
2190
2191    /// Returns the latest (possibly refreshed) storage options.
2192    ///
2193    /// If a dynamic storage options provider is configured, this will return
2194    /// the cached options if still valid, or fetch fresh options if expired.
2195    ///
2196    /// For the initial static options without refresh, use [`Self::storage_options`].
2197    ///
2198    /// # Returns
2199    ///
2200    /// - `Ok(Some(options))` - Storage options are available (static or refreshed)
2201    /// - `Ok(None)` - No storage options were configured for this dataset
2202    /// - `Err(...)` - Error occurred while fetching/refreshing options from provider
2203    pub async fn latest_storage_options(&self) -> Result<Option<StorageOptions>> {
2204        // First check if we have an accessor (handles both static and dynamic options)
2205        if let Some(accessor) = self.storage_options_accessor() {
2206            let options = accessor.get_storage_options().await?;
2207            return Ok(Some(options));
2208        }
2209
2210        // Fallback to initial storage options if no accessor
2211        Ok(self.initial_storage_options().cloned().map(StorageOptions))
2212    }
2213
2214    pub fn data_dir(&self) -> Path {
2215        self.base.clone().join(DATA_DIR)
2216    }
2217
2218    pub fn indices_dir(&self) -> Path {
2219        self.base.clone().join(INDICES_DIR)
2220    }
2221
2222    pub fn transactions_dir(&self) -> Path {
2223        self.base.clone().join(TRANSACTIONS_DIR)
2224    }
2225
2226    pub fn deletions_dir(&self) -> Path {
2227        self.base.clone().join(DELETIONS_DIR)
2228    }
2229
2230    pub fn versions_dir(&self) -> Path {
2231        self.base.clone().join(VERSIONS_DIR)
2232    }
2233
2234    pub(crate) fn data_file_dir(&self, data_file: &DataFile) -> Result<Path> {
2235        self.data_file_dir_for_base(data_file.base_id)
2236    }
2237
2238    /// Create a [`DataFile`] by reading metadata from an existing lance file.
2239    ///
2240    /// This reads the file's schema and version information, matches columns to
2241    /// the dataset's schema to determine field IDs, and calculates column indices.
2242    /// This is useful for constructing `DataFile` metadata needed for operations
2243    /// like [`Operation::DataReplacement`].
2244    ///
2245    /// # Arguments
2246    ///
2247    /// * `path` - The path to the data file, relative to the dataset's data directory.
2248    /// * `base_id` - The base path ID if the file is outside the dataset directory.
2249    pub async fn create_data_file(&self, path: &str, base_id: Option<u32>) -> Result<DataFile> {
2250        let data_dir = self.data_file_dir_for_base(base_id)?;
2251        let filepath = data_dir.clone().join(path);
2252
2253        let object_store = self.object_store(base_id).await?;
2254
2255        // Get file size
2256        let file_size = object_store.size(&filepath).await?;
2257
2258        // Read file metadata
2259        let scheduler = ScanScheduler::new(
2260            object_store.clone(),
2261            SchedulerConfig::new(2 * 1024 * 1024 * 1024),
2262        );
2263        let file = scheduler
2264            .open_file(&filepath, &CachedFileSize::new(file_size))
2265            .await?;
2266        let file_metadata = FileReader::read_all_metadata(&file).await?;
2267
2268        let lance_file_format = file_metadata.version;
2269        let physical_columns = file_metadata.column_metadatas.len();
2270        let has_footer_orphans = file_metadata.file_schema.fields.len() > physical_columns;
2271        let dataset_schema = self.schema();
2272        let mut represented_columns = 0usize;
2273        let mut column_names = Vec::new();
2274        let mut consumed_top_level_fields = 0usize;
2275
2276        fn field_contains_blob(field: &lance_core::datatypes::Field) -> bool {
2277            field.is_blob() || field.children.iter().any(field_contains_blob)
2278        }
2279
2280        fn field_names_match(
2281            fields: &[lance_core::datatypes::Field],
2282            start: usize,
2283            expected: &arrow_schema::Fields,
2284        ) -> bool {
2285            fields
2286                .get(start..start + expected.len())
2287                .is_some_and(|candidate| {
2288                    candidate
2289                        .iter()
2290                        .zip(expected.iter())
2291                        .all(|(field, expected)| field.name == expected.name().as_str())
2292                })
2293        }
2294
2295        fn blob_descriptor_orphan_len(
2296            fields: &[lance_core::datatypes::Field],
2297            start: usize,
2298        ) -> usize {
2299            if field_names_match(fields, start, &lance_core::datatypes::BLOB_V2_DESC_FIELDS) {
2300                lance_core::datatypes::BLOB_V2_DESC_FIELDS.len()
2301            } else if field_names_match(fields, start, &lance_core::datatypes::BLOB_DESC_FIELDS) {
2302                lance_core::datatypes::BLOB_DESC_FIELDS.len()
2303            } else {
2304                0
2305            }
2306        }
2307
2308        fn validate_file_field_matches_dataset(
2309            dataset_field: &lance_core::datatypes::Field,
2310            file_field: &lance_core::datatypes::Field,
2311            path: &str,
2312        ) -> Result<()> {
2313            if dataset_field.name != file_field.name {
2314                return Err(Error::invalid_input(format!(
2315                    "Schema mismatch: expected field '{}' but file has '{}'",
2316                    path, file_field.name
2317                )));
2318            }
2319
2320            if dataset_field.is_blob() && file_field.is_blob() {
2321                return Ok(());
2322            }
2323
2324            if dataset_field.children.len() != file_field.children.len() {
2325                return Err(Error::invalid_input(format!(
2326                    "Schema mismatch: field '{}' has {} children in dataset schema but {} children in file schema",
2327                    path,
2328                    dataset_field.children.len(),
2329                    file_field.children.len()
2330                )));
2331            }
2332
2333            for (dataset_child, file_child) in
2334                dataset_field.children.iter().zip(&file_field.children)
2335            {
2336                let child_path = format!("{}.{}", path, dataset_child.name);
2337                validate_file_field_matches_dataset(dataset_child, file_child, &child_path)?;
2338            }
2339
2340            Ok(())
2341        }
2342
2343        let file_schema_fields = &file_metadata.file_schema.fields;
2344        let mut idx = 0usize;
2345        while represented_columns < physical_columns {
2346            let Some(field) = file_schema_fields.get(idx) else {
2347                return Err(Error::invalid_input(format!(
2348                    "Schema mismatch: file schema ended after representing {} physical columns but file has {} columns",
2349                    represented_columns, physical_columns
2350                )));
2351            };
2352
2353            let Some(dataset_field) = dataset_schema.field(&field.name) else {
2354                return Err(Error::invalid_input(format!(
2355                    "Schema mismatch: file has extra field '{}'",
2356                    field.name
2357                )));
2358            };
2359            validate_file_field_matches_dataset(dataset_field, field, &field.name)?;
2360
2361            represented_columns += file_versions::physical_column_count(lance_file_format, field);
2362            column_names.push(field.name.as_str());
2363            consumed_top_level_fields = idx + 1;
2364            idx += 1;
2365
2366            if has_footer_orphans && field_contains_blob(field) {
2367                loop {
2368                    let skipped = blob_descriptor_orphan_len(file_schema_fields, idx);
2369                    if skipped == 0 {
2370                        break;
2371                    }
2372                    consumed_top_level_fields = idx + skipped;
2373                    idx += skipped;
2374                }
2375            }
2376        }
2377
2378        if represented_columns != physical_columns {
2379            return Err(Error::invalid_input(format!(
2380                "Schema mismatch: file schema represents {} physical columns but file has {} columns",
2381                represented_columns, physical_columns
2382            )));
2383        }
2384
2385        if let Some(field) = file_schema_fields.get(consumed_top_level_fields) {
2386            return Err(Error::invalid_input(format!(
2387                "Schema mismatch: file has extra field '{}'",
2388                field.name
2389            )));
2390        }
2391
2392        let projected_ds_schema = self.schema().project(&column_names)?;
2393
2394        let (fields, column_indices) =
2395            file_versions::data_file_columns(lance_file_format, &projected_ds_schema);
2396        let represented_dataset_columns = column_indices
2397            .iter()
2398            .filter(|column_index| **column_index >= 0)
2399            .count();
2400
2401        if represented_dataset_columns != physical_columns {
2402            return Err(Error::invalid_input(format!(
2403                "Schema mismatch: dataset projection maps to {} physical columns but file has {} columns",
2404                represented_dataset_columns, physical_columns
2405            )));
2406        }
2407
2408        if fields.is_empty() && physical_columns > 0 {
2409            return Err(Error::invalid_input(
2410                "Schema mismatch: file has columns but none matched the dataset schema",
2411            ));
2412        }
2413
2414        let file_size_nz = NonZero::new(file_size);
2415        Ok(DataFile::new(
2416            path,
2417            fields,
2418            column_indices,
2419            lance_file_format,
2420            file_size_nz,
2421            base_id,
2422        ))
2423    }
2424
2425    /// Resolve the data directory for a given base_id.
2426    ///
2427    /// If `base_id` is `None`, returns the default data directory.
2428    pub(crate) fn data_file_dir_for_base(&self, base_id: Option<u32>) -> Result<Path> {
2429        match base_id {
2430            Some(base_id) => {
2431                let base_path = self.manifest.base_paths.get(&base_id).ok_or_else(|| {
2432                    Error::invalid_input(format!("base_path id {} not found", base_id))
2433                })?;
2434                let path = base_path.extract_path(self.session.store_registry())?;
2435                if base_path.is_dataset_root {
2436                    Ok(path.join(DATA_DIR))
2437                } else {
2438                    Ok(path)
2439                }
2440            }
2441            None => Ok(self.base.clone().join(DATA_DIR)),
2442        }
2443    }
2444
2445    async fn base_object_store(&self, base_id: u32) -> Result<Arc<ObjectStore>> {
2446        let base_path = self.manifest.base_paths.get(&base_id).ok_or_else(|| {
2447            Error::invalid_input(format!("Dataset base path with ID {} not found", base_id))
2448        })?;
2449        let store_params = self.store_params_for_base(Some(base_path));
2450
2451        let cell = {
2452            let mut stores = self.base_object_stores.lock().unwrap();
2453            stores.entry(base_id).or_default().clone()
2454        };
2455        let store = cell
2456            .get_or_try_init(|| async {
2457                // Wrappers define a request or execution scope. Keep the
2458                // fully resolved store in this dataset's OnceCell, but do not
2459                // also put it in the global registry: provider-local state
2460                // such as GCS AIMD token buckets must not cross that scope.
2461                let (store, _) = if store_params.object_store_wrapper.is_some() {
2462                    ObjectStore::from_uri_and_params_uncached(
2463                        self.session.store_registry(),
2464                        &base_path.path,
2465                        &store_params,
2466                    )
2467                    .await?
2468                } else {
2469                    ObjectStore::from_uri_and_params(
2470                        self.session.store_registry(),
2471                        &base_path.path,
2472                        &store_params,
2473                    )
2474                    .await?
2475                };
2476                Ok::<_, Error>(store)
2477            })
2478            .await?;
2479        Ok(store.clone())
2480    }
2481
2482    /// Resolve the object store for the primary dataset or an additional base.
2483    ///
2484    /// Pass `None` to get the primary dataset object store. Pass `Some(base_id)`
2485    /// when resolving a file whose metadata references an additional base.
2486    pub async fn object_store(&self, base_id: Option<u32>) -> Result<Arc<ObjectStore>> {
2487        match base_id {
2488            Some(base_id) => self.base_object_store(base_id).await,
2489            None => Ok(self.object_store.clone()),
2490        }
2491    }
2492
2493    /// The `ObjectStoreParams` this dataset was opened with, or `None` when
2494    /// opened without explicit params. Lets a caller re-open a derived path
2495    /// (e.g. a MemWAL SSTable) with the same store this dataset used.
2496    pub fn store_params(&self) -> Option<&ObjectStoreParams> {
2497        self.store_params.as_deref()
2498    }
2499
2500    pub(crate) async fn object_store_for_data_file(
2501        &self,
2502        data_file: &DataFile,
2503    ) -> Result<Arc<ObjectStore>> {
2504        self.object_store(data_file.base_id).await
2505    }
2506
2507    pub(crate) async fn object_store_for_deletion(
2508        &self,
2509        deletion_file: &DeletionFile,
2510    ) -> Result<Arc<ObjectStore>> {
2511        self.object_store(deletion_file.base_id).await
2512    }
2513
2514    pub(crate) async fn object_store_for_index(
2515        &self,
2516        index: &IndexMetadata,
2517    ) -> Result<Arc<ObjectStore>> {
2518        self.object_store(index.base_id).await
2519    }
2520
2521    pub(crate) fn dataset_dir_for_deletion(&self, deletion_file: &DeletionFile) -> Result<Path> {
2522        match deletion_file.base_id.as_ref() {
2523            Some(base_id) => {
2524                let base_paths = &self.manifest.base_paths;
2525                let base_path = base_paths.get(base_id).ok_or_else(|| {
2526                    Error::invalid_input(format!(
2527                        "base_path id {} not found for deletion_file {:?}",
2528                        base_id, deletion_file
2529                    ))
2530                })?;
2531
2532                if !base_path.is_dataset_root {
2533                    return Err(Error::internal(format!(
2534                        "base_path id {} is not a dataset root for deletion_file {:?}",
2535                        base_id, deletion_file
2536                    )));
2537                }
2538                base_path.extract_path(self.session.store_registry())
2539            }
2540            None => Ok(self.base.clone()),
2541        }
2542    }
2543
2544    /// Get the indices directory for a specific index, considering its base_id
2545    pub(crate) fn indice_files_dir(&self, index: &IndexMetadata) -> Result<Path> {
2546        match index.base_id.as_ref() {
2547            Some(base_id) => {
2548                let base_paths = &self.manifest.base_paths;
2549                let base_path = base_paths.get(base_id).ok_or_else(|| {
2550                    Error::invalid_input(format!(
2551                        "base_path id {} not found for index {}",
2552                        base_id, index.uuid
2553                    ))
2554                })?;
2555                let path = base_path.extract_path(self.session.store_registry())?;
2556                if base_path.is_dataset_root {
2557                    Ok(path.join(INDICES_DIR))
2558                } else {
2559                    // For non-dataset-root base paths, we assume the path already points to the indices directory
2560                    Ok(path)
2561                }
2562            }
2563            None => Ok(self.base.clone().join(INDICES_DIR)),
2564        }
2565    }
2566
2567    pub fn session(&self) -> Arc<Session> {
2568        self.session.clone()
2569    }
2570
2571    /// Get the currently checked-out version id.
2572    ///
2573    /// This is a cheap accessor that reads the id directly from the loaded
2574    /// manifest without constructing the full [Version] summary.
2575    pub fn version_id(&self) -> u64 {
2576        self.manifest.version
2577    }
2578
2579    /// Get the currently checked-out version details.
2580    ///
2581    /// This constructs a full [Version], including summary metadata derived
2582    /// from the loaded manifest fragments.
2583    pub fn version(&self) -> Version {
2584        Version::from(self.manifest.as_ref())
2585    }
2586
2587    /// Get the number of entries currently in the index cache.
2588    pub async fn index_cache_entry_count(&self) -> usize {
2589        self.session.index_cache.size().await
2590    }
2591
2592    /// Get cache hit ratio.
2593    pub async fn index_cache_hit_rate(&self) -> f32 {
2594        let stats = self.session.index_cache_stats().await;
2595        stats.hit_ratio()
2596    }
2597
2598    pub fn cache_size_bytes(&self) -> u64 {
2599        self.session.deep_size_of() as u64
2600    }
2601
2602    /// Get all versions.
2603    pub async fn versions(&self) -> Result<Vec<Version>> {
2604        let mut versions: Vec<Version> = self
2605            .commit_handler
2606            .list_manifest_locations(&self.base, &self.object_store, false)
2607            .try_filter_map(|location| async move {
2608                match read_manifest(&self.object_store, &location.path, location.size).await {
2609                    Ok(manifest) => Ok(Some(Version::from(&manifest))),
2610                    Err(e) => Err(e),
2611                }
2612            })
2613            .try_collect()
2614            .await?;
2615
2616        // TODO: this API should support pagination
2617        versions.sort_by_key(|v| v.version);
2618
2619        Ok(versions)
2620    }
2621
2622    /// Get the number of versions in the current version history.
2623    ///
2624    /// Unlike [`Self::versions`], this only enumerates manifest locations and does not read or
2625    /// deserialize every manifest.
2626    pub async fn count_versions(&self) -> Result<u64> {
2627        self.commit_handler
2628            .list_manifest_locations(&self.base, &self.object_store, false)
2629            .try_fold(0_u64, |count, _| async move { Ok(count + 1) })
2630            .await
2631    }
2632
2633    /// List lightweight references to all attached versions in the current branch's history.
2634    ///
2635    /// Unlike [`Self::versions`], this only enumerates manifest locations and does not read or
2636    /// deserialize every manifest. The references are sorted by version in ascending order.
2637    /// Detached manifests are excluded; see [`Self::list_detached_manifests`].
2638    ///
2639    /// Use [`Self::latest_version_id`] instead when only the latest version is needed.
2640    pub async fn version_refs(&self) -> Result<Vec<VersionRef>> {
2641        let mut versions: Vec<_> = self
2642            .commit_handler
2643            .list_manifest_locations(&self.base, &self.object_store, false)
2644            .map_ok(|location| VersionRef {
2645                version: location.version,
2646            })
2647            .try_collect()
2648            .await?;
2649        versions.sort_unstable_by_key(|version| version.version);
2650        Ok(versions)
2651    }
2652
2653    /// List all detached manifest locations.
2654    ///
2655    /// Detached manifests are versions that are not part of the main version history.
2656    /// They are created by `commit_detached` and can be used for staging changes.
2657    ///
2658    /// To read transaction properties from a detached manifest:
2659    /// ```ignore
2660    /// let detached = dataset.list_detached_manifests().await?;
2661    /// for location in detached {
2662    ///     let ds = dataset.checkout_version(location.version).await?;
2663    ///     let tx = ds.read_transaction().await?;
2664    ///     // Access tx.transaction_properties
2665    /// }
2666    /// ```
2667    pub async fn list_detached_manifests(&self) -> Result<Vec<ManifestLocation>> {
2668        self.commit_handler
2669            .list_detached_manifest_locations(&self.base, &self.object_store)
2670            .try_collect()
2671            .await
2672    }
2673
2674    /// Get the latest version of the dataset
2675    /// This is meant to be a fast path for checking if a dataset has changed. This is why
2676    /// we don't return the full version struct.
2677    pub async fn latest_version_id(&self) -> Result<u64> {
2678        Ok(self
2679            .commit_handler
2680            .resolve_latest_location(&self.base, &self.object_store)
2681            .await?
2682            .version)
2683    }
2684
2685    /// Return whether the dataset has a newer committed version.
2686    pub async fn is_stale(&self) -> Result<bool> {
2687        let latest_version = self.latest_version_id().await?;
2688        Ok(latest_version != self.manifest.version)
2689    }
2690
2691    /// Return whether the immediate attached successor manifest exists.
2692    ///
2693    /// This is a fast contiguous-history probe. It does not resolve the latest
2694    /// version and may return `false` if intermediate manifests have been
2695    /// removed. Callers that need a general freshness check should use
2696    /// [`Self::is_stale`].
2697    #[doc(hidden)]
2698    pub async fn has_successor_version(&self) -> Result<bool> {
2699        let Some(next_version) = self.manifest.version.checked_add(1) else {
2700            return Ok(false);
2701        };
2702        if lance_table::format::is_detached_version(next_version) {
2703            return Ok(false);
2704        }
2705
2706        let exists = self
2707            .commit_handler
2708            .version_exists(
2709                &self.base,
2710                next_version,
2711                self.object_store.inner.as_ref(),
2712                self.manifest_location.naming_scheme,
2713            )
2714            .await?;
2715        Ok(exists)
2716    }
2717
2718    pub fn count_fragments(&self) -> usize {
2719        self.manifest.fragments.len()
2720    }
2721
2722    /// Get the schema of the dataset
2723    pub fn schema(&self) -> &Schema {
2724        &self.manifest.schema
2725    }
2726
2727    /// Similar to [Self::schema], but only returns fields that are not marked as blob columns
2728    /// Creates a new empty projection into the dataset schema
2729    pub fn empty_projection(self: &Arc<Self>) -> Projection {
2730        Projection::empty(self.clone())
2731    }
2732
2733    /// Creates a projection that includes all columns in the dataset
2734    pub fn full_projection(self: &Arc<Self>) -> Projection {
2735        Projection::full(self.clone())
2736    }
2737
2738    /// Get fragments.
2739    pub fn get_fragments(&self) -> Vec<FileFragment> {
2740        let dataset = Arc::new(self.clone());
2741        self.manifest
2742            .fragments
2743            .iter()
2744            .map(|f| FileFragment::new(dataset.clone(), f.clone()))
2745            .collect()
2746    }
2747
2748    /// Iterate over manifest fragments without allocating [`FileFragment`] wrappers.
2749    pub fn iter_fragments(&self) -> impl Iterator<Item = &Fragment> {
2750        self.manifest.fragments.iter()
2751    }
2752
2753    pub fn get_fragment(&self, fragment_id: usize) -> Option<FileFragment> {
2754        let metadata = self.find_fragment(fragment_id as u64)?.clone();
2755        Some(FileFragment::new(Arc::new(self.clone()), metadata))
2756    }
2757
2758    pub fn fragments(&self) -> &Arc<Vec<Fragment>> {
2759        &self.manifest.fragments
2760    }
2761
2762    pub(crate) fn normalize_fragment_ids(fragment_ids: &[u32]) -> Vec<u32> {
2763        let mut ids = fragment_ids.to_vec();
2764        ids.sort_unstable();
2765        ids.dedup();
2766        ids
2767    }
2768
2769    pub(crate) fn get_fragments_from_ids(&self, fragment_ids: &[u32]) -> Result<Vec<FileFragment>> {
2770        let ordered_ids = Self::normalize_fragment_ids(fragment_ids);
2771        let fragments = self.get_frags_from_ordered_ids(&ordered_ids);
2772        if let Some(missing_id) = fragments
2773            .iter()
2774            .zip(ordered_ids.iter())
2775            .find_map(|(fragment, fragment_id)| fragment.is_none().then_some(*fragment_id))
2776        {
2777            return Err(Error::invalid_input(format!(
2778                "Unknown fragment id {missing_id} in fragment filter; not part of the current dataset version"
2779            )));
2780        }
2781
2782        Ok(fragments.into_iter().flatten().collect())
2783    }
2784
2785    pub(crate) fn get_existing_fragments_from_ids(
2786        &self,
2787        fragment_ids: &[u32],
2788    ) -> Vec<FileFragment> {
2789        let ordered_ids = Self::normalize_fragment_ids(fragment_ids);
2790        self.get_frags_from_ordered_ids(&ordered_ids)
2791            .into_iter()
2792            .flatten()
2793            .collect()
2794    }
2795
2796    pub(crate) fn get_fragment_metadata_from_ids(
2797        &self,
2798        fragment_ids: &[u32],
2799    ) -> Result<Vec<Fragment>> {
2800        Ok(self
2801            .get_fragments_from_ids(fragment_ids)?
2802            .into_iter()
2803            .map(|fragment| fragment.metadata().clone())
2804            .collect())
2805    }
2806
2807    pub(crate) fn get_existing_fragment_metadata_from_ids(
2808        &self,
2809        fragment_ids: &[u32],
2810    ) -> Vec<Fragment> {
2811        self.get_existing_fragments_from_ids(fragment_ids)
2812            .into_iter()
2813            .map(|fragment| fragment.metadata().clone())
2814            .collect()
2815    }
2816
2817    pub(crate) async fn count_rows_in_fragments(&self, fragment_ids: &[u32]) -> Result<usize> {
2818        let fragments = self.get_fragments_from_ids(fragment_ids)?;
2819        self.count_rows_in_resolved_fragments(fragments).await
2820    }
2821
2822    pub(crate) async fn count_rows_in_existing_fragments(
2823        &self,
2824        fragment_ids: &[u32],
2825    ) -> Result<usize> {
2826        let fragments = self.get_existing_fragments_from_ids(fragment_ids);
2827        self.count_rows_in_resolved_fragments(fragments).await
2828    }
2829
2830    async fn count_rows_in_resolved_fragments(
2831        &self,
2832        fragments: Vec<FileFragment>,
2833    ) -> Result<usize> {
2834        let counts = stream::iter(fragments)
2835            .map(|fragment| async move { fragment.count_rows(None).await })
2836            .buffer_unordered(16)
2837            .try_collect::<Vec<_>>()
2838            .await?;
2839        Ok(counts.iter().sum())
2840    }
2841
2842    /// Resolves fragments for the given ids without scanning the manifest.
2843    ///
2844    /// The ids do not need to be sorted or deduplicated. Each id is resolved
2845    /// independently via the fragment bitmap.
2846    pub fn get_frags_from_ordered_ids(&self, ordered_ids: &[u32]) -> Vec<Option<FileFragment>> {
2847        let dataset = Arc::new(self.clone());
2848        ordered_ids
2849            .iter()
2850            .map(|id| {
2851                if !self.fragment_bitmap.contains(*id) {
2852                    return None;
2853                }
2854                let fragment_index = self.fragment_bitmap.rank(*id) as usize - 1;
2855                let fragment = self.manifest.fragments.get(fragment_index)?;
2856                debug_assert_eq!(
2857                    fragment.id, *id as u64,
2858                    "fragment_bitmap rank({id}) resolved to fragment {}, but fragment_bitmap and manifest.fragments are expected to stay in sync",
2859                    fragment.id
2860                );
2861                Some(FileFragment::new(dataset.clone(), fragment.clone()))
2862            })
2863            .collect()
2864    }
2865
2866    /// Look up the fragment with `id` in the manifest.
2867    ///
2868    /// `Manifest::fragments` is kept sorted by id, so this binary searches
2869    /// rather than scanning. Two kinds of manifest predate that invariant and
2870    /// are still readable: those written before fragments were forced into id
2871    /// order (Lance 0.10 and earlier), and those with duplicate fragment ids
2872    /// (Lance 0.16 and earlier). Neither is rejected on read, so the search
2873    /// result is checked and a scan takes over when it does not match --
2874    /// returning some other fragment's data would be silent corruption.
2875    fn find_fragment(&self, id: u64) -> Option<&Fragment> {
2876        if !u32::try_from(id).is_ok_and(|id| self.fragment_bitmap.contains(id)) {
2877            return None;
2878        }
2879        let fragments = self.manifest.fragments.as_slice();
2880        let index = fragments.partition_point(|fragment| fragment.id < id);
2881        match fragments.get(index) {
2882            Some(fragment) if fragment.id == id => Some(fragment),
2883            _ => fragments.iter().find(|fragment| fragment.id == id),
2884        }
2885    }
2886
2887    // This method filters deleted items from `addr_or_ids` using `addrs` as a reference
2888    async fn filter_addr_or_ids(&self, addr_or_ids: &[u64], addrs: &[u64]) -> Result<Vec<u64>> {
2889        // The final zip pairs these positionally; misalignment must fail
2890        // loud rather than truncate.
2891        if addr_or_ids.len() != addrs.len() {
2892            return Err(Error::internal(format!(
2893                "filter_addr_or_ids: addr_or_ids has {} entries but addrs has {}",
2894                addr_or_ids.len(),
2895                addrs.len()
2896            )));
2897        }
2898        if addrs.is_empty() {
2899            return Ok(Vec::new());
2900        }
2901
2902        let mut perm = permutation::sort(addrs);
2903        // First we sort the addrs, then we transform from Vec<u64> to Vec<Option<u64>> and then
2904        // we un-sort and use the None values to filter `addr_or_ids`
2905        let sorted_addrs = perm.apply_slice(addrs);
2906
2907        // Only collect deletion vectors for the fragments referenced by the given addrs
2908        let referenced_frag_ids = sorted_addrs
2909            .iter()
2910            .map(|addr| RowAddress::from(*addr).fragment_id())
2911            .dedup()
2912            .collect::<Vec<_>>();
2913        let frags = self.get_frags_from_ordered_ids(&referenced_frag_ids);
2914        let dv_futs = frags
2915            .iter()
2916            .map(|frag| {
2917                if let Some(frag) = frag {
2918                    frag.get_deletion_vector().boxed()
2919                } else {
2920                    std::future::ready(Ok(None)).boxed()
2921                }
2922            })
2923            .collect::<Vec<_>>();
2924        let dvs = stream::iter(dv_futs)
2925            .buffered(self.object_store.io_parallelism())
2926            .try_collect::<Vec<_>>()
2927            .await?;
2928
2929        // Iterate through the sorted addresses and sorted fragments (and sorted deletion vectors)
2930        // and filter out addresses that have been deleted
2931        let mut filtered_sorted_addrs = Vec::with_capacity(sorted_addrs.len());
2932        let mut sorted_addr_iter = sorted_addrs.into_iter().map(RowAddress::from);
2933        let mut next_addr = sorted_addr_iter.next().unwrap();
2934        let mut exhausted = false;
2935
2936        for frag_dv in frags.iter().zip(dvs).zip(referenced_frag_ids.iter()) {
2937            let ((frag, dv), frag_id) = frag_dv;
2938            if frag.is_some() {
2939                // Frag exists
2940                if let Some(dv) = dv.as_ref() {
2941                    // Deletion vector exists, scan DV
2942                    for deleted in dv.to_sorted_iter() {
2943                        while next_addr.fragment_id() == *frag_id
2944                            && next_addr.row_offset() < deleted
2945                        {
2946                            filtered_sorted_addrs.push(Some(u64::from(next_addr)));
2947                            if let Some(next) = sorted_addr_iter.next() {
2948                                next_addr = next;
2949                            } else {
2950                                exhausted = true;
2951                                break;
2952                            }
2953                        }
2954                        if exhausted {
2955                            break;
2956                        }
2957                        if next_addr.fragment_id() != *frag_id {
2958                            break;
2959                        }
2960                        if next_addr.row_offset() == deleted {
2961                            filtered_sorted_addrs.push(None);
2962                            if let Some(next) = sorted_addr_iter.next() {
2963                                next_addr = next;
2964                            } else {
2965                                exhausted = true;
2966                                break;
2967                            }
2968                        }
2969                    }
2970                }
2971                if exhausted {
2972                    break;
2973                }
2974                // Either no deletion vector, or we've exhausted it, keep everything else
2975                // in this frag
2976                while next_addr.fragment_id() == *frag_id {
2977                    filtered_sorted_addrs.push(Some(u64::from(next_addr)));
2978                    if let Some(next) = sorted_addr_iter.next() {
2979                        next_addr = next;
2980                    } else {
2981                        break;
2982                    }
2983                }
2984            } else {
2985                // Frag doesn't exist (possibly deleted), delete all items
2986                while next_addr.fragment_id() == *frag_id {
2987                    filtered_sorted_addrs.push(None);
2988                    if let Some(next) = sorted_addr_iter.next() {
2989                        next_addr = next;
2990                    } else {
2991                        break;
2992                    }
2993                }
2994            }
2995        }
2996
2997        // filtered_sorted_ids is now a Vec with the same size as sorted_addrs, but with None
2998        // values where the corresponding address was deleted.  We now need to un-sort it and
2999        // filter out the deleted addresses.
3000        perm.apply_inv_slice_in_place(&mut filtered_sorted_addrs);
3001        Ok(addr_or_ids
3002            .iter()
3003            .zip(filtered_sorted_addrs)
3004            .filter_map(|(addr_or_id, maybe_addr)| maybe_addr.map(|_| *addr_or_id))
3005            .collect())
3006    }
3007
3008    pub(crate) async fn filter_deleted_ids(&self, ids: &[u64]) -> Result<Vec<u64>> {
3009        let (ids, addresses) = if let Some(row_id_index) = get_row_id_index(self).await? {
3010            // Ids absent from the deletion-aware index are deleted; drop
3011            // them from both lists to keep the zip aligned. ids.len() is an
3012            // upper bound on the output size, so allocate once up front.
3013            let mut live_ids = Vec::with_capacity(ids.len());
3014            let mut addresses = Vec::with_capacity(ids.len());
3015            for id in ids {
3016                if let Some(address) = row_id_index.get(*id) {
3017                    live_ids.push(*id);
3018                    addresses.push(u64::from(address));
3019                }
3020            }
3021            (Cow::Owned(live_ids), Cow::Owned(addresses))
3022        } else {
3023            (Cow::Borrowed(ids), Cow::Borrowed(ids))
3024        };
3025
3026        self.filter_addr_or_ids(&ids, &addresses).await
3027    }
3028
3029    /// Gets the number of files that are so small they don't even have a full
3030    /// group. These are considered too small because reading many of them is
3031    /// much less efficient than reading a single file because the separate files
3032    /// split up what would otherwise be single IO requests into multiple.
3033    pub async fn num_small_files(&self, max_rows_per_group: usize) -> usize {
3034        futures::stream::iter(self.get_fragments())
3035            .map(|f| async move { f.physical_rows().await })
3036            .buffered(self.object_store.io_parallelism())
3037            .try_filter(|row_count| futures::future::ready(*row_count < max_rows_per_group))
3038            .count()
3039            .await
3040    }
3041
3042    pub async fn validate(&self) -> Result<()> {
3043        // All fragments have unique ids
3044        let id_counts =
3045            self.manifest
3046                .fragments
3047                .iter()
3048                .map(|f| f.id)
3049                .fold(HashMap::new(), |mut acc, id| {
3050                    *acc.entry(id).or_insert(0) += 1;
3051                    acc
3052                });
3053        for (id, count) in id_counts {
3054            if count > 1 {
3055                return Err(Error::corrupt_file(
3056                    self.base.clone(),
3057                    format!(
3058                        "Duplicate fragment id {} found in dataset {:?}",
3059                        id, self.base
3060                    ),
3061                ));
3062            }
3063        }
3064
3065        // Fragments are sorted in increasing fragment id order
3066        self.manifest
3067            .fragments
3068            .iter()
3069            .map(|f| f.id)
3070            .try_fold(0, |prev, id| {
3071                if id < prev {
3072                    Err(Error::corrupt_file(self.base.clone(), format!(
3073                        "Fragment ids are not sorted in increasing fragment-id order. Found {} after {} in dataset {:?}",
3074                        id, prev, self.base
3075                    )))
3076                } else {
3077                    Ok(id)
3078                }
3079            })?;
3080
3081        // All fragments have equal lengths
3082        futures::stream::iter(self.get_fragments())
3083            .map(|f| async move { f.validate().await })
3084            .buffer_unordered(self.object_store.io_parallelism())
3085            .try_collect::<Vec<()>>()
3086            .await?;
3087
3088        rowids::validate_stable_row_ids(self).await?;
3089
3090        // Validate indices
3091        let indices = self.load_indices().await?;
3092        self.validate_indices(&indices)?;
3093
3094        Ok(())
3095    }
3096
3097    fn validate_indices(&self, indices: &[IndexMetadata]) -> Result<()> {
3098        // Make sure there are no duplicate ids
3099        let mut index_ids = HashSet::new();
3100        for index in indices.iter() {
3101            if !index_ids.insert(&index.uuid) {
3102                return Err(Error::corrupt_file(
3103                    self.manifest_location.path.clone(),
3104                    format!(
3105                        "Duplicate index id {} found in dataset {:?}",
3106                        index.uuid, self.base
3107                    ),
3108                ));
3109            }
3110        }
3111
3112        // For each index name, make sure there is no overlap in fragment bitmaps
3113        if let Err(err) = detect_overlapping_fragments(indices) {
3114            let mut message = "Overlapping fragments detected in dataset.".to_string();
3115            for (index_name, overlapping_frags) in err.bad_indices {
3116                message.push_str(&format!(
3117                    "\nIndex {:?} has overlapping fragments: {:?}",
3118                    index_name, overlapping_frags
3119                ));
3120            }
3121            return Err(Error::corrupt_file(
3122                self.manifest_location.path.clone(),
3123                message,
3124            ));
3125        };
3126
3127        Ok(())
3128    }
3129
3130    /// Migrate the dataset to use the new manifest path scheme.
3131    ///
3132    /// This function will rename all V1 manifests to [ManifestNamingScheme::V2].
3133    /// These paths provide more efficient opening of datasets with many versions
3134    /// on object stores.
3135    ///
3136    /// This function is idempotent, and can be run multiple times without
3137    /// changing the state of the object store.
3138    ///
3139    /// However, it should not be run while other concurrent operations are happening.
3140    /// And it should also run until completion before resuming other operations.
3141    ///
3142    /// ```rust
3143    /// # use lance::dataset::Dataset;
3144    /// # use lance_table::io::commit::ManifestNamingScheme;
3145    /// # use lance_datagen::{array, RowCount, BatchCount};
3146    /// # use arrow_array::types::Int32Type;
3147    /// # use lance::dataset::write::WriteParams;
3148    /// # let data = lance_datagen::gen_batch()
3149    /// #  .col("key", array::step::<Int32Type>())
3150    /// #  .into_reader_rows(RowCount::from(10), BatchCount::from(1));
3151    /// # let fut = async {
3152    /// # let params = WriteParams {
3153    /// #     enable_v2_manifest_paths: false,
3154    /// #     ..Default::default()
3155    /// # };
3156    /// let mut dataset = Dataset::write(data, "memory://test", Some(params)).await.unwrap();
3157    /// assert_eq!(dataset.manifest_location().naming_scheme, ManifestNamingScheme::V1);
3158    ///
3159    /// dataset.migrate_manifest_paths_v2().await.unwrap();
3160    /// assert_eq!(dataset.manifest_location().naming_scheme, ManifestNamingScheme::V2);
3161    /// # };
3162    /// # tokio::runtime::Runtime::new().unwrap().block_on(fut);
3163    /// ```
3164    pub async fn migrate_manifest_paths_v2(&mut self) -> Result<()> {
3165        migrate_scheme_to_v2(self.object_store.as_ref(), &self.base).await?;
3166        // We need to re-open.
3167        let latest_version = self.latest_version_id().await?;
3168        *self = self.checkout_version(latest_version).await?;
3169        Ok(())
3170    }
3171
3172    /// Assign stable row ID sequences to fragments that do not yet have them,
3173    /// contiguously from `start`, and return the resulting `next_row_id`
3174    /// high-water mark.
3175    fn assign_stable_row_ids_for_migration(fragments: &mut [Fragment], start: u64) -> Result<u64> {
3176        let mut next_row_id = start;
3177        for fragment in fragments.iter_mut() {
3178            let physical_rows = fragment.physical_rows.ok_or_else(|| {
3179                Error::internal(format!(
3180                    "Fragment {} is missing physical_rows; cannot assign stable row IDs",
3181                    fragment.id
3182                ))
3183            })? as u64;
3184            let end = next_row_id
3185                .checked_add(physical_rows)
3186                .ok_or_else(|| Error::internal("Row ID overflow during stable row ID migration"))?;
3187            let sequence = RowIdSequence::from(next_row_id..end);
3188            fragment.row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&sequence).into()));
3189            next_row_id = end;
3190        }
3191        Ok(next_row_id)
3192    }
3193
3194    /// Migrate a table to use stable row IDs.
3195    ///
3196    /// Stable row IDs assign a persistent identifier to each row that remains
3197    /// stable across compaction operations. This enables more efficient updates
3198    /// to secondary indices.
3199    ///
3200    /// A single Merge commit assigns row ID sequences to all fragments and
3201    /// activates the stable row ID feature flag atomically. Because `Merge`
3202    /// conflicts with all data-modifying operations, a successful commit
3203    /// guarantees no concurrent write occurred — no separate validation step
3204    /// is needed.
3205    ///
3206    /// **No retries are attempted.** Callers should quiesce concurrent writes
3207    /// before running this migration. If a conflicting write is detected, this
3208    /// method returns an error and the caller must retry.
3209    ///
3210    /// This method is idempotent: if the table already uses stable row IDs,
3211    /// it returns `Ok(())` immediately.
3212    pub async fn migrate_to_stable_row_ids(&mut self) -> Result<()> {
3213        if self.manifest.uses_stable_row_ids() {
3214            return Ok(());
3215        }
3216
3217        let mut fragments = self.manifest.fragments.as_ref().clone();
3218        // Restore carries the high-water mark forward across a version that
3219        // predates activation, so a re-migration must allocate above it rather
3220        // than reissue ids the earlier versions still hold.
3221        let next_row_id =
3222            Self::assign_stable_row_ids_for_migration(&mut fragments, self.manifest.next_row_id)?;
3223        let schema = self.manifest.schema.clone();
3224        let read_version = self.manifest.version;
3225
3226        let transaction = Transaction::new(
3227            read_version,
3228            Operation::Merge {
3229                fragments,
3230                schema,
3231                preserves_nullability: true,
3232            },
3233            None,
3234        );
3235
3236        let new_ds = CommitBuilder::new(Arc::new(self.clone()))
3237            .with_max_retries(0)
3238            .with_stable_row_id_migration_activation(next_row_id)
3239            .execute(transaction)
3240            .await?;
3241
3242        *self = new_ds;
3243        Ok(())
3244    }
3245
3246    /// Shallow clone the target version into a new dataset at target_path.
3247    /// 'target_path': the uri string to clone the dataset into.
3248    /// 'version': the version cloned from, could be a version number or tag.
3249    /// 'store_params': the object store params to use for the new dataset.
3250    pub async fn shallow_clone(
3251        &mut self,
3252        target_path: &str,
3253        version: impl Into<refs::Ref>,
3254        store_params: Option<ObjectStoreParams>,
3255    ) -> Result<Self> {
3256        let (ref_name, version_number) = self.resolve_reference(version.into()).await?;
3257        let source_location = self.branch_location().find_branch(ref_name.as_deref())?;
3258        let clone_op = Operation::Clone {
3259            is_shallow: true,
3260            ref_name,
3261            ref_version: version_number,
3262            ref_path: source_location.uri,
3263            branch_name: None,
3264        };
3265        let transaction = Transaction::new(version_number, clone_op, None);
3266
3267        let builder = CommitBuilder::new(WriteDestination::Uri(target_path))
3268            .with_store_params(
3269                store_params.unwrap_or(self.store_params.as_deref().cloned().unwrap_or_default()),
3270            )
3271            .with_object_store(Arc::new(self.object_store.as_ref().clone()))
3272            .with_commit_handler(self.commit_handler.clone())
3273            .with_exact_storage_format(self.manifest.data_storage_format.lance_file_format());
3274        builder.execute(transaction).await
3275    }
3276
3277    /// Deep clone the target version into a new dataset at target_path.
3278    /// This copies all relevant dataset files (data files, deletion files, and
3279    /// index files) into the target dataset without loading data into memory.
3280    ///
3281    /// The source files are read through this dataset's own object store while the
3282    /// copies are written through the target object store built from `store_params`.
3283    /// This makes the clone work across accounts/stores (e.g. between two abfss
3284    /// accounts): when the source and target stores are the same the copy stays
3285    /// server-side, otherwise the data is streamed through this process.
3286    ///
3287    /// Parameters:
3288    /// - `target_path`: the URI string to clone the dataset into.
3289    /// - `version`: the version cloned from, could be a version number, branch head, or tag.
3290    /// - `store_params`: the object store params for the target dataset (e.g. the
3291    ///   credentials of the target account).
3292    ///
3293    /// Note: external `base_paths` referenced by the source manifest are read through
3294    /// this dataset's object store; per-base distinct source credentials are not yet
3295    /// supported (see <https://github.com/lance-format/lance/issues/6093>).
3296    pub async fn deep_clone(
3297        &mut self,
3298        target_path: &str,
3299        version: impl Into<refs::Ref>,
3300        store_params: Option<ObjectStoreParams>,
3301    ) -> Result<Self> {
3302        use futures::StreamExt;
3303
3304        // Resolve source dataset and its manifest using checkout_version
3305        let src_ds = self.checkout_version(version).await?;
3306        let src_paths = src_ds.collect_paths().await?;
3307
3308        // Prepare target object store and base path
3309        let (target_store, target_base) = ObjectStore::from_uri_and_params(
3310            self.session.store_registry(),
3311            target_path,
3312            &store_params.clone().unwrap_or_default(),
3313        )
3314        .await?;
3315
3316        // Prevent cloning into an existing target dataset
3317        if self
3318            .commit_handler
3319            .resolve_latest_location(&target_base, &target_store)
3320            .await
3321            .is_ok()
3322        {
3323            return Err(Error::dataset_already_exists(target_path.to_string()));
3324        }
3325
3326        let build_absolute_path = |relative_path: &str, base: &Path| -> Path {
3327            let mut path = base.clone();
3328            for seg in relative_path.split('/') {
3329                if !seg.is_empty() {
3330                    path = path.clone().join(seg);
3331                }
3332            }
3333            path
3334        };
3335
3336        // When the source and target live in the same store we can keep the copy
3337        // server-side. Otherwise (e.g. cloning across accounts) we stream each file
3338        // from the source store to the target store.
3339        let same_store = src_ds.object_store.store_prefix == target_store.store_prefix;
3340
3341        // TODO: Leverage object store bulk copy for efficient same-store deep_clone.
3342        //
3343        // All cloud storage providers support batch copy APIs that would provide significant
3344        // performance improvements. We use single file copy before we have upstream support.
3345        //
3346        // Tracked by: https://github.com/lance-format/lance/issues/5435
3347        let io_parallelism = self.object_store.io_parallelism();
3348        let copy_futures = src_paths
3349            .iter()
3350            .map(|(relative_path, base)| {
3351                let source_store = Arc::clone(&src_ds.object_store);
3352                let target_store = Arc::clone(&target_store);
3353                let src_path = build_absolute_path(relative_path, base);
3354                let target_path = build_absolute_path(relative_path, &target_base);
3355                async move {
3356                    if same_store {
3357                        target_store.copy(&src_path, &target_path).await?;
3358                    } else {
3359                        let reader = source_store.open(&src_path).await?;
3360                        let mut writer = target_store.create(&target_path).await?;
3361                        writer.copy_from_reader(reader.as_ref()).await?;
3362                        writer.shutdown().await?;
3363                    }
3364                    Result::Ok(())
3365                }
3366            })
3367            .collect::<Vec<_>>();
3368
3369        futures::stream::iter(copy_futures)
3370            .buffer_unordered(io_parallelism)
3371            .collect::<Vec<_>>()
3372            .await
3373            .into_iter()
3374            .collect::<Result<Vec<_>>>()?;
3375
3376        // Record a Clone operation and commit via CommitBuilder
3377        let ref_name = src_ds.manifest.branch.clone();
3378        let ref_version = src_ds.manifest_location.version;
3379        let clone_op = Operation::Clone {
3380            is_shallow: false,
3381            ref_name,
3382            ref_version,
3383            ref_path: src_ds.uri().to_string(),
3384            branch_name: None,
3385        };
3386        let txn = Transaction::new(ref_version, clone_op, None);
3387        let builder = CommitBuilder::new(WriteDestination::Uri(target_path))
3388            .with_store_params(store_params.clone().unwrap_or_default())
3389            .with_object_store(target_store.clone())
3390            .with_source_store(src_ds.object_store.clone())
3391            .with_commit_handler(self.commit_handler.clone())
3392            .with_exact_storage_format(self.manifest.data_storage_format.lance_file_format());
3393        let new_ds = builder.execute(txn).await?;
3394        Ok(new_ds)
3395    }
3396
3397    async fn resolve_reference(&self, reference: refs::Ref) -> Result<(Option<String>, u64)> {
3398        match reference {
3399            refs::Ref::Version(branch, version_number) => {
3400                if let Some(version_number) = version_number {
3401                    Ok((branch, version_number))
3402                } else {
3403                    let branch_location = self.branch_location().find_branch(branch.as_deref())?;
3404                    let version_number = self
3405                        .commit_handler
3406                        .resolve_latest_location(&branch_location.path, &self.object_store)
3407                        .await?
3408                        .version;
3409                    Ok((branch, version_number))
3410                }
3411            }
3412            refs::Ref::VersionNumber(version_number) => {
3413                Ok((self.manifest.branch.clone(), version_number))
3414            }
3415            refs::Ref::Tag(tag_name) => {
3416                let tag_contents = self.tags().get(tag_name.as_str()).await?;
3417                Ok((tag_contents.branch, tag_contents.version))
3418            }
3419        }
3420    }
3421
3422    /// Collect all (relative_path, path) of the dataset files.
3423    async fn collect_paths(&self) -> Result<Vec<(String, Path)>> {
3424        let mut file_paths: Vec<(String, Path)> = Vec::new();
3425        for fragment in self.manifest.fragments.iter() {
3426            if let Some(RowIdMeta::External(external_file)) = &fragment.row_id_meta {
3427                return Err(Error::internal(format!(
3428                    "External row_id_meta is not supported yet. external file path: {}",
3429                    external_file.path
3430                )));
3431            }
3432            for data_file in fragment.referenced_lance_files() {
3433                let base_root = if let Some(base_id) = data_file.base_id {
3434                    let base_path =
3435                        self.manifest.base_paths.get(&base_id).ok_or_else(|| {
3436                            Error::internal(format!("base_id {} not found", base_id))
3437                        })?;
3438                    Path::parse(base_path.path.as_str())?
3439                } else {
3440                    self.base.clone()
3441                };
3442                file_paths.push((
3443                    format!("{}/{}", DATA_DIR, data_file.path.clone()),
3444                    base_root,
3445                ));
3446            }
3447            if let Some(deletion_file) = &fragment.deletion_file {
3448                let base_root = if let Some(base_id) = deletion_file.base_id {
3449                    let base_path =
3450                        self.manifest.base_paths.get(&base_id).ok_or_else(|| {
3451                            Error::internal(format!("base_id {} not found", base_id))
3452                        })?;
3453                    Path::parse(base_path.path.as_str())?
3454                } else {
3455                    self.base.clone()
3456                };
3457                file_paths.push((
3458                    relative_deletion_file_path(fragment.id, deletion_file),
3459                    base_root,
3460                ));
3461            }
3462        }
3463
3464        let indices = read_manifest_indexes(
3465            self.object_store.as_ref(),
3466            &self.manifest_location,
3467            &self.manifest,
3468        )
3469        .await?;
3470
3471        for index in &indices {
3472            let base_root = if let Some(base_id) = index.base_id {
3473                let base_path = self
3474                    .manifest
3475                    .base_paths
3476                    .get(&base_id)
3477                    .ok_or_else(|| Error::internal(format!("base_id {} not found", base_id)))?;
3478                Path::parse(base_path.path.as_str())?
3479            } else {
3480                self.base.clone()
3481            };
3482            let index_root = base_root
3483                .clone()
3484                .join(INDICES_DIR)
3485                .join(index.uuid.to_string());
3486            let mut stream = self.object_store.read_dir_all(&index_root, None);
3487            while let Some(meta) = stream.next().await.transpose()? {
3488                if let Some(filename) = meta.location.filename() {
3489                    file_paths.push((
3490                        format!("{}/{}/{}", INDICES_DIR, index.uuid, filename),
3491                        base_root.clone(),
3492                    ));
3493                }
3494            }
3495        }
3496        Ok(file_paths)
3497    }
3498
3499    /// Run a SQL query against the dataset.
3500    /// The underlying SQL engine is DataFusion.
3501    /// Please refer to the DataFusion documentation for supported SQL syntax.
3502    pub fn sql(&self, sql: &str) -> SqlQueryBuilder {
3503        SqlQueryBuilder::new(self.clone(), sql)
3504    }
3505}
3506
3507pub(crate) struct NewTransactionResult<'a> {
3508    pub dataset: BoxFuture<'a, Result<Dataset>>,
3509    pub new_transactions: BoxStream<'a, Result<(u64, Arc<Transaction>)>>,
3510}
3511
3512pub(crate) fn load_new_transactions(dataset: &Dataset) -> NewTransactionResult<'_> {
3513    // Resolve every manifest with version > our current version (the latest plus
3514    // the ones in between). On non-lexically-ordered stores this uses the version
3515    // hint to avoid an O(n) listing.
3516    let io_parallelism = dataset.object_store.as_ref().io_parallelism();
3517    let locations = dataset.commit_handler.list_manifest_locations_since(
3518        &dataset.base,
3519        dataset.object_store.as_ref(),
3520        dataset.manifest.version,
3521    );
3522
3523    // Will send the latest manifest via a channel.
3524    let (latest_tx, latest_rx) = tokio::sync::oneshot::channel();
3525    let mut latest_tx = Some(latest_tx);
3526
3527    let manifests = locations
3528        .map_ok(move |location| {
3529            let latest_tx = latest_tx.take();
3530            async move {
3531                let manifest = Dataset::get_manifest(
3532                    dataset.object_store.as_ref(),
3533                    &location,
3534                    &dataset.uri,
3535                    dataset.session.as_ref(),
3536                )
3537                .await?;
3538
3539                if let Some(latest_tx) = latest_tx {
3540                    // We ignore the error, since we don't care if the receiver is dropped.
3541                    let _ = latest_tx.send((manifest.clone(), location.clone()));
3542                }
3543
3544                Ok((manifest, location))
3545            }
3546        })
3547        .try_buffer_unordered(io_parallelism / 2);
3548    let transactions = manifests
3549        .map_ok(move |(manifest, location)| async move {
3550            let manifest_copy = manifest.clone();
3551            let tx_key = TransactionKey {
3552                version: manifest.version,
3553            };
3554            let transaction =
3555                if let Some(cached) = dataset.metadata_cache.get_with_key(&tx_key).await {
3556                    cached
3557                } else {
3558                    let dataset_version = Dataset::checkout_manifest(
3559                        dataset.object_store.clone(),
3560                        dataset.base.clone(),
3561                        dataset.uri.clone(),
3562                        manifest_copy.clone(),
3563                        location,
3564                        dataset.session(),
3565                        dataset.commit_handler.clone(),
3566                        dataset.file_reader_options.clone(),
3567                        dataset.store_params.as_deref().cloned(),
3568                        dataset.base_store_params.clone(),
3569                    )?;
3570                    let loaded =
3571                        Arc::new(dataset_version.read_transaction().await?.ok_or_else(|| {
3572                            Error::internal(format!(
3573                                "Dataset version {} does not have a transaction file",
3574                                manifest_copy.version
3575                            ))
3576                        })?);
3577                    dataset
3578                        .metadata_cache
3579                        .insert_with_key(&tx_key, loaded.clone())
3580                        .await;
3581                    loaded
3582                };
3583            Ok((manifest.version, transaction))
3584        })
3585        .try_buffer_unordered(io_parallelism / 2);
3586
3587    let dataset = async move {
3588        if let Ok((latest_manifest, location)) = latest_rx.await {
3589            // If we got the latest manifest, we can checkout the dataset.
3590            Dataset::checkout_manifest(
3591                dataset.object_store.clone(),
3592                dataset.base.clone(),
3593                dataset.uri.clone(),
3594                latest_manifest,
3595                location,
3596                dataset.session(),
3597                dataset.commit_handler.clone(),
3598                dataset.file_reader_options.clone(),
3599                dataset.store_params.as_deref().cloned(),
3600                dataset.base_store_params.clone(),
3601            )
3602        } else {
3603            // If we didn't get the latest manifest, we can still return the dataset
3604            // with the current manifest.
3605            Ok(dataset.clone())
3606        }
3607    }
3608    .boxed();
3609
3610    let new_transactions = transactions.boxed();
3611
3612    NewTransactionResult {
3613        dataset,
3614        new_transactions,
3615    }
3616}
3617
3618/// # Schema Evolution
3619///
3620/// Lance datasets support evolving the schema. Several operations are
3621/// supported that mirror common SQL operations:
3622///
3623/// - [Self::add_columns()]: Add new columns to the dataset, similar to `ALTER TABLE ADD COLUMN`.
3624/// - [Self::drop_columns()]: Drop columns from the dataset, similar to `ALTER TABLE DROP COLUMN`.
3625/// - [Self::alter_columns()]: Modify columns in the dataset, changing their name, type, or nullability.
3626///   Similar to `ALTER TABLE ALTER COLUMN`.
3627///
3628/// In addition, one operation is unique to Lance: [`merge`](Self::merge). This
3629/// operation allows inserting precomputed data into the dataset.
3630///
3631/// Because these operations change the schema of the dataset, they will conflict
3632/// with most other concurrent operations. Therefore, they should be performed
3633/// when no other write operations are being run.
3634impl Dataset {
3635    /// Append new columns to the dataset.
3636    pub async fn add_columns(
3637        &mut self,
3638        transforms: NewColumnTransform,
3639        read_columns: Option<Vec<String>>,
3640        batch_size: Option<u32>,
3641    ) -> Result<()> {
3642        schema_evolution::add_columns(self, transforms, read_columns, batch_size).await
3643    }
3644
3645    /// Modify columns in the dataset, changing their name, type, or nullability.
3646    ///
3647    /// If only changing the name or nullability of a column, this is a zero-copy
3648    /// operation and any indices will be preserved. If changing the type of a
3649    /// column, the data for that column will be rewritten and any indices will
3650    /// be dropped. The old column data will not be immediately deleted. To remove
3651    /// it, call [optimize::compact_files()] and then
3652    /// [cleanup::cleanup_old_versions()] on the dataset.
3653    pub async fn alter_columns(&mut self, alterations: &[ColumnAlteration]) -> Result<()> {
3654        schema_evolution::alter_columns(self, alterations).await
3655    }
3656
3657    /// Remove columns from the dataset.
3658    ///
3659    /// This is a metadata-only operation and does not remove the data from the
3660    /// underlying storage. In order to remove the data, you must subsequently
3661    /// call [optimize::compact_files()] to rewrite the data without the removed columns and
3662    /// then call [cleanup::cleanup_old_versions()] to remove the old files.
3663    pub async fn drop_columns(&mut self, columns: &[&str]) -> Result<()> {
3664        info!(target: TRACE_DATASET_EVENTS, event=DATASET_DROPPING_COLUMN_EVENT, uri = &self.uri, columns = columns.join(","));
3665        schema_evolution::drop_columns(self, columns).await
3666    }
3667
3668    /// Drop columns from the dataset and return updated dataset. Note that this
3669    /// is a zero-copy operation and column is not physically removed from the
3670    /// dataset.
3671    /// Parameters:
3672    /// - `columns`: the list of column names to drop.
3673    #[deprecated(since = "0.9.12", note = "Please use `drop_columns` instead.")]
3674    pub async fn drop(&mut self, columns: &[&str]) -> Result<()> {
3675        self.drop_columns(columns).await
3676    }
3677
3678    async fn merge_impl(
3679        &mut self,
3680        stream: Box<dyn RecordBatchReader + Send>,
3681        left_on: &str,
3682        right_on: &str,
3683    ) -> Result<()> {
3684        // Sanity check.
3685        if self.schema().field(left_on).is_none() && left_on != ROW_ID && left_on != ROW_ADDR {
3686            return Err(Error::invalid_input(format!(
3687                "Column {} does not exist in the left side dataset",
3688                left_on
3689            )));
3690        };
3691        let right_schema = stream.schema();
3692        if right_schema.field_with_name(right_on).is_err() {
3693            return Err(Error::invalid_input(format!(
3694                "Column {} does not exist in the right side dataset",
3695                right_on
3696            )));
3697        };
3698        for field in right_schema.fields() {
3699            if field.name() == right_on {
3700                // right_on is allowed to exist in the dataset, since it may be
3701                // the same as left_on.
3702                continue;
3703            }
3704            if self.schema().field(field.name()).is_some() {
3705                return Err(Error::invalid_input(format!(
3706                    "Column {} exists in both sides of the dataset",
3707                    field.name()
3708                )));
3709            }
3710        }
3711
3712        // Hash join
3713        let joiner = Arc::new(HashJoiner::try_new(stream, right_on).await?);
3714        // Final schema is union of current schema, plus the RHS schema without
3715        // the right_on key.
3716        let mut new_schema: Schema = self.schema().merge(joiner.out_schema().as_ref())?;
3717        new_schema.set_field_id(Some(self.manifest.max_field_id()));
3718
3719        // Write new data file to each fragment. Parallelism is done over columns,
3720        // so no parallelism done at this level.
3721        let updated_fragments: Vec<Fragment> = stream::iter(self.get_fragments())
3722            .then(|f| {
3723                let joiner = joiner.clone();
3724                async move { f.merge(left_on, &joiner).await.map(|f| f.metadata) }
3725            })
3726            .try_collect::<Vec<_>>()
3727            .await?;
3728
3729        let preserves_nullability =
3730            !schema_evolution::merge_introduces_required_field(self.schema(), &new_schema);
3731        let transaction = Transaction::new(
3732            self.manifest.version,
3733            Operation::Merge {
3734                fragments: updated_fragments,
3735                schema: new_schema,
3736                preserves_nullability,
3737            },
3738            None,
3739        );
3740
3741        self.apply_commit(transaction, &Default::default(), &Default::default())
3742            .await?;
3743
3744        Ok(())
3745    }
3746
3747    /// Merge this dataset with another arrow Table / Dataset, and returns a new version of dataset.
3748    ///
3749    /// Parameters:
3750    ///
3751    /// - `stream`: the stream of [`RecordBatch`] to merge.
3752    /// - `left_on`: the column name to join on the left side (self).
3753    /// - `right_on`: the column name to join on the right side (stream).
3754    ///
3755    /// Returns: a new version of dataset.
3756    ///
3757    /// It performs a left-join on the two datasets.
3758    pub async fn merge(
3759        &mut self,
3760        stream: impl RecordBatchReader + Send + 'static,
3761        left_on: &str,
3762        right_on: &str,
3763    ) -> Result<()> {
3764        let stream = Box::new(stream);
3765        self.merge_impl(stream, left_on, right_on).await
3766    }
3767
3768    /// Merge a distributed scalar index into a single root artifact and report
3769    /// progress via the supplied callback.
3770    pub async fn merge_index_metadata(
3771        &self,
3772        index_uuid: &Uuid,
3773        index_type: IndexType,
3774        _batch_readhead: Option<usize>,
3775        progress: Arc<dyn IndexBuildProgress>,
3776    ) -> Result<()> {
3777        let store = LanceIndexStore::from_dataset_for_new(self, index_uuid)?;
3778        let index_dir = self.indices_dir().join(index_uuid.to_string());
3779        match index_type {
3780            IndexType::Inverted => {
3781                // Call merge_index_files function for inverted index
3782                lance_index::scalar::inverted::builder::merge_index_files(
3783                    self.object_store.as_ref(),
3784                    &index_dir,
3785                    Arc::new(store),
3786                    progress,
3787                )
3788                .await
3789            }
3790            IndexType::BTree => {
3791                Err(Error::invalid_input(
3792                    "BTree distributed indexing no longer supports merge_index_metadata; \
3793                     build segments, optionally merge groups with merge_existing_index_segments(...), \
3794                     and commit with commit_existing_index_segments(...)"
3795                        .to_string(),
3796                ))
3797            }
3798            IndexType::Bitmap => {
3799                Err(Error::invalid_input(
3800                    "Bitmap distributed indexing no longer supports merge_index_metadata; \
3801                     build segments with create_index_uncommitted(...), merge them with \
3802                     merge_existing_index_segments(...), and commit with \
3803                     commit_existing_index_segments(...)"
3804                        .to_string(),
3805                ))
3806            }
3807            IndexType::IvfFlat | IndexType::IvfPq | IndexType::IvfSq | IndexType::Vector => {
3808                Err(Error::invalid_input(
3809                    "Vector distributed indexing no longer supports merge_index_metadata; \
3810                     build segments, optionally merge groups with merge_existing_index_segments(...), \
3811                     and commit with commit_existing_index_segments(...)"
3812                        .to_string(),
3813                ))
3814            }
3815            _ => Err(Error::invalid_input_source(Box::new(std::io::Error::new(
3816                std::io::ErrorKind::InvalidInput,
3817                format!("Unsupported index type (patched): {}", index_type),
3818            )))),
3819        }
3820    }
3821}
3822
3823/// # Dataset metadata APIs
3824///
3825/// There are four kinds of metadata on datasets:
3826///
3827///  - **Schema metadata**: metadata about the data itself.
3828///  - **Field metadata**: metadata about the dataset itself.
3829///  - **Dataset metadata**: metadata about the dataset. For example, this could
3830///    store a created_at date.
3831///  - **Dataset config**: configuration values controlling how engines should
3832///    manage the dataset. This configures things like auto-cleanup.
3833///
3834/// You can get
3835impl Dataset {
3836    /// Get dataset metadata.
3837    pub fn metadata(&self) -> &HashMap<String, String> {
3838        &self.manifest.table_metadata
3839    }
3840
3841    /// Get the dataset config from manifest
3842    pub fn config(&self) -> &HashMap<String, String> {
3843        &self.manifest.config
3844    }
3845
3846    /// Delete keys from the config.
3847    #[deprecated(
3848        note = "Use the new update_config(values, replace) method - pass None values to delete keys"
3849    )]
3850    pub async fn delete_config_keys(&mut self, delete_keys: &[&str]) -> Result<()> {
3851        let updates = delete_keys.iter().map(|key| (*key, None));
3852        self.update_config(updates).await?;
3853        Ok(())
3854    }
3855
3856    /// Update table metadata.
3857    ///
3858    /// Pass `None` for a value to remove that key.
3859    ///
3860    /// Use `.replace()` to replace the entire metadata map instead of merging.
3861    ///
3862    /// Returns the updated metadata map after the operation.
3863    ///
3864    /// ```
3865    /// # use lance::{Dataset, Result};
3866    /// # use lance::dataset::transaction::UpdateMapEntry;
3867    /// # async fn test_update_metadata(dataset: &mut Dataset) -> Result<()> {
3868    /// // Update single key
3869    /// dataset.update_metadata([("key", "value")]).await?;
3870    ///
3871    /// // Remove a key
3872    /// dataset.update_metadata([("to_delete", None)]).await?;
3873    ///
3874    /// // Clear all metadata
3875    /// dataset.update_metadata([] as [UpdateMapEntry; 0]).replace().await?;
3876    ///
3877    /// // Replace full metadata
3878    /// dataset.update_metadata([("k1", "v1"), ("k2", "v2")]).replace().await?;
3879    /// # Ok(())
3880    /// # }
3881    /// ```
3882    pub fn update_metadata(
3883        &mut self,
3884        values: impl IntoIterator<Item = impl Into<UpdateMapEntry>>,
3885    ) -> metadata::UpdateMetadataBuilder<'_> {
3886        metadata::UpdateMetadataBuilder::new(self, values, metadata::MetadataType::TableMetadata)
3887    }
3888
3889    /// Update config.
3890    ///
3891    /// Pass `None` for a value to remove that key.
3892    ///
3893    /// Use `.replace()` to replace the entire config map instead of merging.
3894    ///
3895    /// Returns the updated config map after the operation.
3896    ///
3897    /// ```
3898    /// # use lance::{Dataset, Result};
3899    /// # use lance::dataset::transaction::UpdateMapEntry;
3900    /// # async fn test_update_config(dataset: &mut Dataset) -> Result<()> {
3901    /// // Update single key
3902    /// dataset.update_config([("key", "value")]).await?;
3903    ///
3904    /// // Remove a key
3905    /// dataset.update_config([("to_delete", None)]).await?;
3906    ///
3907    /// // Clear all config
3908    /// dataset.update_config([] as [UpdateMapEntry; 0]).replace().await?;
3909    ///
3910    /// // Replace full config
3911    /// dataset.update_config([("k1", "v1"), ("k2", "v2")]).replace().await?;
3912    /// # Ok(())
3913    /// # }
3914    /// ```
3915    pub fn update_config(
3916        &mut self,
3917        values: impl IntoIterator<Item = impl Into<UpdateMapEntry>>,
3918    ) -> metadata::UpdateMetadataBuilder<'_> {
3919        metadata::UpdateMetadataBuilder::new(self, values, metadata::MetadataType::Config)
3920    }
3921
3922    /// Update schema metadata.
3923    ///
3924    /// Pass `None` for a value to remove that key.
3925    ///
3926    /// Use `.replace()` to replace the entire schema metadata map instead of merging.
3927    ///
3928    /// Returns the updated schema metadata map after the operation.
3929    ///
3930    /// ```
3931    /// # use lance::{Dataset, Result};
3932    /// # use lance::dataset::transaction::UpdateMapEntry;
3933    /// # async fn test_update_schema_metadata(dataset: &mut Dataset) -> Result<()> {
3934    /// // Update single key
3935    /// dataset.update_schema_metadata([("key", "value")]).await?;
3936    ///
3937    /// // Remove a key
3938    /// dataset.update_schema_metadata([("to_delete", None)]).await?;
3939    ///
3940    /// // Clear all schema metadata
3941    /// dataset.update_schema_metadata([] as [UpdateMapEntry; 0]).replace().await?;
3942    ///
3943    /// // Replace full schema metadata
3944    /// dataset.update_schema_metadata([("k1", "v1"), ("k2", "v2")]).replace().await?;
3945    /// # Ok(())
3946    /// # }
3947    /// ```
3948    pub fn update_schema_metadata(
3949        &mut self,
3950        values: impl IntoIterator<Item = impl Into<UpdateMapEntry>>,
3951    ) -> metadata::UpdateMetadataBuilder<'_> {
3952        metadata::UpdateMetadataBuilder::new(self, values, metadata::MetadataType::SchemaMetadata)
3953    }
3954
3955    /// Update schema metadata
3956    #[deprecated(note = "Use the new update_schema_metadata(values).replace() instead")]
3957    pub async fn replace_schema_metadata(
3958        &mut self,
3959        new_values: impl IntoIterator<Item = (String, String)>,
3960    ) -> Result<()> {
3961        let new_values = new_values
3962            .into_iter()
3963            .map(|(k, v)| (k, Some(v)))
3964            .collect::<HashMap<_, _>>();
3965        self.update_schema_metadata(new_values).replace().await?;
3966        Ok(())
3967    }
3968
3969    /// Update field metadata
3970    ///
3971    /// ```
3972    /// # use lance::{Dataset, Result};
3973    /// # use lance::dataset::transaction::UpdateMapEntry;
3974    /// # async fn test_update_field_metadata(dataset: &mut Dataset) -> Result<()> {
3975    /// // Update metadata by field path
3976    /// dataset.update_field_metadata()
3977    ///     .update("path.to_field", [("key", "value")])?
3978    ///     .await?;
3979    ///
3980    /// // Update metadata by field id
3981    /// dataset.update_field_metadata()
3982    ///     .update(12, [("key", "value")])?
3983    ///     .await?;
3984    ///
3985    /// // Clear field metadata
3986    /// dataset.update_field_metadata()
3987    ///     .replace("path.to_field", [] as [UpdateMapEntry; 0])?
3988    ///     .replace(12, [] as [UpdateMapEntry; 0])?
3989    ///     .await?;
3990    ///
3991    /// // Replace field metadata
3992    /// dataset.update_field_metadata()
3993    ///     .replace("field_name", [("k1", "v1"), ("k2", "v2")])?
3994    ///     .await?;
3995    /// # Ok(())
3996    /// # }
3997    /// ```
3998    pub fn update_field_metadata(&mut self) -> UpdateFieldMetadataBuilder<'_> {
3999        UpdateFieldMetadataBuilder::new(self)
4000    }
4001
4002    /// Update field metadata
4003    pub async fn replace_field_metadata(
4004        &mut self,
4005        new_values: impl IntoIterator<Item = (u32, HashMap<String, String>)>,
4006    ) -> Result<()> {
4007        let new_values = new_values.into_iter().collect::<HashMap<_, _>>();
4008        let field_metadata_updates = new_values
4009            .into_iter()
4010            .map(|(field_id, metadata)| {
4011                (
4012                    field_id as i32,
4013                    translate_schema_metadata_updates(&metadata),
4014                )
4015            })
4016            .collect();
4017        metadata::execute_metadata_update(
4018            self,
4019            Operation::UpdateConfig {
4020                config_updates: None,
4021                table_metadata_updates: None,
4022                schema_metadata_updates: None,
4023                field_metadata_updates,
4024            },
4025        )
4026        .await
4027    }
4028}
4029
4030#[async_trait::async_trait]
4031impl DatasetTakeRows for Dataset {
4032    fn schema(&self) -> &Schema {
4033        Self::schema(self)
4034    }
4035
4036    async fn take_rows(&self, row_ids: &[u64], projection: &Schema) -> Result<RecordBatch> {
4037        Self::take_rows(self, row_ids, projection.clone()).await
4038    }
4039}
4040
4041#[derive(Debug)]
4042pub(crate) struct ManifestWriteConfig {
4043    auto_set_feature_flags: bool,              // default true
4044    timestamp: Option<SystemTime>,             // default None
4045    use_stable_row_ids: bool,                  // default false
4046    use_legacy_format: Option<bool>,           // default None
4047    storage_format: Option<DataStorageFormat>, // default None
4048    disable_transaction_file: bool,            // default false
4049    /// When `Some`, this commit is the second step of `migrate_to_stable_row_ids`.
4050    /// It bypasses the "cannot enable stable row ids on existing dataset" guard and
4051    /// sets `manifest.next_row_id` to the provided value before activating the flag.
4052    migration_next_row_id: Option<u64>, // default None
4053}
4054
4055impl Default for ManifestWriteConfig {
4056    fn default() -> Self {
4057        Self {
4058            auto_set_feature_flags: true,
4059            timestamp: None,
4060            use_stable_row_ids: false,
4061            disable_transaction_file: false,
4062            use_legacy_format: None,
4063            storage_format: None,
4064            migration_next_row_id: None,
4065        }
4066    }
4067}
4068
4069impl ManifestWriteConfig {
4070    pub fn disable_transaction_file(&self) -> bool {
4071        self.disable_transaction_file
4072    }
4073
4074    #[cfg(test)]
4075    pub(crate) fn with_transaction_file_disabled(mut self) -> Self {
4076        self.disable_transaction_file = true;
4077        self
4078    }
4079
4080    /// Resolve into the config `Transaction::build_manifest` consumes.
4081    ///
4082    /// The timestamp is resolved here rather than during the build so it goes
4083    /// through this crate's mockable `SystemTime`.
4084    pub(crate) fn to_build_config(&self) -> ManifestBuildConfig {
4085        ManifestBuildConfig {
4086            auto_set_feature_flags: self.auto_set_feature_flags,
4087            timestamp_nanos: timestamp_to_nanos(self.timestamp),
4088            use_stable_row_ids: self.use_stable_row_ids,
4089            use_legacy_format: self.use_legacy_format,
4090            storage_format: self.storage_format.clone(),
4091            disable_transaction_file: self.disable_transaction_file,
4092            migration_next_row_id: self.migration_next_row_id,
4093        }
4094    }
4095}
4096
4097/// Commit a manifest file and create a copy at the latest manifest path.
4098#[allow(clippy::too_many_arguments)]
4099pub(crate) async fn write_manifest_file(
4100    object_store: &ObjectStore,
4101    commit_handler: &dyn CommitHandler,
4102    base_path: &Path,
4103    manifest: &mut Manifest,
4104    indices: Option<Vec<IndexMetadata>>,
4105    config: &ManifestWriteConfig,
4106    naming_scheme: ManifestNamingScheme,
4107    transaction: Option<lance_table::format::Transaction>,
4108) -> std::result::Result<ManifestLocation, CommitError> {
4109    if config.auto_set_feature_flags {
4110        // build_manifest may have already set FLAG_STABLE_ROW_IDS on the manifest.
4111        // Preserve it here so this second apply_feature_flags call does not clear it
4112        // when config.use_stable_row_ids is false (the ManifestWriteConfig default).
4113        let use_stable_row_ids = config.use_stable_row_ids || manifest.uses_stable_row_ids();
4114        apply_feature_flags(
4115            manifest,
4116            use_stable_row_ids,
4117            config.disable_transaction_file,
4118        )?;
4119    }
4120
4121    manifest.set_timestamp(timestamp_to_nanos(config.timestamp));
4122
4123    manifest.update_max_fragment_id();
4124
4125    commit_handler
4126        .commit(
4127            manifest,
4128            indices,
4129            base_path,
4130            object_store,
4131            write_manifest_file_to_path,
4132            naming_scheme,
4133            transaction,
4134        )
4135        .await
4136}
4137
4138impl Projectable for Dataset {
4139    fn schema(&self) -> &Schema {
4140        self.schema()
4141    }
4142}
4143
4144#[cfg(test)]
4145mod tests;