Skip to main content

lance_namespace_impls/dir/
manifest.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Manifest-based namespace implementation
5//!
6//! This module provides a namespace implementation that uses a manifest table
7//! to track tables and nested namespaces.
8
9use super::manifest_feature_flags::{ensure_readable, ensure_writable};
10use arrow::array::builder::{ListBuilder, StringBuilder};
11use arrow::array::{Array, ListArray, RecordBatch, RecordBatchIterator, StringArray, UInt64Array};
12use arrow::datatypes::{DataType, Field, Schema as ArrowSchema, SchemaRef};
13use arrow_ipc::reader::StreamReader;
14use async_trait::async_trait;
15use bytes::Bytes;
16use datafusion_common::DataFusionError;
17use datafusion_physical_plan::{
18    SendableRecordBatchStream,
19    stream::RecordBatchStreamAdapter as DatafusionRecordBatchStreamAdapter,
20};
21use futures::{
22    FutureExt, TryStreamExt,
23    stream::{self, StreamExt},
24};
25use lance::dataset::index::LanceIndexStoreExt;
26use lance::dataset::transaction::{Operation, Transaction};
27use lance::dataset::{
28    InsertBuilder, ReadParams, WhenMatched, WriteMode, WriteParams, builder::DatasetBuilder,
29};
30use lance::session::Session;
31use lance::{Dataset, dataset::scanner::Scanner};
32use lance_core::Error as LanceError;
33use lance_core::datatypes::LANCE_UNENFORCED_PRIMARY_KEY_POSITION;
34use lance_core::{Error, ROW_ADDR, ROW_ID, Result, box_error};
35use lance_index::progress::noop_progress;
36use lance_index::registry::IndexPluginRegistry;
37use lance_index::scalar::lance_format::LanceIndexStore;
38use lance_index::scalar::registry::VALUE_COLUMN_NAME;
39use lance_index::scalar::{
40    BuiltinIndexType, CreatedIndex, ScalarIndexParams, index_files_to_table,
41};
42use lance_io::object_store::{ObjectStore, ObjectStoreParams};
43use lance_io::stream::RecordBatchStream as LanceRecordBatchStream;
44use lance_namespace::LanceNamespace;
45use lance_namespace::error::NamespaceError;
46use lance_namespace::models::{
47    AlterTableAddColumnsRequest, AlterTableAddColumnsResponse, AlterTableAlterColumnsRequest,
48    AlterTableAlterColumnsResponse, AlterTableDropColumnsRequest, AlterTableDropColumnsResponse,
49    CreateNamespaceRequest, CreateNamespaceResponse, CreateTableRequest, CreateTableResponse,
50    DeclareTableRequest, DeclareTableResponse, DeregisterTableRequest, DeregisterTableResponse,
51    DescribeNamespaceRequest, DescribeNamespaceResponse, DescribeTableRequest,
52    DescribeTableResponse, DropNamespaceRequest, DropNamespaceResponse, DropTableRequest,
53    DropTableResponse, ListNamespacesRequest, ListNamespacesResponse, ListTablesRequest,
54    ListTablesResponse, NamespaceExistsRequest, RegisterTableRequest, RegisterTableResponse,
55    TableExistsRequest,
56};
57use lance_namespace::schema::arrow_schema_to_json;
58use lance_table::feature_flags::{apply_feature_flags, ensure_can_write_manifest};
59use lance_table::format::{Fragment, IndexMetadata, Manifest};
60use lance_table::io::commit::{
61    CommitError, CommitHandler, commit_handler_from_url, write_manifest_file_to_path,
62};
63use lance_table::transaction::validate_non_reusable_field_id_transition;
64use object_store::{Error as ObjectStoreError, path::Path};
65use roaring::RoaringBitmap;
66use std::io::Cursor;
67use std::time::{SystemTime, UNIX_EPOCH};
68use std::{
69    collections::{BTreeMap, HashMap, HashSet},
70    hash::{DefaultHasher, Hash, Hasher},
71    ops::{Deref, DerefMut},
72    sync::{Arc, LazyLock, Mutex as StdMutex, MutexGuard as StdMutexGuard},
73};
74use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
75use uuid::Uuid;
76
77const MANIFEST_TABLE_NAME: &str = "__manifest";
78const LANCE_DATA_DIR: &str = "data";
79const LANCE_INDICES_DIR: &str = "_indices";
80const DELIMITER: &str = "$";
81/// Bounded concurrency for per-table `_versions/` probes when filtering declared tables.
82/// Higher values reduce latency but increase burst load against the object store.
83pub(crate) const DECLARED_FILTER_CONCURRENCY: usize = 16;
84
85// Index names for the __manifest table
86/// BTREE index on the object_id column for fast lookups
87const OBJECT_ID_INDEX_NAME: &str = "object_id_btree";
88/// Bitmap index on the object_type column for filtering by type
89const OBJECT_TYPE_INDEX_NAME: &str = "object_type_bitmap";
90/// LabelList index on the base_objects column for view dependencies
91const BASE_OBJECTS_INDEX_NAME: &str = "base_objects_label_list";
92/// Value field of the base_objects index, whose nested `List` type would
93/// otherwise allocate an inner field per use.
94static BASE_OBJECTS_VALUE_FIELD: LazyLock<Field> = LazyLock::new(|| {
95    Field::new(
96        VALUE_COLUMN_NAME,
97        DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
98        true,
99    )
100});
101// Each retry reloads and rewrites the full manifest. Match the regular Lance
102// commit retry budget so multi-process namespace writes can make progress.
103const DEFAULT_MANIFEST_REWRITE_COMMIT_RETRIES: u32 = 20;
104const MANIFEST_INDEX_BATCH_SIZE: usize = 8192;
105// Dataset config keys of Lance's automatic old-version cleanup policy. Lance
106// releases before 7.0 wrote this policy into every new dataset by default, so
107// directory manifests created by those releases still carry it.
108const AUTO_CLEANUP_CONFIG_PREFIX: &str = "lance.auto_cleanup.";
109
110fn has_auto_cleanup_config(manifest: &Manifest) -> bool {
111    manifest
112        .config
113        .keys()
114        .any(|key| key.starts_with(AUTO_CLEANUP_CONFIG_PREFIX))
115}
116
117/// Object types that can be stored in the manifest
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum ObjectType {
120    Namespace,
121    Table,
122}
123
124impl ObjectType {
125    pub fn as_str(&self) -> &'static str {
126        match self {
127            Self::Namespace => "namespace",
128            Self::Table => "table",
129        }
130    }
131
132    pub fn parse(s: &str) -> Result<Self> {
133        match s {
134            "namespace" => Ok(Self::Namespace),
135            "table" => Ok(Self::Table),
136            _ => Err(NamespaceError::Internal {
137                message: format!("Invalid object type: {}", s),
138            }
139            .into()),
140        }
141    }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145enum CreateTableMode {
146    Create,
147    ExistOk,
148    Overwrite,
149}
150
151impl CreateTableMode {
152    fn parse(mode: Option<&str>) -> Result<Self> {
153        match mode {
154            None => Ok(Self::Create),
155            Some(mode) if mode.eq_ignore_ascii_case("create") => Ok(Self::Create),
156            Some(mode)
157                if mode.eq_ignore_ascii_case("existok")
158                    || mode.eq_ignore_ascii_case("exist_ok") =>
159            {
160                Ok(Self::ExistOk)
161            }
162            Some(mode) if mode.eq_ignore_ascii_case("overwrite") => Ok(Self::Overwrite),
163            Some(mode) => Err(NamespaceError::InvalidInput {
164                message: format!(
165                    "Unsupported create_table mode '{}'. Supported modes are: 'Create', 'ExistOk', 'Overwrite'",
166                    mode
167                ),
168            }
169            .into()),
170        }
171    }
172
173    fn write_mode(self) -> WriteMode {
174        match self {
175            Self::Overwrite => WriteMode::Overwrite,
176            Self::Create | Self::ExistOk => WriteMode::Create,
177        }
178    }
179}
180
181/// Information about a table stored in the manifest
182#[derive(Debug, Clone)]
183pub struct TableInfo {
184    pub namespace: Vec<String>,
185    pub name: String,
186    pub location: String,
187    pub metadata: Option<HashMap<String, String>>,
188}
189
190/// An entry to be inserted into the manifest table.
191///
192/// This struct makes the meaning of each field explicit, replacing the
193/// previous tuple-based API `(String, ObjectType, Option<String>, Option<String>)`.
194#[derive(Debug, Clone)]
195pub struct ManifestEntry {
196    /// The unique object identifier (e.g., table name or version object_id)
197    pub object_id: String,
198    /// The type of the object (Namespace or Table)
199    pub object_type: ObjectType,
200    /// The storage location (e.g., directory name for tables)
201    pub location: Option<String>,
202    /// Additional metadata serialized as JSON
203    pub metadata: Option<String>,
204}
205
206struct CopyOnWriteMutation<T> {
207    result: T,
208    has_changes: bool,
209}
210
211impl<T> CopyOnWriteMutation<T> {
212    fn updated(result: T) -> Self {
213        Self {
214            result,
215            has_changes: true,
216        }
217    }
218
219    fn unchanged(result: T) -> Self {
220        Self {
221            result,
222            has_changes: false,
223        }
224    }
225}
226
227struct ManifestIndexBuildInput {
228    index_name: &'static str,
229    column_name: &'static str,
230    params: ScalarIndexParams,
231    field: Field,
232    stream: SendableRecordBatchStream,
233}
234
235struct ManifestTrainedIndex {
236    index_name: &'static str,
237    column_name: &'static str,
238    uuid: Uuid,
239    created_index: CreatedIndex,
240}
241
242struct ManifestRowValue {
243    object_id: String,
244    object_type: ObjectType,
245    location: Option<String>,
246    metadata: Option<String>,
247    base_objects: Option<Vec<String>>,
248}
249
250struct ManifestOutputRow<'a> {
251    object_id: &'a str,
252    object_type: ObjectType,
253    location: Option<&'a str>,
254    metadata: Option<&'a str>,
255    base_objects: Option<&'a [String]>,
256}
257
258#[derive(Default)]
259struct ManifestIndexAccumulator {
260    object_ids: BTreeMap<Arc<str>, u64>,
261    object_types: BTreeMap<&'static str, RoaringBitmap>,
262    base_objects_values: Vec<Option<Vec<String>>>,
263    base_objects_row_ids: Vec<u64>,
264    row_count: u64,
265}
266
267impl ManifestIndexAccumulator {
268    fn next_row_id(&self) -> Result<u64> {
269        if self.row_count >= u64::from(u32::MAX) {
270            return Err(NamespaceError::Internal {
271                message: format!(
272                    "Manifest rewrite exceeded maximum single-fragment row count: {}",
273                    self.row_count
274                ),
275            }
276            .into());
277        }
278        Ok(self.row_count)
279    }
280
281    fn push(&mut self, row: &ManifestOutputRow<'_>) -> Result<u64> {
282        let row_id = self.next_row_id()?;
283        if self
284            .object_ids
285            .insert(Arc::<str>::from(row.object_id), row_id)
286            .is_some()
287        {
288            return Err(NamespaceError::Internal {
289                message: format!("Manifest contains duplicate object_id '{}'", row.object_id),
290            }
291            .into());
292        }
293        self.object_types
294            .entry(row.object_type.as_str())
295            .or_default()
296            .insert(row_id as u32);
297        self.base_objects_values
298            .push(row.base_objects.map(|objects| objects.to_vec()));
299        self.base_objects_row_ids.push(row_id);
300        self.row_count += 1;
301        Ok(row_id)
302    }
303}
304
305struct ManifestBatchBuilder {
306    object_ids: Vec<String>,
307    object_types: Vec<&'static str>,
308    locations: Vec<Option<String>>,
309    metadatas: Vec<Option<String>>,
310    base_objects: Vec<Option<Vec<String>>>,
311}
312
313impl ManifestBatchBuilder {
314    fn new() -> Self {
315        Self {
316            object_ids: Vec::new(),
317            object_types: Vec::new(),
318            locations: Vec::new(),
319            metadatas: Vec::new(),
320            base_objects: Vec::new(),
321        }
322    }
323
324    fn is_empty(&self) -> bool {
325        self.object_ids.is_empty()
326    }
327
328    fn append(
329        &mut self,
330        index_data: &mut ManifestIndexAccumulator,
331        row: ManifestOutputRow<'_>,
332    ) -> Result<()> {
333        index_data.push(&row)?;
334        self.object_ids.push(row.object_id.to_string());
335        self.object_types.push(row.object_type.as_str());
336        self.locations.push(row.location.map(ToString::to_string));
337        self.metadatas.push(row.metadata.map(ToString::to_string));
338        self.base_objects
339            .push(row.base_objects.map(|objects| objects.to_vec()));
340        Ok(())
341    }
342
343    fn finish(self) -> Result<RecordBatch> {
344        let base_objects_array = ManifestNamespace::base_objects_array(&self.base_objects);
345        RecordBatch::try_new(
346            ManifestNamespace::manifest_schema(),
347            vec![
348                Arc::new(StringArray::from(self.object_ids)),
349                Arc::new(StringArray::from(self.object_types)),
350                Arc::new(StringArray::from(self.locations)),
351                Arc::new(StringArray::from(self.metadatas)),
352                Arc::new(base_objects_array),
353            ],
354        )
355        .map_err(|e| {
356            lance_core::Error::from(NamespaceError::Internal {
357                message: format!("Failed to create manifest snapshot batch: {:?}", e),
358            })
359        })
360    }
361}
362
363/// How to resolve a storage commit conflict (or an ambiguous commit error that did
364/// not land) against the latest catalog state, without re-staging the full rewrite.
365enum ConflictResolution<O> {
366    /// Re-read the latest manifest and re-apply the mutation (upserts, version-range
367    /// deletes). The staged data/index files are discarded and a new rewrite is attempted.
368    Retry,
369    /// Creating these object ids with fail-on-conflict semantics. If any of them now
370    /// exists in the latest manifest, the create lost the race and must fail with a
371    /// concurrent-modification error; otherwise retry the rewrite.
372    FailIfExists(Vec<String>),
373    /// Deleting `object_id`. If it is already absent from the latest manifest the delete
374    /// has effectively happened, so return `output` as success; otherwise retry.
375    SucceedIfAbsent { object_id: String, output: O },
376}
377
378trait ManifestStreamMutation: Send {
379    type Output: Clone + Send + 'static;
380
381    fn process_existing_row(
382        &mut self,
383        row: ManifestRowValue,
384        output: &mut ManifestBatchBuilder,
385        index_data: &mut ManifestIndexAccumulator,
386    ) -> Result<()>;
387
388    fn append_rows(
389        &mut self,
390        output: &mut ManifestBatchBuilder,
391        index_data: &mut ManifestIndexAccumulator,
392    ) -> Result<()>;
393
394    fn finish(&self) -> CopyOnWriteMutation<Self::Output>;
395
396    /// Declares how a storage commit conflict should be resolved against the latest
397    /// committed catalog state. Defaults to re-reading and re-applying.
398    fn conflict_resolution(&self) -> ConflictResolution<Self::Output> {
399        ConflictResolution::Retry
400    }
401}
402
403struct ManifestRewriteShared<M: ManifestStreamMutation> {
404    mutation: M,
405    index_data: Option<ManifestIndexAccumulator>,
406    result: Option<CopyOnWriteMutation<M::Output>>,
407    error: Option<LanceError>,
408}
409
410impl<M: ManifestStreamMutation> ManifestRewriteShared<M> {
411    fn new(mutation: M) -> Self {
412        Self {
413            mutation,
414            index_data: Some(ManifestIndexAccumulator::default()),
415            result: None,
416            error: None,
417        }
418    }
419}
420
421struct UpsertManifestMutation {
422    entries: Vec<ManifestEntry>,
423    base_objects: Vec<Option<Vec<String>>>,
424    entry_positions: HashMap<String, usize>,
425    matched: Vec<bool>,
426    when_matched: WhenMatched,
427}
428
429impl UpsertManifestMutation {
430    fn new(
431        entries: Vec<ManifestEntry>,
432        base_objects: Option<Vec<String>>,
433        when_matched: WhenMatched,
434    ) -> Self {
435        let entry_positions = entries
436            .iter()
437            .enumerate()
438            .map(|(index, entry)| (entry.object_id.clone(), index))
439            .collect();
440        let matched = vec![false; entries.len()];
441        let mut entry_base_objects = vec![None; entries.len()];
442        if !entry_base_objects.is_empty() {
443            entry_base_objects[0] = base_objects;
444        }
445        Self {
446            entries,
447            base_objects: entry_base_objects,
448            entry_positions,
449            matched,
450            when_matched,
451        }
452    }
453
454    fn entry_row(&self, index: usize) -> ManifestOutputRow<'_> {
455        let entry = &self.entries[index];
456        ManifestOutputRow {
457            object_id: &entry.object_id,
458            object_type: entry.object_type,
459            location: entry.location.as_deref(),
460            metadata: entry.metadata.as_deref(),
461            base_objects: self.base_objects[index].as_deref(),
462        }
463    }
464}
465
466impl ManifestStreamMutation for UpsertManifestMutation {
467    type Output = ();
468
469    fn process_existing_row(
470        &mut self,
471        row: ManifestRowValue,
472        output: &mut ManifestBatchBuilder,
473        index_data: &mut ManifestIndexAccumulator,
474    ) -> Result<()> {
475        if let Some(index) = self.entry_positions.get(&row.object_id).copied() {
476            match self.when_matched {
477                WhenMatched::Fail => {
478                    return Err(NamespaceError::ConcurrentModification {
479                        message: format!(
480                            "Object '{}' was concurrently created by another operation",
481                            row.object_id
482                        ),
483                    }
484                    .into());
485                }
486                WhenMatched::UpdateAll => {
487                    self.matched[index] = true;
488                    output.append(index_data, self.entry_row(index))?;
489                    return Ok(());
490                }
491                _ => {
492                    return Err(NamespaceError::Internal {
493                        message: format!(
494                            "Unsupported manifest rewrite matched action: {:?}",
495                            self.when_matched
496                        ),
497                    }
498                    .into());
499                }
500            }
501        }
502
503        output.append(
504            index_data,
505            ManifestOutputRow {
506                object_id: &row.object_id,
507                object_type: row.object_type,
508                location: row.location.as_deref(),
509                metadata: row.metadata.as_deref(),
510                base_objects: row.base_objects.as_deref(),
511            },
512        )
513    }
514
515    fn append_rows(
516        &mut self,
517        output: &mut ManifestBatchBuilder,
518        index_data: &mut ManifestIndexAccumulator,
519    ) -> Result<()> {
520        for index in 0..self.entries.len() {
521            if !self.matched[index] {
522                output.append(index_data, self.entry_row(index))?;
523            }
524        }
525        Ok(())
526    }
527
528    fn finish(&self) -> CopyOnWriteMutation<Self::Output> {
529        CopyOnWriteMutation::updated(())
530    }
531
532    fn conflict_resolution(&self) -> ConflictResolution<Self::Output> {
533        match self.when_matched {
534            // Fail-on-conflict create: a concurrent writer may have created one of these
535            // ids. Re-applying would still fail, so check directly instead of re-staging.
536            WhenMatched::Fail => ConflictResolution::FailIfExists(
537                self.entries.iter().map(|e| e.object_id.clone()).collect(),
538            ),
539            // Metadata upsert is last-writer-wins: re-read and re-apply.
540            _ => ConflictResolution::Retry,
541        }
542    }
543}
544
545struct DeleteObjectMutation {
546    object_id: String,
547    deleted: bool,
548}
549
550impl ManifestStreamMutation for DeleteObjectMutation {
551    type Output = ();
552
553    fn process_existing_row(
554        &mut self,
555        row: ManifestRowValue,
556        output: &mut ManifestBatchBuilder,
557        index_data: &mut ManifestIndexAccumulator,
558    ) -> Result<()> {
559        if row.object_id == self.object_id {
560            self.deleted = true;
561            return Ok(());
562        }
563
564        output.append(
565            index_data,
566            ManifestOutputRow {
567                object_id: &row.object_id,
568                object_type: row.object_type,
569                location: row.location.as_deref(),
570                metadata: row.metadata.as_deref(),
571                base_objects: row.base_objects.as_deref(),
572            },
573        )
574    }
575
576    fn append_rows(
577        &mut self,
578        _output: &mut ManifestBatchBuilder,
579        _index_data: &mut ManifestIndexAccumulator,
580    ) -> Result<()> {
581        Ok(())
582    }
583
584    fn finish(&self) -> CopyOnWriteMutation<Self::Output> {
585        if self.deleted {
586            CopyOnWriteMutation::updated(())
587        } else {
588            CopyOnWriteMutation::unchanged(())
589        }
590    }
591
592    fn conflict_resolution(&self) -> ConflictResolution<Self::Output> {
593        // If a concurrent writer already removed the object, the delete is satisfied.
594        ConflictResolution::SucceedIfAbsent {
595            object_id: self.object_id.clone(),
596            output: (),
597        }
598    }
599}
600
601/// Information about a namespace stored in the manifest
602#[derive(Debug, Clone)]
603pub struct NamespaceInfo {
604    pub namespace: Vec<String>,
605    pub name: String,
606    pub metadata: Option<HashMap<String, String>>,
607}
608
609/// A wrapper around a Dataset that provides concurrent access.
610///
611/// This can be cloned cheaply. It supports concurrent reads or exclusive writes.
612///
613/// Manifest rewrites never run old-version cleanup or add an auto-cleanup
614/// policy. Policy-free manifests therefore keep contiguous attached versions,
615/// so reloads only need to probe the immediate successor manifest. A manifest
616/// created by an older Lance release can still carry an auto-cleanup policy,
617/// and older writers act on it by removing old versions, which leaves gaps
618/// after the held version. While that version carries the policy, reloads
619/// resolve the latest version instead.
620#[derive(Debug, Clone)]
621pub struct DatasetConsistencyWrapper(Arc<RwLock<Dataset>>);
622
623impl DatasetConsistencyWrapper {
624    /// Create a new wrapper with the given dataset.
625    pub fn new(dataset: Dataset) -> Self {
626        Self(Arc::new(RwLock::new(dataset)))
627    }
628
629    /// Get an immutable reference to the dataset.
630    /// Always reloads to ensure strong consistency.
631    pub async fn get(&self) -> Result<DatasetReadGuard<'_>> {
632        self.reload().await?;
633        let guard = DatasetReadGuard {
634            guard: self.0.read().await,
635        };
636        // Refuse manifests written with a reader feature flag this build does
637        // not understand instead of misreading them.
638        ensure_readable(guard.metadata())?;
639        Ok(guard)
640    }
641
642    /// Reload the dataset and return a reference.
643    pub async fn get_refreshed(&self) -> Result<DatasetReadGuard<'_>> {
644        self.reload().await?;
645        let guard = DatasetReadGuard {
646            guard: self.0.read().await,
647        };
648        ensure_readable(guard.metadata())?;
649        Ok(guard)
650    }
651
652    /// Get a mutable reference to the dataset.
653    /// Always reloads to ensure strong consistency.
654    ///
655    /// Acquires the write lock before reloading so that tokio's write-fairness
656    /// prevents reader starvation of the writer.
657    pub async fn get_mut(&self) -> Result<DatasetWriteGuard<'_>> {
658        let mut write_guard = self.0.write().await;
659        Self::reload_under_write_lock(&mut write_guard).await?;
660        let guard = DatasetWriteGuard { guard: write_guard };
661        ensure_readable(guard.metadata())?;
662        ensure_writable(guard.metadata())?;
663        Ok(guard)
664    }
665
666    /// Provide a known latest version of the dataset.
667    ///
668    /// This is usually done after some write operation, which inherently will
669    /// have the latest version.
670    pub async fn set_latest(&self, dataset: Dataset) {
671        let mut write_guard = self.0.write().await;
672        if dataset.manifest().version > write_guard.manifest().version {
673            *write_guard = dataset;
674        }
675    }
676
677    /// Reload the dataset to the latest version (for the read path).
678    ///
679    /// Takes a read lock first to check if a reload is needed, then upgrades
680    /// to a write lock only if necessary.
681    async fn reload(&self) -> Result<()> {
682        // First check if we need to reload (with read lock)
683        let read_guard = self.0.read().await;
684        let dataset_uri = read_guard.uri().to_string();
685        let current_version = read_guard.version().version;
686        log::debug!(
687            "Reload starting for uri={}, current_version={}",
688            dataset_uri,
689            current_version
690        );
691        let has_newer_version = Self::has_newer_version(&read_guard).await?;
692        log::debug!(
693            "Reload checked has_newer_version={} for uri={}, current_version={}",
694            has_newer_version,
695            dataset_uri,
696            current_version
697        );
698        drop(read_guard);
699
700        // If already up-to-date, return early
701        if !has_newer_version {
702            log::debug!("Already up-to-date for uri={}", dataset_uri);
703            return Ok(());
704        }
705
706        // Need to reload, acquire write lock
707        let mut write_guard = self.0.write().await;
708        Self::reload_under_write_lock(&mut write_guard).await
709    }
710
711    /// Reload the dataset while already holding the write lock.
712    async fn reload_under_write_lock(
713        dataset: &mut tokio::sync::RwLockWriteGuard<'_, Dataset>,
714    ) -> Result<()> {
715        let dataset_uri = dataset.uri().to_string();
716        let current_version = dataset.version().version;
717        log::debug!(
718            "Reload (under write lock) for uri={}, current_version={}",
719            dataset_uri,
720            current_version
721        );
722
723        if Self::has_newer_version(dataset).await? {
724            dataset.checkout_latest().await.map_err(|e| {
725                lance_core::Error::from(NamespaceError::Internal {
726                    message: format!("Failed to checkout latest: {:?}", e),
727                })
728            })?;
729        }
730
731        Ok(())
732    }
733
734    /// Return whether a version newer than the held one has been committed.
735    ///
736    /// Probing the immediate successor avoids resolving the latest manifest on
737    /// every namespace read, but misses newer versions once cleanup has removed
738    /// the successor. A held version with an auto-cleanup policy can have such
739    /// gaps after it, so it resolves the latest version instead.
740    async fn has_newer_version(dataset: &Dataset) -> Result<bool> {
741        let has_newer_version = if has_auto_cleanup_config(dataset.manifest()) {
742            dataset.is_stale().await
743        } else {
744            dataset.has_successor_version().await
745        };
746        has_newer_version.map_err(|e| {
747            lance_core::Error::from(NamespaceError::Internal {
748                message: format!("Failed to check dataset staleness: {:?}", e),
749            })
750        })
751    }
752}
753
754pub struct DatasetReadGuard<'a> {
755    guard: RwLockReadGuard<'a, Dataset>,
756}
757
758impl Deref for DatasetReadGuard<'_> {
759    type Target = Dataset;
760
761    fn deref(&self) -> &Self::Target {
762        &self.guard
763    }
764}
765
766pub struct DatasetWriteGuard<'a> {
767    guard: RwLockWriteGuard<'a, Dataset>,
768}
769
770impl Deref for DatasetWriteGuard<'_> {
771    type Target = Dataset;
772
773    fn deref(&self) -> &Self::Target {
774        &self.guard
775    }
776}
777
778impl DerefMut for DatasetWriteGuard<'_> {
779    fn deref_mut(&mut self) -> &mut Self::Target {
780        &mut self.guard
781    }
782}
783
784/// Manifest-based namespace implementation
785///
786/// Uses a special `__manifest` Lance table to track tables and nested namespaces.
787pub struct ManifestNamespace {
788    root: String,
789    storage_options: Option<HashMap<String, String>>,
790    session: Option<Arc<Session>>,
791    object_store: Arc<ObjectStore>,
792    base_path: Path,
793    manifest_dataset: DatasetConsistencyWrapper,
794    /// Whether directory listing is enabled in dual mode
795    /// If true, root namespace tables use {table_name}.lance naming
796    /// If false, they use namespace-prefixed names
797    dir_listing_enabled: bool,
798    /// Whether copy-on-write manifest rewrites should build replacement indices.
799    /// Defaults to true.
800    inline_optimization_enabled: bool,
801    /// Number of retries for commit operations on the manifest table.
802    /// If None, defaults to [`lance_table::io::commit::CommitConfig`] default (20).
803    commit_retries: Option<u32>,
804    /// Serialize manifest mutations within a single namespace instance so concurrent
805    /// create/drop calls do not compete with each other on the same in-memory snapshot.
806    manifest_mutation_lock: Arc<Mutex<()>>,
807}
808
809impl std::fmt::Debug for ManifestNamespace {
810    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
811        f.debug_struct("ManifestNamespace")
812            .field("root", &self.root)
813            .field("storage_options", &self.storage_options)
814            .field("dir_listing_enabled", &self.dir_listing_enabled)
815            .field(
816                "inline_optimization_enabled",
817                &self.inline_optimization_enabled,
818            )
819            .finish()
820    }
821}
822
823/// Convert a Lance commit error to an appropriate namespace error.
824///
825/// Maps lance commit errors to namespace errors:
826/// - `CommitConflict`: version collision retries exhausted -> Throttling (safe to retry)
827/// - `TooMuchWriteContention`: RetryableCommitConflict (semantic conflict) retries exhausted -> ConcurrentModification
828/// - `IncompatibleTransaction`: incompatible concurrent change -> ConcurrentModification
829/// - Errors containing "matched/duplicate/already exists": ConcurrentModification (from WhenMatched::Fail)
830/// - Other errors: IO error with the operation description
831fn convert_lance_commit_error(e: &LanceError, operation: &str, object_id: Option<&str>) -> Error {
832    match e {
833        // CommitConflict: version collision retries exhausted -> Throttling (safe to retry)
834        LanceError::CommitConflict { .. } => NamespaceError::Throttling {
835            message: format!("Too many concurrent writes, please retry later: {:?}", e),
836        }
837        .into(),
838        // TooMuchWriteContention: RetryableCommitConflict (semantic conflict) retries exhausted -> ConcurrentModification
839        // IncompatibleTransaction: incompatible concurrent change -> ConcurrentModification
840        LanceError::TooMuchWriteContention { .. } | LanceError::IncompatibleTransaction { .. } => {
841            let message = if let Some(id) = object_id {
842                format!(
843                    "Object '{}' was concurrently modified by another operation: {:?}",
844                    id, e
845                )
846            } else {
847                format!(
848                    "Object was concurrently modified by another operation: {:?}",
849                    e
850                )
851            };
852            NamespaceError::ConcurrentModification { message }.into()
853        }
854        // Other errors: check message for semantic conflicts (matched/duplicate from WhenMatched::Fail)
855        _ => {
856            let error_msg = e.to_string();
857            if error_msg.contains("matched")
858                || error_msg.contains("duplicate")
859                || error_msg.contains("already exists")
860            {
861                let message = if let Some(id) = object_id {
862                    format!(
863                        "Object '{}' was concurrently created by another operation: {:?}",
864                        id, e
865                    )
866                } else {
867                    format!(
868                        "Object was concurrently created by another operation: {:?}",
869                        e
870                    )
871                };
872                return NamespaceError::ConcurrentModification { message }.into();
873            }
874            lance_core::Error::from(NamespaceError::Internal {
875                message: format!("{}: {:?}", operation, e),
876            })
877        }
878    }
879}
880
881impl ManifestNamespace {
882    /// Create a new ManifestNamespace from an existing DirectoryNamespace
883    #[allow(clippy::too_many_arguments)]
884    pub async fn from_directory(
885        root: String,
886        storage_options: Option<HashMap<String, String>>,
887        session: Option<Arc<Session>>,
888        object_store: Arc<ObjectStore>,
889        base_path: Path,
890        dir_listing_enabled: bool,
891        inline_optimization_enabled: bool,
892        commit_retries: Option<u32>,
893    ) -> Result<Self> {
894        let manifest_dataset =
895            Self::ensure_manifest_table_up_to_date(&root, &storage_options, session.clone())
896                .await?;
897
898        Ok(Self::new(
899            root,
900            storage_options,
901            session,
902            object_store,
903            base_path,
904            manifest_dataset,
905            dir_listing_enabled,
906            inline_optimization_enabled,
907            commit_retries,
908        ))
909    }
910
911    /// Open an existing manifest dataset without creating or migrating it.
912    #[allow(clippy::too_many_arguments)]
913    pub async fn open_from_directory(
914        root: String,
915        storage_options: Option<HashMap<String, String>>,
916        session: Option<Arc<Session>>,
917        object_store: Arc<ObjectStore>,
918        base_path: Path,
919        dir_listing_enabled: bool,
920        inline_optimization_enabled: bool,
921        commit_retries: Option<u32>,
922    ) -> Result<Self> {
923        let manifest_dataset =
924            Self::open_manifest_table(&root, &storage_options, session.clone()).await?;
925
926        Ok(Self::new(
927            root,
928            storage_options,
929            session,
930            object_store,
931            base_path,
932            manifest_dataset,
933            dir_listing_enabled,
934            inline_optimization_enabled,
935            commit_retries,
936        ))
937    }
938
939    #[allow(clippy::too_many_arguments)]
940    fn new(
941        root: String,
942        storage_options: Option<HashMap<String, String>>,
943        session: Option<Arc<Session>>,
944        object_store: Arc<ObjectStore>,
945        base_path: Path,
946        manifest_dataset: DatasetConsistencyWrapper,
947        dir_listing_enabled: bool,
948        inline_optimization_enabled: bool,
949        commit_retries: Option<u32>,
950    ) -> Self {
951        Self {
952            root,
953            storage_options,
954            session,
955            object_store,
956            base_path,
957            manifest_dataset,
958            dir_listing_enabled,
959            inline_optimization_enabled,
960            commit_retries,
961            manifest_mutation_lock: Arc::new(Mutex::new(())),
962        }
963    }
964
965    /// Build object ID from namespace path and name
966    pub fn build_object_id(namespace: &[String], name: &str) -> String {
967        if namespace.is_empty() {
968            name.to_string()
969        } else {
970            let mut id = namespace.join(DELIMITER);
971            id.push_str(DELIMITER);
972            id.push_str(name);
973            id
974        }
975    }
976
977    /// Parse object ID into namespace path and name
978    pub fn parse_object_id(object_id: &str) -> (Vec<String>, String) {
979        let parts: Vec<&str> = object_id.split(DELIMITER).collect();
980        if parts.len() == 1 {
981            (Vec::new(), parts[0].to_string())
982        } else {
983            let namespace = parts[..parts.len() - 1]
984                .iter()
985                .map(|s| s.to_string())
986                .collect();
987            let name = parts[parts.len() - 1].to_string();
988            (namespace, name)
989        }
990    }
991
992    /// Split an object ID (vec of strings) into namespace and table name
993    pub fn split_object_id(object_id: &[String]) -> (Vec<String>, String) {
994        if object_id.len() == 1 {
995            (vec![], object_id[0].clone())
996        } else {
997            (
998                object_id[..object_id.len() - 1].to_vec(),
999                object_id[object_id.len() - 1].clone(),
1000            )
1001        }
1002    }
1003
1004    /// Convert an ID (vec of strings) to an object_id string
1005    pub fn str_object_id(object_id: &[String]) -> String {
1006        object_id.join(DELIMITER)
1007    }
1008
1009    fn format_table_id(table_id: &[String]) -> String {
1010        format!("table id '{}'", Self::str_object_id(table_id))
1011    }
1012
1013    /// Generate a new directory name in format: `<hash>_<object_id>`
1014    /// The hash is used to (1) optimize object store throughput,
1015    /// (2) have high enough entropy in a short period of time to prevent issues like
1016    /// failed table creation, delete and create new table of the same name, etc.
1017    /// The object_id is added after the hash to ensure
1018    /// dir name uniqueness and make debugging easier.
1019    pub fn generate_dir_name(object_id: &str) -> String {
1020        // Generate a random number for uniqueness
1021        let random_num: u64 = rand::random();
1022
1023        // Create hash from random number + object_id
1024        let mut hasher = DefaultHasher::new();
1025        random_num.hash(&mut hasher);
1026        object_id.hash(&mut hasher);
1027        let hash = hasher.finish();
1028
1029        // Format as lowercase hex (8 characters - sufficient entropy for uniqueness)
1030        format!("{:08x}_{}", (hash & 0xFFFFFFFF) as u32, object_id)
1031    }
1032
1033    /// Construct a full URI from root and relative location
1034    pub(crate) fn construct_full_uri(root: &str, relative_location: &str) -> Result<String> {
1035        let mut base_url = lance_io::object_store::uri_to_url(root)?;
1036
1037        // Ensure the base URL has a trailing slash so that path segment mutation
1038        // appends rather than replaces the last path segment.
1039        // Without this fix, appending "table.lance" to "s3://bucket/path/subdir"
1040        // would incorrectly produce "s3://bucket/path/table.lance" (missing subdir).
1041        if !base_url.path().ends_with('/') {
1042            base_url.set_path(&format!("{}/", base_url.path()));
1043        }
1044
1045        let mut full_url = base_url.clone();
1046        full_url
1047            .path_segments_mut()
1048            .map_err(|_| {
1049                lance_core::Error::from(NamespaceError::InvalidInput {
1050                    message: format!("Cannot modify path segments for URI '{}'", root),
1051                })
1052            })?
1053            .pop_if_empty()
1054            .extend(
1055                relative_location
1056                    .split('/')
1057                    .filter(|segment| !segment.is_empty()),
1058            );
1059
1060        // Clear any query string to avoid trailing "?" in the URL.
1061        // Use set_query(None) instead of set_query("") because the latter
1062        // would still add a trailing '?' to the URL when serialized.
1063        full_url.set_query(None);
1064
1065        Ok(full_url.to_string())
1066    }
1067
1068    fn string_list_array(values: &[Option<Vec<String>>], child_name: &str) -> ListArray {
1069        let string_builder = StringBuilder::new();
1070        let mut list_builder = ListBuilder::new(string_builder).with_field(Arc::new(Field::new(
1071            child_name,
1072            DataType::Utf8,
1073            true,
1074        )));
1075        for value in values {
1076            match value {
1077                Some(objects) => {
1078                    for object in objects {
1079                        list_builder.values().append_value(object);
1080                    }
1081                    list_builder.append(true);
1082                }
1083                None => list_builder.append_null(),
1084            }
1085        }
1086        list_builder.finish()
1087    }
1088
1089    fn base_objects_array(values: &[Option<Vec<String>>]) -> ListArray {
1090        Self::string_list_array(values, "object_id")
1091    }
1092
1093    fn value_row_id_schema(value_field: Field) -> SchemaRef {
1094        Arc::new(ArrowSchema::new(vec![
1095            value_field,
1096            Field::new(ROW_ID, DataType::UInt64, false),
1097        ]))
1098    }
1099
1100    /// Like [`Self::value_row_id_schema`], but for a BTree stream: BTree
1101    /// training always reads its id column as `ROW_ADDR`, never `ROW_ID` (see
1102    /// `BTREE_ROW_ADDR_DOMAIN_VERSION`). The counter these streams number
1103    /// rows with is already an address, not a row id -- see the single
1104    /// fragment id 0 invariant in `rewrite_manifest` -- so only the column
1105    /// name needs to change, not the values.
1106    fn value_row_addr_schema(value_field: Field) -> SchemaRef {
1107        Arc::new(ArrowSchema::new(vec![
1108            value_field,
1109            Field::new(ROW_ADDR, DataType::UInt64, false),
1110        ]))
1111    }
1112
1113    fn string_row_id_batch(
1114        schema: SchemaRef,
1115        values: Vec<String>,
1116        row_ids: Vec<u64>,
1117    ) -> Result<RecordBatch> {
1118        RecordBatch::try_new(
1119            schema,
1120            vec![
1121                Arc::new(StringArray::from(values)),
1122                Arc::new(UInt64Array::from(row_ids)),
1123            ],
1124        )
1125        .map_err(Into::into)
1126    }
1127
1128    fn list_row_id_batch(
1129        schema: SchemaRef,
1130        values: Vec<Option<Vec<String>>>,
1131        row_ids: Vec<u64>,
1132    ) -> Result<RecordBatch> {
1133        RecordBatch::try_new(
1134            schema,
1135            vec![
1136                Arc::new(Self::string_list_array(&values, "item")),
1137                Arc::new(UInt64Array::from(row_ids)),
1138            ],
1139        )
1140        .map_err(Into::into)
1141    }
1142
1143    fn object_id_index_stream(object_ids: BTreeMap<Arc<str>, u64>) -> SendableRecordBatchStream {
1144        let schema =
1145            Self::value_row_addr_schema(Field::new(VALUE_COLUMN_NAME, DataType::Utf8, false));
1146        let stream_schema = schema.clone();
1147        let stream = stream::unfold(
1148            (object_ids.into_iter(), false, schema),
1149            |(mut iter, emitted, schema)| async move {
1150                let mut values = Vec::with_capacity(MANIFEST_INDEX_BATCH_SIZE);
1151                let mut row_ids = Vec::with_capacity(MANIFEST_INDEX_BATCH_SIZE);
1152                for _ in 0..MANIFEST_INDEX_BATCH_SIZE {
1153                    let Some((value, row_id)) = iter.next() else {
1154                        break;
1155                    };
1156                    values.push(value.to_string());
1157                    row_ids.push(row_id);
1158                }
1159                if values.is_empty() {
1160                    if emitted {
1161                        None
1162                    } else {
1163                        let batch = Self::string_row_id_batch(schema.clone(), values, row_ids)
1164                            .map_err(|err| DataFusionError::External(Box::new(err)));
1165                        Some((batch, (iter, true, schema)))
1166                    }
1167                } else {
1168                    let batch = Self::string_row_id_batch(schema.clone(), values, row_ids)
1169                        .map_err(|err| DataFusionError::External(Box::new(err)));
1170                    Some((batch, (iter, true, schema)))
1171                }
1172            },
1173        );
1174        Box::pin(DatafusionRecordBatchStreamAdapter::new(
1175            stream_schema,
1176            stream.fuse(),
1177        ))
1178    }
1179
1180    fn object_type_index_stream(
1181        object_types: BTreeMap<&'static str, RoaringBitmap>,
1182    ) -> SendableRecordBatchStream {
1183        let schema =
1184            Self::value_row_id_schema(Field::new(VALUE_COLUMN_NAME, DataType::Utf8, false));
1185        let stream_schema = schema.clone();
1186        let entries = object_types
1187            .into_iter()
1188            .map(|(value, bitmap)| {
1189                (
1190                    value,
1191                    Box::new(bitmap.into_iter()) as Box<dyn Iterator<Item = u32> + Send>,
1192                )
1193            })
1194            .collect::<Vec<_>>()
1195            .into_iter();
1196        let stream = stream::unfold(
1197            (entries, None, false, schema),
1198            |(mut entries, mut current, emitted, schema)| async move {
1199                let mut values = Vec::with_capacity(MANIFEST_INDEX_BATCH_SIZE);
1200                let mut row_ids = Vec::with_capacity(MANIFEST_INDEX_BATCH_SIZE);
1201                while values.len() < MANIFEST_INDEX_BATCH_SIZE {
1202                    if current.is_none() {
1203                        current = entries.next();
1204                    }
1205                    let Some((value, iter)) = current.as_mut() else {
1206                        break;
1207                    };
1208                    if let Some(row_id) = iter.next() {
1209                        values.push((*value).to_string());
1210                        row_ids.push(u64::from(row_id));
1211                    } else {
1212                        current = None;
1213                    }
1214                }
1215
1216                if values.is_empty() {
1217                    if emitted {
1218                        None
1219                    } else {
1220                        let batch = Self::string_row_id_batch(schema.clone(), values, row_ids)
1221                            .map_err(|err| DataFusionError::External(Box::new(err)));
1222                        Some((batch, (entries, current, true, schema)))
1223                    }
1224                } else {
1225                    let batch = Self::string_row_id_batch(schema.clone(), values, row_ids)
1226                        .map_err(|err| DataFusionError::External(Box::new(err)));
1227                    Some((batch, (entries, current, true, schema)))
1228                }
1229            },
1230        );
1231        Box::pin(DatafusionRecordBatchStreamAdapter::new(
1232            stream_schema,
1233            stream.fuse(),
1234        ))
1235    }
1236
1237    fn base_objects_index_stream(
1238        base_objects_values: Vec<Option<Vec<String>>>,
1239        base_objects_row_ids: Vec<u64>,
1240    ) -> SendableRecordBatchStream {
1241        let schema = Self::value_row_id_schema(BASE_OBJECTS_VALUE_FIELD.clone());
1242        let stream_schema = schema.clone();
1243        let stream = stream::unfold(
1244            (
1245                base_objects_values.into_iter().zip(base_objects_row_ids),
1246                false,
1247                schema,
1248            ),
1249            |(mut iter, emitted, schema)| async move {
1250                let mut values = Vec::with_capacity(MANIFEST_INDEX_BATCH_SIZE);
1251                let mut row_ids = Vec::with_capacity(MANIFEST_INDEX_BATCH_SIZE);
1252                for _ in 0..MANIFEST_INDEX_BATCH_SIZE {
1253                    let Some((value, row_id)) = iter.next() else {
1254                        break;
1255                    };
1256                    values.push(value);
1257                    row_ids.push(row_id);
1258                }
1259                if values.is_empty() {
1260                    if emitted {
1261                        None
1262                    } else {
1263                        let batch = Self::list_row_id_batch(schema.clone(), values, row_ids)
1264                            .map_err(|err| DataFusionError::External(Box::new(err)));
1265                        Some((batch, (iter, true, schema)))
1266                    }
1267                } else {
1268                    let batch = Self::list_row_id_batch(schema.clone(), values, row_ids)
1269                        .map_err(|err| DataFusionError::External(Box::new(err)));
1270                    Some((batch, (iter, true, schema)))
1271                }
1272            },
1273        );
1274        Box::pin(DatafusionRecordBatchStreamAdapter::new(
1275            stream_schema,
1276            stream.fuse(),
1277        ))
1278    }
1279
1280    async fn train_manifest_index(
1281        dataset: &Dataset,
1282        registry: Arc<IndexPluginRegistry>,
1283        input: ManifestIndexBuildInput,
1284        index_uuid: Uuid,
1285    ) -> Result<ManifestTrainedIndex> {
1286        let index_store = LanceIndexStore::from_dataset_for_new(dataset, &index_uuid)?;
1287        let trainer = registry
1288            .get_plugin_by_name(&input.params.index_type)?
1289            .basic_trainer()
1290            .ok_or_else(|| {
1291                lance_core::Error::invalid_input_source(
1292                    format!(
1293                        "The '{}' index type does not support basic training, please refer to the index's documentation for more details on how to create this index.",
1294                        input.params.index_type
1295                    )
1296                    .into(),
1297                )
1298            })?;
1299        let training_request = trainer
1300            .new_training_request(input.params.params.as_deref().unwrap_or("{}"), &input.field)?;
1301        let created_index = trainer
1302            .train_index(
1303                input.stream,
1304                &index_store,
1305                training_request,
1306                None,
1307                noop_progress(),
1308            )
1309            .await?;
1310        Ok(ManifestTrainedIndex {
1311            index_name: input.index_name,
1312            column_name: input.column_name,
1313            uuid: index_uuid,
1314            created_index,
1315        })
1316    }
1317
1318    fn manifest_index_metadata(
1319        lance_schema: &lance_core::datatypes::Schema,
1320        fragment_bitmap: &RoaringBitmap,
1321        dataset_version: u64,
1322        trained_index: ManifestTrainedIndex,
1323    ) -> Result<IndexMetadata> {
1324        Ok(IndexMetadata {
1325            uuid: trained_index.uuid,
1326            fields: vec![lance_schema.field_id(trained_index.column_name)?],
1327            covering_fields: vec![],
1328            name: trained_index.index_name.to_string(),
1329            dataset_version,
1330            fragment_bitmap: Some(fragment_bitmap.clone()),
1331            index_details: Some(Arc::new(trained_index.created_index.index_details)),
1332            index_version: trained_index.created_index.index_version as i32,
1333            created_at: None,
1334            base_id: None,
1335            files: Some(index_files_to_table(trained_index.created_index.files)),
1336        })
1337    }
1338
1339    fn manifest_fragment_bitmap(manifest: &Manifest) -> Result<RoaringBitmap> {
1340        let mut bitmap = RoaringBitmap::new();
1341        for fragment in manifest.fragments.iter() {
1342            let fragment_id = u32::try_from(fragment.id).map_err(|_| {
1343                lance_core::Error::from(NamespaceError::Internal {
1344                    message: format!("Manifest fragment id {} exceeds u32", fragment.id),
1345                })
1346            })?;
1347            bitmap.insert(fragment_id);
1348        }
1349        Ok(bitmap)
1350    }
1351
1352    fn manifest_from_overwrite_transaction(
1353        previous: &Manifest,
1354        schema: lance_core::datatypes::Schema,
1355        fragments: &[Fragment],
1356    ) -> Manifest {
1357        let mut next_fragment_id = 0;
1358        let mut fragments = fragments
1359            .iter()
1360            .cloned()
1361            .map(|mut fragment| {
1362                if fragment.id == 0 {
1363                    fragment.id = next_fragment_id;
1364                    next_fragment_id += 1;
1365                }
1366                fragment
1367            })
1368            .collect::<Vec<_>>();
1369        fragments.sort_by_key(|fragment| fragment.id);
1370        let mut manifest = Manifest::new_from_previous(previous, schema, Arc::new(fragments));
1371        // Drop a legacy auto-cleanup policy so later commits, including those of
1372        // older writers that inherit this config, stop removing old versions.
1373        manifest
1374            .config
1375            .retain(|key, _| !key.starts_with(AUTO_CLEANUP_CONFIG_PREFIX));
1376        manifest
1377    }
1378
1379    async fn build_manifest_indices(
1380        dataset: &Dataset,
1381        manifest: &Manifest,
1382        index_data: ManifestIndexAccumulator,
1383        index_uuids: [Uuid; 3],
1384    ) -> Result<Vec<IndexMetadata>> {
1385        let fragment_bitmap = Self::manifest_fragment_bitmap(manifest)?;
1386        let schema = &manifest.schema;
1387        let ManifestIndexAccumulator {
1388            object_ids,
1389            object_types,
1390            base_objects_values,
1391            base_objects_row_ids,
1392            ..
1393        } = index_data;
1394        let [object_id_uuid, object_type_uuid, base_objects_uuid] = index_uuids;
1395        let registry = IndexPluginRegistry::with_default_plugins();
1396
1397        let dataset_version = manifest.version;
1398        let object_id_index_fut = Self::build_manifest_index(
1399            dataset,
1400            registry.clone(),
1401            schema,
1402            ManifestIndexBuildInput {
1403                index_name: OBJECT_ID_INDEX_NAME,
1404                column_name: "object_id",
1405                params: ScalarIndexParams::for_builtin(BuiltinIndexType::BTree),
1406                field: Field::new(VALUE_COLUMN_NAME, DataType::Utf8, false),
1407                stream: Self::object_id_index_stream(object_ids),
1408            },
1409            &fragment_bitmap,
1410            dataset_version,
1411            object_id_uuid,
1412        );
1413        let object_type_index_fut = Self::build_manifest_index(
1414            dataset,
1415            registry.clone(),
1416            schema,
1417            ManifestIndexBuildInput {
1418                index_name: OBJECT_TYPE_INDEX_NAME,
1419                column_name: "object_type",
1420                params: ScalarIndexParams::for_builtin(BuiltinIndexType::Bitmap),
1421                field: Field::new(VALUE_COLUMN_NAME, DataType::Utf8, false),
1422                stream: Self::object_type_index_stream(object_types),
1423            },
1424            &fragment_bitmap,
1425            dataset_version,
1426            object_type_uuid,
1427        );
1428        let base_objects_index_fut = Self::build_manifest_index(
1429            dataset,
1430            registry,
1431            schema,
1432            ManifestIndexBuildInput {
1433                index_name: BASE_OBJECTS_INDEX_NAME,
1434                column_name: "base_objects",
1435                params: ScalarIndexParams::for_builtin(BuiltinIndexType::LabelList),
1436                field: BASE_OBJECTS_VALUE_FIELD.clone(),
1437                stream: Self::base_objects_index_stream(base_objects_values, base_objects_row_ids),
1438            },
1439            &fragment_bitmap,
1440            dataset_version,
1441            base_objects_uuid,
1442        );
1443
1444        let (object_id_index, object_type_index, base_objects_index) = futures::join!(
1445            object_id_index_fut,
1446            object_type_index_fut,
1447            base_objects_index_fut
1448        );
1449
1450        Ok(vec![
1451            object_id_index?,
1452            object_type_index?,
1453            base_objects_index?,
1454        ])
1455    }
1456
1457    async fn build_manifest_index(
1458        dataset: &Dataset,
1459        registry: Arc<IndexPluginRegistry>,
1460        lance_schema: &lance_core::datatypes::Schema,
1461        input: ManifestIndexBuildInput,
1462        fragment_bitmap: &RoaringBitmap,
1463        dataset_version: u64,
1464        index_uuid: Uuid,
1465    ) -> Result<IndexMetadata> {
1466        let trained_index =
1467            Self::train_manifest_index(dataset, registry, input, index_uuid).await?;
1468        Self::manifest_index_metadata(
1469            lance_schema,
1470            fragment_bitmap,
1471            dataset_version,
1472            trained_index,
1473        )
1474    }
1475
1476    /// Get the manifest schema
1477    fn manifest_schema() -> Arc<ArrowSchema> {
1478        Arc::new(ArrowSchema::new(vec![
1479            // Set unenforced primary key on object_id for bloom filter conflict detection
1480            Field::new("object_id", DataType::Utf8, false).with_metadata(
1481                [(
1482                    LANCE_UNENFORCED_PRIMARY_KEY_POSITION.to_string(),
1483                    "0".to_string(),
1484                )]
1485                .into_iter()
1486                .collect(),
1487            ),
1488            Field::new("object_type", DataType::Utf8, false),
1489            Field::new("location", DataType::Utf8, true),
1490            Field::new("metadata", DataType::Utf8, true),
1491            Field::new(
1492                "base_objects",
1493                DataType::List(Arc::new(Field::new("object_id", DataType::Utf8, true))),
1494                true,
1495            ),
1496        ]))
1497    }
1498
1499    /// Get a scanner for the manifest dataset
1500    async fn manifest_scanner(&self) -> Result<Scanner> {
1501        let dataset_guard = self.manifest_dataset.get().await?;
1502        Ok(dataset_guard.scan())
1503    }
1504
1505    /// Helper to execute a scanner and collect results into a Vec
1506    async fn execute_scanner(scanner: Scanner) -> Result<Vec<RecordBatch>> {
1507        let mut stream = scanner.try_into_stream().await.map_err(|e| {
1508            lance_core::Error::from(NamespaceError::Internal {
1509                message: format!("Failed to create stream: {:?}", e),
1510            })
1511        })?;
1512
1513        let mut batches = Vec::new();
1514        while let Some(batch) = stream.next().await {
1515            batches.push(batch.map_err(|e| {
1516                lance_core::Error::from(NamespaceError::Internal {
1517                    message: format!("Failed to read batch: {:?}", e),
1518                })
1519            })?);
1520        }
1521
1522        Ok(batches)
1523    }
1524
1525    /// Helper to get a string column from a record batch
1526    fn get_string_column<'a>(batch: &'a RecordBatch, column_name: &str) -> Result<&'a StringArray> {
1527        let column = batch.column_by_name(column_name).ok_or_else(|| {
1528            lance_core::Error::from(NamespaceError::Internal {
1529                message: format!("Column '{}' not found", column_name),
1530            })
1531        })?;
1532        column
1533            .as_any()
1534            .downcast_ref::<StringArray>()
1535            .ok_or_else(|| {
1536                lance_core::Error::from(NamespaceError::Internal {
1537                    message: format!("Column '{}' is not a string array", column_name),
1538                })
1539            })
1540    }
1541
1542    fn required_string_value<'a>(
1543        array: &'a StringArray,
1544        row: usize,
1545        column_name: &str,
1546    ) -> Result<&'a str> {
1547        if array.is_null(row) {
1548            return Err(NamespaceError::Internal {
1549                message: format!("Manifest column '{}' has null at row {}", column_name, row),
1550            }
1551            .into());
1552        }
1553        Ok(array.value(row))
1554    }
1555
1556    fn optional_string_value(array: &StringArray, row: usize) -> Option<String> {
1557        (!array.is_null(row)).then(|| array.value(row).to_string())
1558    }
1559
1560    fn base_objects_column_values(batch: &RecordBatch) -> Result<Vec<Option<Vec<String>>>> {
1561        let Some(column) = batch.column_by_name("base_objects") else {
1562            return Ok(vec![None; batch.num_rows()]);
1563        };
1564        let array = column.as_any().downcast_ref::<ListArray>().ok_or_else(|| {
1565            lance_core::Error::from(NamespaceError::Internal {
1566                message: format!(
1567                    "Column 'base_objects' is not a list array: {:?}",
1568                    column.data_type()
1569                ),
1570            })
1571        })?;
1572
1573        let mut values = Vec::with_capacity(batch.num_rows());
1574        for row in 0..batch.num_rows() {
1575            if array.is_null(row) {
1576                values.push(None);
1577                continue;
1578            }
1579            let row_values = array.value(row);
1580            let row_values = row_values
1581                .as_any()
1582                .downcast_ref::<StringArray>()
1583                .ok_or_else(|| {
1584                    lance_core::Error::from(NamespaceError::Internal {
1585                        message: "Column 'base_objects' values are not strings".to_string(),
1586                    })
1587                })?;
1588            let mut objects = Vec::with_capacity(row_values.len());
1589            for value_index in 0..row_values.len() {
1590                if row_values.is_null(value_index) {
1591                    return Err(NamespaceError::Internal {
1592                        message: format!(
1593                            "Manifest column 'base_objects' has null item at row {} item {}",
1594                            row, value_index
1595                        ),
1596                    }
1597                    .into());
1598                }
1599                objects.push(row_values.value(value_index).to_string());
1600            }
1601            values.push(Some(objects));
1602        }
1603        Ok(values)
1604    }
1605
1606    async fn manifest_projected_stream(dataset: &Dataset) -> Result<SendableRecordBatchStream> {
1607        let mut scanner = dataset.scan();
1608        scanner
1609            .project(&[
1610                "object_id",
1611                "object_type",
1612                "location",
1613                "metadata",
1614                "base_objects",
1615            ])
1616            .map_err(|e| {
1617                lance_core::Error::from(NamespaceError::Internal {
1618                    message: format!("Failed to project manifest columns: {:?}", e),
1619                })
1620            })?;
1621        let stream = scanner.try_into_stream().await.map_err(|e| {
1622            lance_core::Error::from(NamespaceError::Internal {
1623                message: format!("Failed to create manifest stream: {:?}", e),
1624            })
1625        })?;
1626        let schema = stream.schema();
1627        let stream = stream.map_err(|err| DataFusionError::External(Box::new(err)));
1628        Ok(Box::pin(DatafusionRecordBatchStreamAdapter::new(
1629            schema,
1630            stream.fuse(),
1631        )))
1632    }
1633
1634    fn manifest_rewrite_commit_retries(&self) -> u32 {
1635        self.commit_retries
1636            .unwrap_or(DEFAULT_MANIFEST_REWRITE_COMMIT_RETRIES)
1637    }
1638
1639    fn lock_manifest_rewrite_shared<M: ManifestStreamMutation>(
1640        shared: &Arc<StdMutex<ManifestRewriteShared<M>>>,
1641    ) -> Result<StdMutexGuard<'_, ManifestRewriteShared<M>>> {
1642        shared.lock().map_err(|_| {
1643            lance_core::Error::from(NamespaceError::Internal {
1644                message: "Manifest rewrite state mutex was poisoned".to_string(),
1645            })
1646        })
1647    }
1648
1649    fn set_manifest_rewrite_error<M: ManifestStreamMutation>(
1650        shared: &Arc<StdMutex<ManifestRewriteShared<M>>>,
1651        err: LanceError,
1652    ) {
1653        match shared.lock() {
1654            Ok(mut guard) => {
1655                guard.error = Some(err);
1656            }
1657            Err(poisoned) => {
1658                let mut guard = poisoned.into_inner();
1659                guard.error = Some(err);
1660            }
1661        }
1662    }
1663
1664    fn take_manifest_rewrite_error<M: ManifestStreamMutation>(
1665        shared: &Arc<StdMutex<ManifestRewriteShared<M>>>,
1666    ) -> Result<Option<LanceError>> {
1667        let mut guard = Self::lock_manifest_rewrite_shared(shared)?;
1668        Ok(guard.error.take())
1669    }
1670
1671    fn process_manifest_rewrite_batch<M: ManifestStreamMutation>(
1672        batch: RecordBatch,
1673        shared: &Arc<StdMutex<ManifestRewriteShared<M>>>,
1674    ) -> Result<Option<RecordBatch>> {
1675        let object_ids = Self::get_string_column(&batch, "object_id")?;
1676        let object_types = Self::get_string_column(&batch, "object_type")?;
1677        let locations = Self::get_string_column(&batch, "location")?;
1678        let metadatas = Self::get_string_column(&batch, "metadata")?;
1679        let base_objects = Self::base_objects_column_values(&batch)?;
1680        let mut output = ManifestBatchBuilder::new();
1681        let mut guard = Self::lock_manifest_rewrite_shared(shared)?;
1682        let mut index_data = guard.index_data.take().ok_or_else(|| {
1683            lance_core::Error::from(NamespaceError::Internal {
1684                message: "Manifest rewrite index state is unavailable".to_string(),
1685            })
1686        })?;
1687        for (row, base_objects) in base_objects.into_iter().enumerate().take(batch.num_rows()) {
1688            let row_value = ManifestRowValue {
1689                object_id: Self::required_string_value(object_ids, row, "object_id")?.to_string(),
1690                object_type: ObjectType::parse(Self::required_string_value(
1691                    object_types,
1692                    row,
1693                    "object_type",
1694                )?)?,
1695                location: Self::optional_string_value(locations, row),
1696                metadata: Self::optional_string_value(metadatas, row),
1697                base_objects,
1698            };
1699            guard
1700                .mutation
1701                .process_existing_row(row_value, &mut output, &mut index_data)?;
1702        }
1703        guard.index_data = Some(index_data);
1704        if output.is_empty() {
1705            return Ok(None);
1706        }
1707        Ok(Some(output.finish()?))
1708    }
1709
1710    fn finish_manifest_rewrite_stream<M: ManifestStreamMutation>(
1711        shared: &Arc<StdMutex<ManifestRewriteShared<M>>>,
1712    ) -> Result<Option<RecordBatch>> {
1713        let mut output = ManifestBatchBuilder::new();
1714        let mut guard = Self::lock_manifest_rewrite_shared(shared)?;
1715        let mut index_data = guard.index_data.take().ok_or_else(|| {
1716            lance_core::Error::from(NamespaceError::Internal {
1717                message: "Manifest rewrite index state is unavailable".to_string(),
1718            })
1719        })?;
1720        guard.mutation.append_rows(&mut output, &mut index_data)?;
1721        let result = guard.mutation.finish();
1722        let force_empty_batch = index_data.row_count == 0;
1723        guard.result = Some(result);
1724        guard.index_data = Some(index_data);
1725        if output.is_empty() && !force_empty_batch {
1726            Ok(None)
1727        } else {
1728            Ok(Some(output.finish()?))
1729        }
1730    }
1731
1732    fn manifest_rewrite_output_stream<M: ManifestStreamMutation + 'static>(
1733        source: SendableRecordBatchStream,
1734        shared: Arc<StdMutex<ManifestRewriteShared<M>>>,
1735    ) -> SendableRecordBatchStream {
1736        enum Phase {
1737            Source,
1738            Finish,
1739            Done,
1740        }
1741
1742        let schema = Self::manifest_schema();
1743        let stream = stream::unfold(
1744            (source, shared, Phase::Source),
1745            |(mut source, shared, mut phase)| async move {
1746                loop {
1747                    match phase {
1748                        Phase::Source => match source.next().await {
1749                            Some(Ok(batch)) => {
1750                                match Self::process_manifest_rewrite_batch(batch, &shared) {
1751                                    Ok(Some(batch)) => {
1752                                        return Some((Ok(batch), (source, shared, phase)));
1753                                    }
1754                                    Ok(None) => continue,
1755                                    Err(err) => {
1756                                        let message = err.to_string();
1757                                        Self::set_manifest_rewrite_error(&shared, err);
1758                                        return Some((
1759                                            Err(DataFusionError::External(Box::new(
1760                                                std::io::Error::other(message),
1761                                            ))),
1762                                            (source, shared, Phase::Done),
1763                                        ));
1764                                    }
1765                                }
1766                            }
1767                            Some(Err(err)) => {
1768                                return Some((Err(err), (source, shared, Phase::Done)));
1769                            }
1770                            None => phase = Phase::Finish,
1771                        },
1772                        Phase::Finish => {
1773                            phase = Phase::Done;
1774                            match Self::finish_manifest_rewrite_stream(&shared) {
1775                                Ok(Some(batch)) => {
1776                                    return Some((Ok(batch), (source, shared, phase)));
1777                                }
1778                                Ok(None) => continue,
1779                                Err(err) => {
1780                                    let message = err.to_string();
1781                                    Self::set_manifest_rewrite_error(&shared, err);
1782                                    return Some((
1783                                        Err(DataFusionError::External(Box::new(
1784                                            std::io::Error::other(message),
1785                                        ))),
1786                                        (source, shared, Phase::Done),
1787                                    ));
1788                                }
1789                            }
1790                        }
1791                        Phase::Done => return None,
1792                    }
1793                }
1794            },
1795        );
1796        Box::pin(DatafusionRecordBatchStreamAdapter::new(
1797            schema,
1798            stream.fuse(),
1799        ))
1800    }
1801
1802    fn take_manifest_rewrite_result<M: ManifestStreamMutation>(
1803        shared: &Arc<StdMutex<ManifestRewriteShared<M>>>,
1804    ) -> Result<(CopyOnWriteMutation<M::Output>, ManifestIndexAccumulator)> {
1805        let mut guard = Self::lock_manifest_rewrite_shared(shared)?;
1806        let result = guard.result.take().ok_or_else(|| {
1807            lance_core::Error::from(NamespaceError::Internal {
1808                message: "Manifest rewrite stream did not finish".to_string(),
1809            })
1810        })?;
1811        let index_data = guard.index_data.take().ok_or_else(|| {
1812            lance_core::Error::from(NamespaceError::Internal {
1813                message: "Manifest rewrite index state is unavailable".to_string(),
1814            })
1815        })?;
1816        Ok((result, index_data))
1817    }
1818
1819    /// Delete the staged (uncommitted) data files and index directories for a rewrite.
1820    /// Only call this once the rewrite is known *not* to have landed (a put-if-not-exists
1821    /// conflict, or an ambiguous error whose target version does not reference our data
1822    /// file) — otherwise it would orphan files a committed manifest still references.
1823    async fn cleanup_staged_manifest_files(
1824        &self,
1825        object_store: &ObjectStore,
1826        data_files: &HashSet<String>,
1827        index_uuids: &[Uuid],
1828    ) {
1829        let data_dir = self
1830            .base_path
1831            .clone()
1832            .join(MANIFEST_TABLE_NAME)
1833            .join(LANCE_DATA_DIR);
1834        for path in data_files {
1835            let data_path = data_dir.clone().join(path.as_str());
1836            if let Err(err) = object_store.delete(&data_path).await {
1837                log::warn!(
1838                    "Failed to clean up uncommitted manifest rewrite data file '{}': {}",
1839                    data_path,
1840                    err
1841                );
1842            }
1843        }
1844        self.cleanup_uncommitted_manifest_index_dirs(object_store, index_uuids.iter().copied())
1845            .await;
1846    }
1847
1848    async fn cleanup_uncommitted_manifest_index_dirs(
1849        &self,
1850        object_store: &ObjectStore,
1851        index_uuids: impl IntoIterator<Item = Uuid>,
1852    ) {
1853        for index_uuid in index_uuids {
1854            let index_dir = self
1855                .base_path
1856                .clone()
1857                .join(MANIFEST_TABLE_NAME)
1858                .join(LANCE_INDICES_DIR)
1859                .join(index_uuid.to_string());
1860            if let Err(err) = object_store.remove_dir_all(index_dir.clone()).await
1861                && !matches!(err, LanceError::NotFound { .. })
1862            {
1863                log::warn!(
1864                    "Failed to clean up uncommitted manifest rewrite index directory '{}': {}",
1865                    index_dir,
1866                    err
1867                );
1868            }
1869        }
1870    }
1871
1872    /// Resolve the commit handler for the `__manifest` dataset's storage backend.
1873    async fn manifest_commit_handler(&self) -> Result<Arc<dyn CommitHandler>> {
1874        commit_handler_from_url(&self.root, &None)
1875            .await
1876            .map_err(|e| {
1877                lance_core::Error::from(NamespaceError::Internal {
1878                    message: format!("Failed to resolve manifest commit handler: {:?}", e),
1879                })
1880            })
1881    }
1882
1883    /// Directly write the rewritten `__manifest` as a new version using the storage
1884    /// backend's atomic put-if-not-exists. The overwrite transaction is embedded inline
1885    /// (no separate transaction file) and the commit handler writes the version hint.
1886    async fn commit_manifest_overwrite(
1887        &self,
1888        dataset: &Dataset,
1889        commit_handler: &dyn CommitHandler,
1890        manifest: &mut Manifest,
1891        indices: Option<Vec<IndexMetadata>>,
1892        transaction: Transaction,
1893    ) -> std::result::Result<(), CommitError> {
1894        ensure_can_write_manifest(manifest).map_err(CommitError::from)?;
1895        apply_feature_flags(manifest, false, false).map_err(CommitError::from)?;
1896        let timestamp_nanos = SystemTime::now()
1897            .duration_since(UNIX_EPOCH)
1898            .map(|d| d.as_nanos())
1899            .unwrap_or(0);
1900        manifest.set_timestamp(timestamp_nanos);
1901        manifest.update_max_fragment_id();
1902
1903        // Commit through the dataset's own object store, not `self.object_store`: for
1904        // stores like `memory://` the namespace and the dataset can hold different
1905        // instances, and a commit written to the wrong one is invisible to reads.
1906        let object_store = dataset
1907            .object_store(None)
1908            .await
1909            .map_err(CommitError::from)?;
1910        let base_path = self.base_path.clone().join(MANIFEST_TABLE_NAME);
1911        let naming_scheme = dataset.manifest_location().naming_scheme;
1912        let inline_transaction =
1913            lance_table::format::Transaction::try_from(&transaction).map_err(CommitError::from)?;
1914        commit_handler
1915            .commit(
1916                manifest,
1917                indices,
1918                &base_path,
1919                &object_store,
1920                write_manifest_file_to_path,
1921                naming_scheme,
1922                Some(inline_transaction),
1923            )
1924            .await
1925            .map(|_location| ())
1926    }
1927
1928    /// After an ambiguous commit error, determine whether our overwrite actually landed at
1929    /// `target_version`. A network failure can leave the manifest committed even though the
1930    /// client observed an error; in that case the committed version references one of our
1931    /// staged data files, and deleting them would corrupt the catalog.
1932    async fn manifest_commit_landed(
1933        &self,
1934        dataset: &Dataset,
1935        target_version: u64,
1936        data_files: &HashSet<String>,
1937    ) -> bool {
1938        let Ok(committed) = dataset.checkout_version(target_version).await else {
1939            return false;
1940        };
1941        committed.manifest().fragments.iter().any(|fragment| {
1942            fragment
1943                .files
1944                .iter()
1945                .any(|file| data_files.contains(file.path.as_str()))
1946        })
1947    }
1948
1949    /// Resolve a storage commit conflict against the latest committed catalog state.
1950    /// Returns `Some(output)` when the mutation's intent is already satisfied (no retry
1951    /// needed), `Ok(None)` to retry the rewrite, or an error for a terminal conflict.
1952    async fn resolve_manifest_conflict<O: Clone>(
1953        &self,
1954        resolution: &ConflictResolution<O>,
1955    ) -> Result<Option<O>> {
1956        match resolution {
1957            ConflictResolution::Retry => Ok(None),
1958            ConflictResolution::FailIfExists(object_ids) => {
1959                for object_id in object_ids {
1960                    if self.manifest_contains_object(object_id).await? {
1961                        return Err(NamespaceError::ConcurrentModification {
1962                            message: format!(
1963                                "Object '{}' was concurrently created by another operation",
1964                                object_id
1965                            ),
1966                        }
1967                        .into());
1968                    }
1969                }
1970                Ok(None)
1971            }
1972            ConflictResolution::SucceedIfAbsent { object_id, output } => {
1973                if self.manifest_contains_object(object_id).await? {
1974                    Ok(None)
1975                } else {
1976                    Ok(Some(output.clone()))
1977                }
1978            }
1979        }
1980    }
1981
1982    /// Validate that this build can write the current `__manifest` before a
1983    /// mutating operation performs any side effect (e.g. writing table data), so
1984    /// a refused write leaves nothing orphaned behind. The eventual
1985    /// `rewrite_manifest` commit re-checks `ensure_writable` on each retry, so a
1986    /// concurrent upgrade in between is still caught.
1987    async fn ensure_manifest_writable(&self) -> Result<()> {
1988        let dataset_guard = self.manifest_dataset.get().await?;
1989        ensure_can_write_manifest(dataset_guard.manifest())?;
1990        ensure_writable(dataset_guard.metadata())
1991    }
1992
1993    async fn rewrite_manifest<M, F>(
1994        &self,
1995        operation: &str,
1996        mut make_mutation: F,
1997    ) -> Result<M::Output>
1998    where
1999        M: ManifestStreamMutation + 'static,
2000        F: FnMut() -> M,
2001    {
2002        let _mutation_guard = self.manifest_mutation_lock.lock().await;
2003        let max_retries = self.manifest_rewrite_commit_retries();
2004        let mut retries = 0;
2005        let build_indices = self.inline_optimization_enabled;
2006        let commit_handler = self.manifest_commit_handler().await?;
2007
2008        loop {
2009            let dataset_guard = self.manifest_dataset.get_refreshed().await?;
2010            ensure_can_write_manifest(dataset_guard.manifest())?;
2011            let dataset = Arc::new(dataset_guard.clone());
2012            drop(dataset_guard);
2013            // The namespace format has its own capabilities in table metadata,
2014            // separate from the Lance manifest capabilities checked above.
2015            ensure_writable(dataset.metadata())?;
2016            // Staged files, indices, the commit, and cleanup must all use the dataset's
2017            // own object store (see `commit_manifest_overwrite`).
2018            let object_store = dataset.object_store(None).await?;
2019
2020            let source = Self::manifest_projected_stream(&dataset).await?;
2021            let resolution = make_mutation().conflict_resolution();
2022            let shared = Arc::new(StdMutex::new(ManifestRewriteShared::new(make_mutation())));
2023            let output_stream = Self::manifest_rewrite_output_stream(source, shared.clone());
2024            // Pin both limits so the overwrite never splits into multiple fragments: the
2025            // replacement indices map each row to address `(0 << 32) | offset`, valid only
2026            // for a single fragment with id 0. The row count is bounded below u32::MAX by
2027            // `ManifestIndexAccumulator::next_row_id`.
2028            let write_params = WriteParams {
2029                mode: WriteMode::Overwrite,
2030                session: self.session.clone(),
2031                max_rows_per_file: u32::MAX as usize,
2032                max_bytes_per_file: usize::MAX,
2033                skip_auto_cleanup: true,
2034                ..WriteParams::default()
2035            };
2036
2037            let transaction = match InsertBuilder::new(dataset.clone())
2038                .with_params(&write_params)
2039                .execute_uncommitted_stream(output_stream)
2040                .await
2041            {
2042                Ok(transaction) => transaction,
2043                Err(err) => {
2044                    if let Some(stream_err) = Self::take_manifest_rewrite_error(&shared)? {
2045                        return Err(stream_err);
2046                    }
2047                    return Err(convert_lance_commit_error(&err, operation, None));
2048                }
2049            };
2050
2051            let (mutation, index_data) = Self::take_manifest_rewrite_result(&shared)?;
2052
2053            let Operation::Overwrite {
2054                fragments, schema, ..
2055            } = &transaction.operation
2056            else {
2057                return Err(NamespaceError::Internal {
2058                    message: "Manifest rewrite transaction is not an overwrite".to_string(),
2059                }
2060                .into());
2061            };
2062            // Unique data files this attempt staged. Used to clean up orphans and to
2063            // attribute an ambiguous commit error back to us.
2064            let staged_data_files = fragments
2065                .iter()
2066                .flat_map(|fragment| fragment.files.iter())
2067                .filter(|file| file.base_id.is_none())
2068                .map(|file| file.path.clone())
2069                .collect::<HashSet<_>>();
2070
2071            if !mutation.has_changes {
2072                self.cleanup_staged_manifest_files(&object_store, &staged_data_files, &[])
2073                    .await;
2074                return Ok(mutation.result);
2075            }
2076
2077            let mut manifest = Self::manifest_from_overwrite_transaction(
2078                dataset.manifest(),
2079                schema.clone(),
2080                fragments,
2081            );
2082            if let Err(err) = validate_non_reusable_field_id_transition(
2083                dataset.manifest(),
2084                &manifest,
2085                &transaction.operation,
2086            ) {
2087                self.cleanup_staged_manifest_files(&object_store, &staged_data_files, &[])
2088                    .await;
2089                return Err(err);
2090            }
2091            manifest.update_max_field_id();
2092            let target_version = manifest.version;
2093
2094            let index_uuids = [Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()];
2095            let indices = if build_indices {
2096                match Self::build_manifest_indices(&dataset, &manifest, index_data, index_uuids)
2097                    .await
2098                {
2099                    Ok(indices) => Some(indices),
2100                    Err(err) => {
2101                        self.cleanup_staged_manifest_files(
2102                            &object_store,
2103                            &staged_data_files,
2104                            &index_uuids,
2105                        )
2106                        .await;
2107                        return Err(err);
2108                    }
2109                }
2110            } else {
2111                None
2112            };
2113            let staged_index_uuids: &[Uuid] = if build_indices { &index_uuids } else { &[] };
2114
2115            let commit_result = self
2116                .commit_manifest_overwrite(
2117                    &dataset,
2118                    commit_handler.as_ref(),
2119                    &mut manifest,
2120                    indices,
2121                    transaction,
2122                )
2123                .await;
2124
2125            match commit_result {
2126                Ok(()) => {
2127                    let _ = self.manifest_dataset.get_refreshed().await;
2128                    return Ok(mutation.result);
2129                }
2130                Err(err) => {
2131                    // The put may have landed even though the client saw an error (lost
2132                    // ack). Verify before deleting anything so we never orphan files that a
2133                    // committed manifest still references.
2134                    if self
2135                        .manifest_commit_landed(&dataset, target_version, &staged_data_files)
2136                        .await
2137                    {
2138                        let _ = self.manifest_dataset.get_refreshed().await;
2139                        return Ok(mutation.result);
2140                    }
2141                    self.cleanup_staged_manifest_files(
2142                        &object_store,
2143                        &staged_data_files,
2144                        staged_index_uuids,
2145                    )
2146                    .await;
2147                    match err {
2148                        CommitError::CommitConflict => {
2149                            if let Some(output) =
2150                                self.resolve_manifest_conflict(&resolution).await?
2151                            {
2152                                return Ok(output);
2153                            }
2154                            if retries >= max_retries {
2155                                return Err(NamespaceError::ConcurrentModification {
2156                                    message: format!(
2157                                        "{}: still conflicting after {} retries",
2158                                        operation, max_retries
2159                                    ),
2160                                }
2161                                .into());
2162                            }
2163                            retries += 1;
2164                            tokio::time::sleep(std::time::Duration::from_millis(
2165                                10 * u64::from(retries),
2166                            ))
2167                            .await;
2168                        }
2169                        CommitError::OtherError(err) => {
2170                            return Err(convert_lance_commit_error(&err, operation, None));
2171                        }
2172                    }
2173                }
2174            }
2175        }
2176    }
2177
2178    /// Check if the manifest contains an object with the given ID
2179    async fn manifest_contains_object(&self, object_id: &str) -> Result<bool> {
2180        let escaped_id = object_id.replace('\'', "''");
2181        let filter = format!("object_id = '{}'", escaped_id);
2182
2183        let dataset_guard = self.manifest_dataset.get().await?;
2184        let mut scanner = dataset_guard.scan();
2185
2186        scanner.filter(&filter).map_err(|e| {
2187            lance_core::Error::from(NamespaceError::Internal {
2188                message: format!("Failed to filter: {:?}", e),
2189            })
2190        })?;
2191
2192        // Project no columns and enable row IDs for count_rows to work
2193        scanner.project::<&str>(&[]).map_err(|e| {
2194            lance_core::Error::from(NamespaceError::Internal {
2195                message: format!("Failed to project: {:?}", e),
2196            })
2197        })?;
2198
2199        scanner.with_row_id();
2200
2201        let count = scanner.count_rows().await.map_err(|e| {
2202            lance_core::Error::from(NamespaceError::Internal {
2203                message: format!("Failed to count rows: {:?}", e),
2204            })
2205        })?;
2206
2207        Ok(count > 0)
2208    }
2209
2210    /// Query the manifest for a table with the given object ID
2211    async fn query_manifest_for_table(&self, object_id: &str) -> Result<Option<TableInfo>> {
2212        let escaped_id = object_id.replace('\'', "''");
2213        let filter = format!("object_id = '{}' AND object_type = 'table'", escaped_id);
2214        let mut scanner = self.manifest_scanner().await?;
2215        scanner.filter(&filter).map_err(|e| {
2216            lance_core::Error::from(NamespaceError::Internal {
2217                message: format!("Failed to filter: {:?}", e),
2218            })
2219        })?;
2220        scanner
2221            .project(&["object_id", "location", "metadata"])
2222            .map_err(|e| {
2223                lance_core::Error::from(NamespaceError::Internal {
2224                    message: format!("Failed to project: {:?}", e),
2225                })
2226            })?;
2227        let batches = Self::execute_scanner(scanner).await?;
2228
2229        let mut found_result: Option<TableInfo> = None;
2230        let mut total_rows = 0;
2231
2232        for batch in batches {
2233            if batch.num_rows() == 0 {
2234                continue;
2235            }
2236
2237            total_rows += batch.num_rows();
2238            if total_rows > 1 {
2239                return Err(NamespaceError::Internal {
2240                    message: format!(
2241                        "Expected exactly 1 table with id '{}', found {}",
2242                        object_id, total_rows
2243                    ),
2244                }
2245                .into());
2246            }
2247
2248            let object_id_array = Self::get_string_column(&batch, "object_id")?;
2249            let location_array = Self::get_string_column(&batch, "location")?;
2250            let metadata_array = Self::get_string_column(&batch, "metadata")?;
2251            let location = location_array.value(0).to_string();
2252            let metadata = if !metadata_array.is_null(0) {
2253                let metadata_str = metadata_array.value(0);
2254                match serde_json::from_str::<HashMap<String, String>>(metadata_str) {
2255                    Ok(map) => Some(map),
2256                    Err(e) => {
2257                        return Err(NamespaceError::Internal {
2258                            message: format!(
2259                                "Failed to deserialize metadata for table '{}': {}",
2260                                object_id, e
2261                            ),
2262                        }
2263                        .into());
2264                    }
2265                }
2266            } else {
2267                None
2268            };
2269            let (namespace, name) = Self::parse_object_id(object_id_array.value(0));
2270            found_result = Some(TableInfo {
2271                namespace,
2272                name,
2273                location,
2274                metadata,
2275            });
2276        }
2277
2278        Ok(found_result)
2279    }
2280
2281    fn serialize_metadata(
2282        properties: Option<&HashMap<String, String>>,
2283        object_type: &str,
2284        object_id: &str,
2285    ) -> Result<Option<String>> {
2286        match properties {
2287            Some(properties) if !properties.is_empty() => {
2288                serde_json::to_string(properties).map(Some).map_err(|e| {
2289                    LanceError::from(NamespaceError::Internal {
2290                        message: format!(
2291                            "Failed to serialize {} metadata for '{}': {}",
2292                            object_type, object_id, e
2293                        ),
2294                    })
2295                })
2296            }
2297            _ => Ok(None),
2298        }
2299    }
2300
2301    pub(crate) async fn path_has_actual_manifests(
2302        object_store: &ObjectStore,
2303        table_path: &Path,
2304    ) -> Result<bool> {
2305        let versions_path = table_path
2306            .clone()
2307            .join(lance_table::io::commit::VERSIONS_DIR);
2308        // `_versions/` should only contain manifest files, so probing the first entry is enough
2309        // to distinguish declared-only tables (empty `_versions/`) from created tables.
2310        Ok(object_store
2311            .list(Some(versions_path))
2312            .try_next()
2313            .await?
2314            .is_some())
2315    }
2316
2317    async fn location_has_actual_manifests(&self, location: &str) -> Result<bool> {
2318        Self::path_has_actual_manifests(&self.object_store, &self.base_path.clone().join(location))
2319            .await
2320    }
2321
2322    pub(crate) fn is_not_found_load_error(err: &LanceError) -> bool {
2323        match err.find_root() {
2324            LanceError::NotFound { .. } => true,
2325            LanceError::IO { source, .. } => source
2326                .downcast_ref::<ObjectStoreError>()
2327                .is_some_and(|source| matches!(source, ObjectStoreError::NotFound { .. })),
2328            LanceError::DatasetNotFound { source, .. } => {
2329                source
2330                    .downcast_ref::<LanceError>()
2331                    .is_some_and(|source| matches!(source, LanceError::NotFound { .. }))
2332                    || source
2333                        .downcast_ref::<ObjectStoreError>()
2334                        .is_some_and(|source| matches!(source, ObjectStoreError::NotFound { .. }))
2335            }
2336            _ => false,
2337        }
2338    }
2339
2340    /// List all table locations in the manifest (for root namespace only)
2341    /// Returns a set of table locations (e.g., "table_name.lance")
2342    pub async fn list_manifest_table_locations(&self) -> Result<std::collections::HashSet<String>> {
2343        let filter = "object_type = 'table' AND NOT contains(object_id, '$')";
2344        let mut scanner = self.manifest_scanner().await?;
2345        scanner.filter(filter).map_err(|e| {
2346            lance_core::Error::from(NamespaceError::Internal {
2347                message: format!("Failed to filter: {:?}", e),
2348            })
2349        })?;
2350        scanner.project(&["location"]).map_err(|e| {
2351            lance_core::Error::from(NamespaceError::Internal {
2352                message: format!("Failed to project: {:?}", e),
2353            })
2354        })?;
2355
2356        let batches = Self::execute_scanner(scanner).await?;
2357        let mut locations = std::collections::HashSet::new();
2358
2359        for batch in batches {
2360            if batch.num_rows() == 0 {
2361                continue;
2362            }
2363            let location_array = Self::get_string_column(&batch, "location")?;
2364            for i in 0..location_array.len() {
2365                locations.insert(location_array.value(i).to_string());
2366            }
2367        }
2368
2369        Ok(locations)
2370    }
2371
2372    /// Insert an entry into the manifest table
2373    async fn insert_into_manifest(
2374        &self,
2375        object_id: String,
2376        object_type: ObjectType,
2377        location: Option<String>,
2378    ) -> Result<()> {
2379        self.insert_into_manifest_with_metadata(
2380            vec![ManifestEntry {
2381                object_id,
2382                object_type,
2383                location,
2384                metadata: None,
2385            }],
2386            None,
2387        )
2388        .await
2389    }
2390
2391    /// Insert one or more entries into the manifest table with metadata and base_objects.
2392    ///
2393    /// This is the unified entry point for both single and batch inserts.
2394    /// If any entry already exists (matching object_id), the entire batch fails.
2395    pub async fn insert_into_manifest_with_metadata(
2396        &self,
2397        entries: Vec<ManifestEntry>,
2398        base_objects: Option<Vec<String>>,
2399    ) -> Result<()> {
2400        self.merge_into_manifest_with_metadata(entries, base_objects, WhenMatched::Fail)
2401            .await
2402    }
2403
2404    async fn upsert_into_manifest_with_metadata(
2405        &self,
2406        entries: Vec<ManifestEntry>,
2407        base_objects: Option<Vec<String>>,
2408    ) -> Result<()> {
2409        self.merge_into_manifest_with_metadata(entries, base_objects, WhenMatched::UpdateAll)
2410            .await
2411    }
2412
2413    async fn merge_into_manifest_with_metadata(
2414        &self,
2415        entries: Vec<ManifestEntry>,
2416        base_objects: Option<Vec<String>>,
2417        when_matched: WhenMatched,
2418    ) -> Result<()> {
2419        if entries.is_empty() {
2420            return Ok(());
2421        }
2422
2423        self.rewrite_manifest("Failed to overwrite manifest", || {
2424            UpsertManifestMutation::new(entries.clone(), base_objects.clone(), when_matched.clone())
2425        })
2426        .await
2427    }
2428
2429    /// Delete an entry from the manifest table
2430    pub async fn delete_from_manifest(&self, object_id: &str) -> Result<()> {
2431        let object_id = object_id.to_string();
2432        self.rewrite_manifest("Failed to delete from manifest", || DeleteObjectMutation {
2433            object_id: object_id.clone(),
2434            deleted: false,
2435        })
2436        .await
2437    }
2438
2439    /// Register a table in the manifest without creating the physical table (internal helper for migration)
2440    pub async fn register_table(&self, name: &str, location: String) -> Result<()> {
2441        let object_id = Self::build_object_id(&[], name);
2442        if self.manifest_contains_object(&object_id).await? {
2443            return Err(NamespaceError::Internal {
2444                message: format!("Table '{}' already exists", name),
2445            }
2446            .into());
2447        }
2448
2449        self.insert_into_manifest(object_id, ObjectType::Table, Some(location))
2450            .await
2451    }
2452
2453    /// Validate that all levels of a namespace path exist
2454    async fn validate_namespace_levels_exist(&self, namespace_path: &[String]) -> Result<()> {
2455        for i in 1..=namespace_path.len() {
2456            let partial_path = &namespace_path[..i];
2457            let object_id = partial_path.join(DELIMITER);
2458            if self
2459                .query_manifest_for_namespace(&object_id)
2460                .await?
2461                .is_none()
2462            {
2463                return Err(NamespaceError::NamespaceNotFound {
2464                    message: format!("parent namespace '{}'", object_id),
2465                }
2466                .into());
2467            }
2468        }
2469        Ok(())
2470    }
2471
2472    /// Query the manifest for a namespace with the given object ID
2473    async fn query_manifest_for_namespace(&self, object_id: &str) -> Result<Option<NamespaceInfo>> {
2474        let escaped_id = object_id.replace('\'', "''");
2475        let filter = format!("object_id = '{}' AND object_type = 'namespace'", escaped_id);
2476        let mut scanner = self.manifest_scanner().await?;
2477        scanner.filter(&filter).map_err(|e| {
2478            lance_core::Error::from(NamespaceError::Internal {
2479                message: format!("Failed to filter: {:?}", e),
2480            })
2481        })?;
2482        scanner.project(&["object_id", "metadata"]).map_err(|e| {
2483            lance_core::Error::from(NamespaceError::Internal {
2484                message: format!("Failed to project: {:?}", e),
2485            })
2486        })?;
2487        let batches = Self::execute_scanner(scanner).await?;
2488
2489        let mut found_result: Option<NamespaceInfo> = None;
2490        let mut total_rows = 0;
2491
2492        for batch in batches {
2493            if batch.num_rows() == 0 {
2494                continue;
2495            }
2496
2497            total_rows += batch.num_rows();
2498            if total_rows > 1 {
2499                return Err(NamespaceError::Internal {
2500                    message: format!(
2501                        "Expected exactly 1 namespace with id '{}', found {}",
2502                        object_id, total_rows
2503                    ),
2504                }
2505                .into());
2506            }
2507
2508            let object_id_array = Self::get_string_column(&batch, "object_id")?;
2509            let metadata_array = Self::get_string_column(&batch, "metadata")?;
2510
2511            let object_id_str = object_id_array.value(0);
2512            let metadata = if !metadata_array.is_null(0) {
2513                let metadata_str = metadata_array.value(0);
2514                match serde_json::from_str::<HashMap<String, String>>(metadata_str) {
2515                    Ok(map) => Some(map),
2516                    Err(e) => {
2517                        return Err(NamespaceError::Internal {
2518                            message: format!(
2519                                "Failed to deserialize metadata for namespace '{}': {}",
2520                                object_id, e
2521                            ),
2522                        }
2523                        .into());
2524                    }
2525                }
2526            } else {
2527                None
2528            };
2529
2530            let (namespace, name) = Self::parse_object_id(object_id_str);
2531            found_result = Some(NamespaceInfo {
2532                namespace,
2533                name,
2534                metadata,
2535            });
2536        }
2537
2538        Ok(found_result)
2539    }
2540
2541    /// Load an existing manifest dataset without creating or migrating it.
2542    async fn open_manifest_table(
2543        root: &str,
2544        storage_options: &Option<HashMap<String, String>>,
2545        session: Option<Arc<Session>>,
2546    ) -> Result<DatasetConsistencyWrapper> {
2547        let manifest_path = format!("{}/{}", root, MANIFEST_TABLE_NAME);
2548        log::debug!("Attempting to load manifest from {}", manifest_path);
2549        let store_options = ObjectStoreParams {
2550            storage_options_accessor: storage_options.as_ref().map(|opts| {
2551                Arc::new(
2552                    lance_io::object_store::StorageOptionsAccessor::with_static_options(
2553                        opts.clone(),
2554                    ),
2555                )
2556            }),
2557            ..Default::default()
2558        };
2559        let read_params = ReadParams {
2560            session,
2561            store_options: Some(store_options),
2562            ..Default::default()
2563        };
2564        let dataset = DatasetBuilder::from_uri(&manifest_path)
2565            .with_read_params(read_params)
2566            .load()
2567            .await?;
2568        ensure_readable(dataset.metadata())?;
2569        Ok(DatasetConsistencyWrapper::new(dataset))
2570    }
2571
2572    /// Create or load the manifest dataset, ensuring it has the latest schema setup.
2573    ///
2574    /// This function will:
2575    /// 1. Try to load an existing manifest table
2576    /// 2. If it exists, check and migrate the schema if needed (e.g., add primary key metadata)
2577    /// 3. If it doesn't exist, create a new manifest table with the current schema
2578    async fn ensure_manifest_table_up_to_date(
2579        root: &str,
2580        storage_options: &Option<HashMap<String, String>>,
2581        session: Option<Arc<Session>>,
2582    ) -> Result<DatasetConsistencyWrapper> {
2583        let manifest_path = format!("{}/{}", root, MANIFEST_TABLE_NAME);
2584        log::debug!("Attempting to load manifest from {}", manifest_path);
2585        let store_options = ObjectStoreParams {
2586            storage_options_accessor: storage_options.as_ref().map(|opts| {
2587                Arc::new(
2588                    lance_io::object_store::StorageOptionsAccessor::with_static_options(
2589                        opts.clone(),
2590                    ),
2591                )
2592            }),
2593            ..Default::default()
2594        };
2595        let read_params = ReadParams {
2596            session: session.clone(),
2597            store_options: Some(store_options.clone()),
2598            ..Default::default()
2599        };
2600        let dataset_result = DatasetBuilder::from_uri(&manifest_path)
2601            .with_read_params(read_params)
2602            .load()
2603            .await;
2604        match dataset_result {
2605            Ok(mut dataset) => {
2606                // Reject a manifest written with a reader feature flag this build
2607                // does not understand before touching it.
2608                ensure_readable(dataset.metadata())?;
2609
2610                // Check if the object_id field has primary key metadata, migrate if not
2611                let needs_pk_migration = dataset
2612                    .schema()
2613                    .field("object_id")
2614                    .map(|f| {
2615                        !f.metadata
2616                            .contains_key(LANCE_UNENFORCED_PRIMARY_KEY_POSITION)
2617                    })
2618                    .unwrap_or(false);
2619
2620                if needs_pk_migration {
2621                    // This legacy migration writes to the manifest, so confirm this
2622                    // build is allowed to write the current format first.
2623                    ensure_writable(dataset.metadata())?;
2624                    log::info!(
2625                        "Migrating __manifest table to add primary key metadata on object_id"
2626                    );
2627                    dataset
2628                        .update_field_metadata()
2629                        .update("object_id", [(LANCE_UNENFORCED_PRIMARY_KEY_POSITION, "0")])
2630                        .map_err(|e| {
2631                            lance_core::Error::from(NamespaceError::Internal {
2632                                message: format!(
2633                                    "Failed to find object_id field for migration: {:?}",
2634                                    e
2635                                ),
2636                            })
2637                        })?
2638                        .await
2639                        .map_err(|e| {
2640                            lance_core::Error::from(NamespaceError::Internal {
2641                                message: format!("Failed to migrate primary key metadata: {:?}", e),
2642                            })
2643                        })?;
2644                }
2645
2646                Ok(DatasetConsistencyWrapper::new(dataset))
2647            }
2648            Err(err) if Self::is_not_found_load_error(&err) => {
2649                log::info!("Creating new manifest table at {}", manifest_path);
2650                let schema = Self::manifest_schema();
2651                let empty_batch = RecordBatch::new_empty(schema.clone());
2652                let reader = RecordBatchIterator::new(vec![Ok(empty_batch)], schema.clone());
2653
2654                let store_params = ObjectStoreParams {
2655                    storage_options_accessor: storage_options.as_ref().map(|opts| {
2656                        Arc::new(
2657                            lance_io::object_store::StorageOptionsAccessor::with_static_options(
2658                                opts.clone(),
2659                            ),
2660                        )
2661                    }),
2662                    ..Default::default()
2663                };
2664                let write_params = WriteParams {
2665                    session: session.clone(),
2666                    store_params: Some(store_params),
2667                    ..Default::default()
2668                };
2669
2670                let dataset =
2671                    Dataset::write(Box::new(reader), &manifest_path, Some(write_params)).await;
2672
2673                // Handle race condition where another process created the manifest concurrently
2674                match dataset {
2675                    Ok(dataset) => {
2676                        log::info!(
2677                            "Successfully created manifest table at {}, version={}, uri={}",
2678                            manifest_path,
2679                            dataset.version().version,
2680                            dataset.uri()
2681                        );
2682                        Ok(DatasetConsistencyWrapper::new(dataset))
2683                    }
2684                    Err(ref e)
2685                        if matches!(
2686                            e,
2687                            LanceError::DatasetAlreadyExists { .. }
2688                                | LanceError::CommitConflict { .. }
2689                                | LanceError::IncompatibleTransaction { .. }
2690                                | LanceError::RetryableCommitConflict { .. }
2691                        ) =>
2692                    {
2693                        // Another process created the manifest concurrently, try to load it
2694                        log::info!(
2695                            "Manifest table was created by another process, loading it: {}",
2696                            manifest_path
2697                        );
2698                        let recovery_store_options = ObjectStoreParams {
2699                            storage_options_accessor: storage_options.as_ref().map(|opts| {
2700                                Arc::new(
2701                                    lance_io::object_store::StorageOptionsAccessor::with_static_options(
2702                                        opts.clone(),
2703                                    ),
2704                                )
2705                            }),
2706                            ..Default::default()
2707                        };
2708                        let recovery_read_params = ReadParams {
2709                            session,
2710                            store_options: Some(recovery_store_options),
2711                            ..Default::default()
2712                        };
2713                        let dataset = DatasetBuilder::from_uri(&manifest_path)
2714                            .with_read_params(recovery_read_params)
2715                            .load()
2716                            .await
2717                            .map_err(|e| {
2718                                lance_core::Error::from(NamespaceError::Internal {
2719                                    message: format!(
2720                                        "Failed to load manifest dataset after creation conflict: {}",
2721                                        e
2722                                    ),
2723                                })
2724                            })?;
2725                        Ok(DatasetConsistencyWrapper::new(dataset))
2726                    }
2727                    Err(e) => Err(lance_core::Error::from(NamespaceError::Internal {
2728                        message: format!("Failed to create manifest dataset: {:?}", e),
2729                    })),
2730                }
2731            }
2732            Err(err) => Err(err),
2733        }
2734    }
2735
2736    /// Sorts names alphabetically and applies pagination using page_token (start_after) and limit.
2737    ///
2738    /// Returns the next page token (last item in this page) if more results exist beyond the limit,
2739    /// or `None` if this is the last page.
2740    fn apply_pagination(
2741        names: &mut Vec<String>,
2742        page_token: Option<String>,
2743        limit: Option<i32>,
2744    ) -> Option<String> {
2745        names.sort();
2746
2747        if let Some(start_after) = page_token {
2748            if let Some(index) = names
2749                .iter()
2750                .position(|name| name.as_str() > start_after.as_str())
2751            {
2752                names.drain(0..index);
2753            } else {
2754                names.clear();
2755            }
2756        }
2757
2758        if let Some(limit) = limit
2759            && limit >= 0
2760        {
2761            let limit = limit as usize;
2762            if names.len() > limit {
2763                let next_page_token = if limit > 0 {
2764                    Some(names[limit - 1].clone())
2765                } else {
2766                    None
2767                };
2768                names.truncate(limit);
2769                return next_page_token;
2770            }
2771        }
2772
2773        None
2774    }
2775}
2776
2777#[async_trait]
2778impl LanceNamespace for ManifestNamespace {
2779    fn namespace_id(&self) -> String {
2780        self.root.clone()
2781    }
2782
2783    async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
2784        let namespace_id = request.id.as_ref().ok_or_else(|| {
2785            lance_core::Error::from(NamespaceError::InvalidInput {
2786                message: "Namespace ID is required".to_string(),
2787            })
2788        })?;
2789
2790        // Build filter to find tables in this namespace
2791        let filter = if namespace_id.is_empty() {
2792            // Root namespace: find tables without a namespace prefix
2793            "object_type = 'table' AND NOT contains(object_id, '$')".to_string()
2794        } else {
2795            // Namespaced: find tables that start with namespace$ but have no additional $
2796            let prefix = namespace_id.join(DELIMITER);
2797            format!(
2798                "object_type = 'table' AND starts_with(object_id, '{}{}') AND NOT contains(substring(object_id, {}), '$')",
2799                prefix,
2800                DELIMITER,
2801                prefix.len() + 2
2802            )
2803        };
2804
2805        let mut scanner = self.manifest_scanner().await?;
2806        scanner.filter(&filter).map_err(|e| {
2807            lance_core::Error::from(NamespaceError::Internal {
2808                message: format!("Failed to filter: {:?}", e),
2809            })
2810        })?;
2811        scanner.project(&["object_id", "location"]).map_err(|e| {
2812            lance_core::Error::from(NamespaceError::Internal {
2813                message: format!("Failed to project: {:?}", e),
2814            })
2815        })?;
2816
2817        let batches = Self::execute_scanner(scanner).await?;
2818
2819        let mut table_entries = Vec::new();
2820        for batch in batches {
2821            if batch.num_rows() == 0 {
2822                continue;
2823            }
2824
2825            let object_id_array = Self::get_string_column(&batch, "object_id")?;
2826            let location_array = Self::get_string_column(&batch, "location")?;
2827            for i in 0..batch.num_rows() {
2828                let object_id = object_id_array.value(i);
2829                let location = location_array.value(i);
2830                let (_namespace, name) = Self::parse_object_id(object_id);
2831                table_entries.push((name, location.to_string()));
2832            }
2833        }
2834
2835        let mut tables: Vec<String> = if request.include_declared.unwrap_or(true) {
2836            table_entries.into_iter().map(|(name, _)| name).collect()
2837        } else {
2838            let mut stream = futures::stream::iter(table_entries.into_iter().map(
2839                |(name, location)| async move {
2840                    // `include_declared=false` is an explicit opt-in. We still pay one
2841                    // `_versions/` probe per table so declared-state is derived from actual
2842                    // manifests. This is linear in the total number of listed tables, and we do
2843                    // the probes with bounded concurrency before pagination.
2844                    if self.location_has_actual_manifests(&location).await? {
2845                        Ok::<Option<String>, Error>(Some(name))
2846                    } else {
2847                        Ok::<Option<String>, Error>(None)
2848                    }
2849                },
2850            ))
2851            .buffered(DECLARED_FILTER_CONCURRENCY);
2852
2853            let mut filtered = Vec::new();
2854            while let Some(result) = stream.next().await {
2855                if let Some(name) = result? {
2856                    filtered.push(name);
2857                }
2858            }
2859            filtered
2860        };
2861
2862        let next_page_token =
2863            Self::apply_pagination(&mut tables, request.page_token, request.limit);
2864        let mut response = ListTablesResponse::new(tables);
2865        response.page_token = next_page_token;
2866        Ok(response)
2867    }
2868
2869    async fn describe_table(&self, request: DescribeTableRequest) -> Result<DescribeTableResponse> {
2870        let table_id = request.id.as_ref().ok_or_else(|| {
2871            lance_core::Error::from(NamespaceError::InvalidInput {
2872                message: "Table ID is required".to_string(),
2873            })
2874        })?;
2875
2876        if table_id.is_empty() {
2877            return Err(NamespaceError::InvalidInput {
2878                message: "Table ID cannot be empty".to_string(),
2879            }
2880            .into());
2881        }
2882
2883        let object_id = Self::str_object_id(table_id);
2884        let table_info = self.query_manifest_for_table(&object_id).boxed().await?;
2885
2886        // Extract table name and namespace from table_id
2887        let table_name = table_id.last().cloned().unwrap_or_default();
2888        let namespace_id: Vec<String> = if table_id.len() > 1 {
2889            table_id[..table_id.len() - 1].to_vec()
2890        } else {
2891            vec![]
2892        };
2893
2894        let load_detailed_metadata = request.load_detailed_metadata.unwrap_or(false);
2895        let should_check_declared =
2896            load_detailed_metadata || request.check_declared.unwrap_or(false);
2897        // For backwards compatibility, only skip vending credentials when explicitly set to false
2898        let vend_credentials = request.vend_credentials.unwrap_or(true);
2899
2900        match table_info {
2901            Some(info) => {
2902                // Construct full URI from relative location
2903                let table_uri = Self::construct_full_uri(&self.root, &info.location)?;
2904
2905                let storage_options = if vend_credentials {
2906                    self.storage_options.clone()
2907                } else {
2908                    None
2909                };
2910                let is_only_declared = if should_check_declared {
2911                    Some(!self.location_has_actual_manifests(&info.location).await?)
2912                } else {
2913                    None
2914                };
2915
2916                if !load_detailed_metadata {
2917                    return Ok(DescribeTableResponse {
2918                        table: Some(table_name),
2919                        namespace: Some(namespace_id),
2920                        location: Some(table_uri.clone()),
2921                        table_uri: Some(table_uri),
2922                        storage_options,
2923                        properties: info.metadata,
2924                        is_only_declared,
2925                        ..Default::default()
2926                    });
2927                }
2928
2929                if is_only_declared == Some(true) {
2930                    return Ok(DescribeTableResponse {
2931                        table: Some(table_name),
2932                        namespace: Some(namespace_id),
2933                        location: Some(table_uri.clone()),
2934                        table_uri: Some(table_uri),
2935                        storage_options,
2936                        properties: info.metadata,
2937                        is_only_declared,
2938                        ..Default::default()
2939                    });
2940                }
2941
2942                let mut builder = DatasetBuilder::from_uri(&table_uri);
2943                if let Some(opts) = &self.storage_options {
2944                    builder = builder.with_storage_options(opts.clone());
2945                }
2946                if let Some(session) = &self.session {
2947                    builder = builder.with_session(session.clone());
2948                }
2949
2950                match builder.load().await {
2951                    Ok(mut dataset) => {
2952                        // If a specific version is requested, checkout that version
2953                        if let Some(requested_version) = request.version {
2954                            dataset = dataset.checkout_version(requested_version as u64).await?;
2955                        }
2956
2957                        let version = dataset.version().version;
2958                        let lance_schema = dataset.schema();
2959                        let arrow_schema: arrow_schema::Schema = lance_schema.into();
2960                        let json_schema = arrow_schema_to_json(&arrow_schema)?;
2961
2962                        Ok(DescribeTableResponse {
2963                            table: Some(table_name.clone()),
2964                            namespace: Some(namespace_id.clone()),
2965                            version: Some(version as i64),
2966                            location: Some(table_uri.clone()),
2967                            table_uri: Some(table_uri),
2968                            schema: Some(Box::new(json_schema)),
2969                            storage_options,
2970                            properties: info.metadata.clone(),
2971                            is_only_declared,
2972                            ..Default::default()
2973                        })
2974                    }
2975                    Err(err) => Err(NamespaceError::Internal {
2976                        message: format!(
2977                            "Table exists in manifest but failed to load dataset '{}': {}",
2978                            object_id, err
2979                        ),
2980                    }
2981                    .into()),
2982                }
2983            }
2984            None => Err(NamespaceError::TableNotFound {
2985                message: Self::format_table_id(table_id),
2986            }
2987            .into()),
2988        }
2989    }
2990
2991    async fn table_exists(&self, request: TableExistsRequest) -> Result<()> {
2992        let table_id = request.id.as_ref().ok_or_else(|| {
2993            lance_core::Error::from(NamespaceError::InvalidInput {
2994                message: "Table ID is required".to_string(),
2995            })
2996        })?;
2997
2998        if table_id.is_empty() {
2999            return Err(NamespaceError::InvalidInput {
3000                message: "Table ID cannot be empty".to_string(),
3001            }
3002            .into());
3003        }
3004
3005        let object_id = Self::str_object_id(table_id);
3006        let exists = self.query_manifest_for_table(&object_id).await?.is_some();
3007        if exists {
3008            Ok(())
3009        } else {
3010            Err(NamespaceError::TableNotFound {
3011                message: Self::format_table_id(table_id),
3012            }
3013            .into())
3014        }
3015    }
3016
3017    async fn create_table(
3018        &self,
3019        request: CreateTableRequest,
3020        data: Bytes,
3021    ) -> Result<CreateTableResponse> {
3022        let table_id = request.id.as_ref().ok_or_else(|| {
3023            lance_core::Error::from(NamespaceError::InvalidInput {
3024                message: "Table ID is required".to_string(),
3025            })
3026        })?;
3027
3028        if table_id.is_empty() {
3029            return Err(NamespaceError::InvalidInput {
3030                message: "Table ID cannot be empty".to_string(),
3031            }
3032            .into());
3033        }
3034
3035        let (namespace, table_name) = Self::split_object_id(table_id);
3036        let object_id = Self::build_object_id(&namespace, &table_name);
3037
3038        // Refuse before writing any table data if this build cannot write the
3039        // manifest, so a refused create leaves no orphaned dataset behind.
3040        self.ensure_manifest_writable().await?;
3041
3042        let existing_table = self.query_manifest_for_table(&object_id).await?;
3043        // Tables and namespaces share object ids. We refuse an id that a
3044        // namespace holds before we write any data.
3045        if existing_table.is_none() && self.manifest_contains_object(&object_id).await? {
3046            return Err(NamespaceError::TableAlreadyExists {
3047                message: table_name,
3048            }
3049            .into());
3050        }
3051        let existing_has_manifests = if let Some(existing_table) = &existing_table {
3052            Some(
3053                self.location_has_actual_manifests(&existing_table.location)
3054                    .await?,
3055            )
3056        } else {
3057            None
3058        };
3059
3060        if existing_has_manifests == Some(false)
3061            && request
3062                .properties
3063                .as_ref()
3064                .is_some_and(|properties| !properties.is_empty())
3065        {
3066            return Err(NamespaceError::InvalidInput {
3067                message: format!(
3068                    "create_table cannot set properties for already declared table '{}'",
3069                    object_id
3070                ),
3071            }
3072            .into());
3073        }
3074
3075        let create_mode = if existing_has_manifests == Some(false) {
3076            CreateTableMode::Create
3077        } else {
3078            CreateTableMode::parse(request.mode.as_deref())?
3079        };
3080        let dir_name = if let Some(existing_table) = &existing_table {
3081            existing_table.location.clone()
3082        } else if namespace.is_empty() && self.dir_listing_enabled {
3083            format!("{}.lance", table_name)
3084        } else {
3085            Self::generate_dir_name(&object_id)
3086        };
3087        let table_uri = Self::construct_full_uri(&self.root, &dir_name)?;
3088        let overwriting_existing_table =
3089            existing_has_manifests == Some(true) && create_mode == CreateTableMode::Overwrite;
3090
3091        if existing_has_manifests == Some(true) {
3092            match create_mode {
3093                CreateTableMode::Create => {
3094                    return Err(NamespaceError::TableAlreadyExists {
3095                        message: table_name.clone(),
3096                    }
3097                    .into());
3098                }
3099                CreateTableMode::ExistOk => {
3100                    let properties = existing_table
3101                        .as_ref()
3102                        .and_then(|table| table.metadata.clone());
3103                    return Ok(CreateTableResponse {
3104                        location: Some(table_uri),
3105                        storage_options: self.storage_options.clone(),
3106                        properties,
3107                        ..Default::default()
3108                    });
3109                }
3110                CreateTableMode::Overwrite => {}
3111            }
3112        }
3113
3114        // Validate that request_data is provided
3115        if data.is_empty() {
3116            return Err(NamespaceError::InvalidInput {
3117                message: "Request data (Arrow IPC stream) is required for create_table".to_string(),
3118            }
3119            .into());
3120        }
3121
3122        // Write the data using Lance Dataset
3123        let cursor = Cursor::new(data.to_vec());
3124        let stream_reader = StreamReader::try_new(cursor, None).map_err(|e| {
3125            lance_core::Error::from(NamespaceError::Internal {
3126                message: format!("Failed to read IPC stream: {:?}", e),
3127            })
3128        })?;
3129
3130        let batches: Vec<RecordBatch> = stream_reader
3131            .collect::<std::result::Result<Vec<_>, _>>()
3132            .map_err(|e| {
3133            lance_core::Error::from(NamespaceError::Internal {
3134                message: format!("Failed to collect batches: {:?}", e),
3135            })
3136        })?;
3137
3138        if batches.is_empty() {
3139            return Err(NamespaceError::Internal {
3140                message: "No data provided for table creation".to_string(),
3141            }
3142            .into());
3143        }
3144
3145        let schema = batches[0].schema();
3146        let batch_results: Vec<std::result::Result<RecordBatch, arrow_schema::ArrowError>> =
3147            batches.into_iter().map(Ok).collect();
3148        let reader = RecordBatchIterator::new(batch_results, schema);
3149
3150        let mut write_storage_options = self.storage_options.clone().unwrap_or_default();
3151        if let Some(request_storage_options) = request.storage_options.as_ref() {
3152            write_storage_options.extend(request_storage_options.clone());
3153        }
3154
3155        let store_params = ObjectStoreParams {
3156            storage_options_accessor: (!write_storage_options.is_empty()).then(|| {
3157                Arc::new(
3158                    lance_io::object_store::StorageOptionsAccessor::with_static_options(
3159                        write_storage_options,
3160                    ),
3161                )
3162            }),
3163            ..Default::default()
3164        };
3165        let write_params = WriteParams {
3166            mode: create_mode.write_mode(),
3167            session: self.session.clone(),
3168            store_params: Some(store_params),
3169            ..Default::default()
3170        };
3171        let dataset = Dataset::write(Box::new(reader), &table_uri, Some(write_params))
3172            .await
3173            .map_err(|e| {
3174                lance_core::Error::from(NamespaceError::Internal {
3175                    message: format!("Failed to write dataset: {:?}", e),
3176                })
3177            })?;
3178        let version = dataset.version().version as i64;
3179
3180        if overwriting_existing_table {
3181            let metadata =
3182                Self::serialize_metadata(request.properties.as_ref(), "table", &object_id)?;
3183            self.upsert_into_manifest_with_metadata(
3184                vec![ManifestEntry {
3185                    object_id,
3186                    object_type: ObjectType::Table,
3187                    location: Some(dir_name),
3188                    metadata,
3189                }],
3190                None,
3191            )
3192            .await?;
3193
3194            Ok(CreateTableResponse {
3195                version: Some(version),
3196                location: Some(table_uri),
3197                storage_options: self.storage_options.clone(),
3198                properties: request.properties,
3199                ..Default::default()
3200            })
3201        } else {
3202            match existing_table {
3203                Some(existing_table) => Ok(CreateTableResponse {
3204                    version: Some(version),
3205                    location: Some(table_uri),
3206                    storage_options: self.storage_options.clone(),
3207                    properties: existing_table.metadata,
3208                    ..Default::default()
3209                }),
3210                None => {
3211                    let metadata =
3212                        Self::serialize_metadata(request.properties.as_ref(), "table", &object_id)?;
3213                    // Register in manifest (store dir_name, not full URI)
3214                    self.insert_into_manifest_with_metadata(
3215                        vec![ManifestEntry {
3216                            object_id,
3217                            object_type: ObjectType::Table,
3218                            location: Some(dir_name.clone()),
3219                            metadata,
3220                        }],
3221                        None,
3222                    )
3223                    .await?;
3224
3225                    Ok(CreateTableResponse {
3226                        version: Some(version),
3227                        location: Some(table_uri),
3228                        storage_options: self.storage_options.clone(),
3229                        properties: request.properties,
3230                        ..Default::default()
3231                    })
3232                }
3233            }
3234        }
3235    }
3236
3237    async fn drop_table(&self, request: DropTableRequest) -> Result<DropTableResponse> {
3238        let table_id = request.id.as_ref().ok_or_else(|| {
3239            lance_core::Error::from(NamespaceError::InvalidInput {
3240                message: "Table ID is required".to_string(),
3241            })
3242        })?;
3243
3244        if table_id.is_empty() {
3245            return Err(NamespaceError::InvalidInput {
3246                message: "Table ID cannot be empty".to_string(),
3247            }
3248            .into());
3249        }
3250
3251        let (namespace, table_name) = Self::split_object_id(table_id);
3252        let object_id = Self::build_object_id(&namespace, &table_name);
3253
3254        // Query manifest for table location
3255        let table_info = self.query_manifest_for_table(&object_id).boxed().await?;
3256
3257        match table_info {
3258            Some(info) => {
3259                // Delete from manifest first
3260                self.delete_from_manifest(&object_id).boxed().await?;
3261
3262                // Delete physical data directory using the dir_name from manifest
3263                let table_path = self.base_path.clone().join(info.location.as_str());
3264                let table_uri = Self::construct_full_uri(&self.root, &info.location)?;
3265
3266                // Remove the table directory
3267                self.object_store
3268                    .remove_dir_all(table_path)
3269                    .boxed()
3270                    .await
3271                    .map_err(|e| {
3272                        lance_core::Error::from(NamespaceError::Internal {
3273                            message: format!("Failed to delete table directory: {:?}", e),
3274                        })
3275                    })?;
3276
3277                Ok(DropTableResponse {
3278                    id: request.id.clone(),
3279                    location: Some(table_uri),
3280                    ..Default::default()
3281                })
3282            }
3283            None => Err(NamespaceError::TableNotFound {
3284                message: table_name.to_string(),
3285            }
3286            .into()),
3287        }
3288    }
3289
3290    async fn list_namespaces(
3291        &self,
3292        request: ListNamespacesRequest,
3293    ) -> Result<ListNamespacesResponse> {
3294        let parent_namespace = request.id.as_ref().ok_or_else(|| {
3295            lance_core::Error::from(NamespaceError::InvalidInput {
3296                message: "Namespace ID is required".to_string(),
3297            })
3298        })?;
3299
3300        // Build filter to find direct child namespaces
3301        let filter = if parent_namespace.is_empty() {
3302            // Root namespace: find all namespaces without a parent
3303            "object_type = 'namespace' AND NOT contains(object_id, '$')".to_string()
3304        } else {
3305            // Non-root: find namespaces that start with parent$ but have no additional $
3306            let prefix = parent_namespace.join(DELIMITER);
3307            format!(
3308                "object_type = 'namespace' AND starts_with(object_id, '{}{}') AND NOT contains(substring(object_id, {}), '$')",
3309                prefix,
3310                DELIMITER,
3311                prefix.len() + 2
3312            )
3313        };
3314
3315        let mut scanner = self.manifest_scanner().await?;
3316        scanner.filter(&filter).map_err(|e| {
3317            lance_core::Error::from(NamespaceError::Internal {
3318                message: format!("Failed to filter: {:?}", e),
3319            })
3320        })?;
3321        scanner.project(&["object_id"]).map_err(|e| {
3322            lance_core::Error::from(NamespaceError::Internal {
3323                message: format!("Failed to project: {:?}", e),
3324            })
3325        })?;
3326
3327        let batches = Self::execute_scanner(scanner).await?;
3328        let mut namespaces = Vec::new();
3329
3330        for batch in batches {
3331            if batch.num_rows() == 0 {
3332                continue;
3333            }
3334
3335            let object_id_array = Self::get_string_column(&batch, "object_id")?;
3336            for i in 0..batch.num_rows() {
3337                let object_id = object_id_array.value(i);
3338                let (_namespace, name) = Self::parse_object_id(object_id);
3339                namespaces.push(name);
3340            }
3341        }
3342
3343        let next_page_token =
3344            Self::apply_pagination(&mut namespaces, request.page_token, request.limit);
3345        let mut response = ListNamespacesResponse::new(namespaces);
3346        response.page_token = next_page_token;
3347        Ok(response)
3348    }
3349
3350    async fn describe_namespace(
3351        &self,
3352        request: DescribeNamespaceRequest,
3353    ) -> Result<DescribeNamespaceResponse> {
3354        let namespace_id = request.id.as_ref().ok_or_else(|| {
3355            lance_core::Error::from(NamespaceError::InvalidInput {
3356                message: "Namespace ID is required".to_string(),
3357            })
3358        })?;
3359
3360        // Root namespace always exists
3361        if namespace_id.is_empty() {
3362            #[allow(clippy::needless_update)]
3363            return Ok(DescribeNamespaceResponse {
3364                properties: Some(HashMap::new()),
3365                ..Default::default()
3366            });
3367        }
3368
3369        // Check if namespace exists in manifest
3370        let object_id = namespace_id.join(DELIMITER);
3371        let namespace_info = self.query_manifest_for_namespace(&object_id).await?;
3372
3373        match namespace_info {
3374            #[allow(clippy::needless_update)]
3375            Some(info) => Ok(DescribeNamespaceResponse {
3376                properties: info.metadata,
3377                ..Default::default()
3378            }),
3379            None => Err(NamespaceError::NamespaceNotFound {
3380                message: object_id.to_string(),
3381            }
3382            .into()),
3383        }
3384    }
3385
3386    async fn create_namespace(
3387        &self,
3388        request: CreateNamespaceRequest,
3389    ) -> Result<CreateNamespaceResponse> {
3390        let namespace_id = request.id.as_ref().ok_or_else(|| {
3391            lance_core::Error::from(NamespaceError::InvalidInput {
3392                message: "Namespace ID is required".to_string(),
3393            })
3394        })?;
3395
3396        // Root namespace always exists and cannot be created
3397        if namespace_id.is_empty() {
3398            return Err(NamespaceError::NamespaceAlreadyExists {
3399                message: "root namespace".to_string(),
3400            }
3401            .into());
3402        }
3403
3404        // Validate parent namespaces exist (but not the namespace being created)
3405        if namespace_id.len() > 1 {
3406            self.validate_namespace_levels_exist(&namespace_id[..namespace_id.len() - 1])
3407                .await?;
3408        }
3409
3410        let object_id = namespace_id.join(DELIMITER);
3411        if self.manifest_contains_object(&object_id).await? {
3412            return Err(NamespaceError::NamespaceAlreadyExists {
3413                message: object_id.to_string(),
3414            }
3415            .into());
3416        }
3417
3418        let metadata =
3419            Self::serialize_metadata(request.properties.as_ref(), "namespace", &object_id)?;
3420
3421        self.insert_into_manifest_with_metadata(
3422            vec![ManifestEntry {
3423                object_id,
3424                object_type: ObjectType::Namespace,
3425                location: None,
3426                metadata,
3427            }],
3428            None,
3429        )
3430        .await?;
3431
3432        Ok(CreateNamespaceResponse {
3433            properties: request.properties,
3434            ..Default::default()
3435        })
3436    }
3437
3438    async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse> {
3439        let namespace_id = request.id.as_ref().ok_or_else(|| {
3440            lance_core::Error::from(NamespaceError::InvalidInput {
3441                message: "Namespace ID is required".to_string(),
3442            })
3443        })?;
3444
3445        // Root namespace always exists and cannot be dropped
3446        if namespace_id.is_empty() {
3447            return Err(NamespaceError::InvalidInput {
3448                message: "Root namespace cannot be dropped".to_string(),
3449            }
3450            .into());
3451        }
3452
3453        let object_id = namespace_id.join(DELIMITER);
3454
3455        // Check if namespace exists. A table's id is not a namespace, and
3456        // dropping it here would delete the table's entry.
3457        if self
3458            .query_manifest_for_namespace(&object_id)
3459            .boxed()
3460            .await?
3461            .is_none()
3462        {
3463            return Err(NamespaceError::NamespaceNotFound {
3464                message: object_id.to_string(),
3465            }
3466            .into());
3467        }
3468
3469        // Check for child namespaces
3470        let escaped_id = object_id.replace('\'', "''");
3471        let prefix = format!("{}{}", escaped_id, DELIMITER);
3472        let filter = format!("starts_with(object_id, '{}')", prefix);
3473        let mut scanner = self.manifest_scanner().boxed().await?;
3474        scanner.filter(&filter).map_err(|e| {
3475            lance_core::Error::from(NamespaceError::Internal {
3476                message: format!("Failed to filter: {:?}", e),
3477            })
3478        })?;
3479        scanner.project::<&str>(&[]).map_err(|e| {
3480            lance_core::Error::from(NamespaceError::Internal {
3481                message: format!("Failed to project: {:?}", e),
3482            })
3483        })?;
3484        scanner.with_row_id();
3485        let count = scanner.count_rows().boxed().await.map_err(|e| {
3486            lance_core::Error::from(NamespaceError::Internal {
3487                message: format!("Failed to count rows: {:?}", e),
3488            })
3489        })?;
3490
3491        if count > 0 {
3492            return Err(NamespaceError::NamespaceNotEmpty {
3493                message: format!("'{}' (contains {} child objects)", object_id, count),
3494            }
3495            .into());
3496        }
3497
3498        self.delete_from_manifest(&object_id).boxed().await?;
3499
3500        Ok(DropNamespaceResponse::default())
3501    }
3502
3503    async fn namespace_exists(&self, request: NamespaceExistsRequest) -> Result<()> {
3504        let namespace_id = request.id.as_ref().ok_or_else(|| {
3505            lance_core::Error::from(NamespaceError::InvalidInput {
3506                message: "Namespace ID is required".to_string(),
3507            })
3508        })?;
3509
3510        // Root namespace always exists
3511        if namespace_id.is_empty() {
3512            return Ok(());
3513        }
3514
3515        let object_id = namespace_id.join(DELIMITER);
3516        if self
3517            .query_manifest_for_namespace(&object_id)
3518            .await?
3519            .is_some()
3520        {
3521            Ok(())
3522        } else {
3523            Err(NamespaceError::NamespaceNotFound {
3524                message: object_id.to_string(),
3525            }
3526            .into())
3527        }
3528    }
3529
3530    async fn declare_table(&self, request: DeclareTableRequest) -> Result<DeclareTableResponse> {
3531        let table_id = request.id.as_ref().ok_or_else(|| {
3532            lance_core::Error::from(NamespaceError::InvalidInput {
3533                message: "Table ID is required".to_string(),
3534            })
3535        })?;
3536
3537        if table_id.is_empty() {
3538            return Err(NamespaceError::InvalidInput {
3539                message: "Table ID cannot be empty".to_string(),
3540            }
3541            .into());
3542        }
3543
3544        let (namespace, table_name) = Self::split_object_id(table_id);
3545        let object_id = Self::build_object_id(&namespace, &table_name);
3546
3547        // Tables and namespaces share object ids. We refuse an id that either
3548        // one holds before we write the marker.
3549        if self.manifest_contains_object(&object_id).await? {
3550            return Err(NamespaceError::TableAlreadyExists {
3551                message: table_name.to_string(),
3552            }
3553            .into());
3554        }
3555
3556        // Create table location path with hash-based naming
3557        // When dir_listing_enabled is true and it's a root table, use directory-style naming: {table_name}.lance
3558        // Otherwise, use hash-based naming: {hash}_{object_id}
3559        let dir_name = if namespace.is_empty() && self.dir_listing_enabled {
3560            // Root table with directory listing enabled: use {table_name}.lance
3561            format!("{}.lance", table_name)
3562        } else {
3563            // Child namespace table or dir listing disabled: use hash-based naming
3564            Self::generate_dir_name(&object_id)
3565        };
3566        let table_path = self.base_path.clone().join(dir_name.as_str());
3567        let table_uri = Self::construct_full_uri(&self.root, &dir_name)?;
3568
3569        // Validate location if provided
3570        if let Some(req_location) = &request.location {
3571            let req_location = req_location.trim_end_matches('/');
3572            if req_location != table_uri {
3573                return Err(NamespaceError::InvalidInput {
3574                    message: format!(
3575                        "Cannot declare table {} at location {}, must be at location {}",
3576                        table_name, req_location, table_uri
3577                    ),
3578                }
3579                .into());
3580            }
3581        }
3582
3583        self.ensure_manifest_writable().await?;
3584
3585        // Atomically create the .lance-reserved file to mark the table as declared.
3586        // Shared with DirectoryNamespace via put_marker_file_atomic (dotfile-safe
3587        // staging + MarkerFileError::AlreadyExists → TableAlreadyExists).
3588        let reserved_file_path = table_path.clone().join(".lance-reserved");
3589        super::put_marker_file_atomic(
3590            &self.object_store,
3591            &reserved_file_path,
3592            &format!("table {}", table_name),
3593        )
3594        .await
3595        .map_err(|e| match e {
3596            super::MarkerFileError::AlreadyExists { .. } => {
3597                lance_core::Error::from(NamespaceError::TableAlreadyExists {
3598                    message: table_name.to_string(),
3599                })
3600            }
3601            super::MarkerFileError::Other { message } => {
3602                lance_core::Error::from(NamespaceError::Internal { message })
3603            }
3604        })?;
3605
3606        let metadata = Self::serialize_metadata(request.properties.as_ref(), "table", &object_id)?;
3607
3608        // Add entry to manifest marking this as a declared table (store dir_name, not full path)
3609        self.insert_into_manifest_with_metadata(
3610            vec![ManifestEntry {
3611                object_id,
3612                object_type: ObjectType::Table,
3613                location: Some(dir_name),
3614                metadata,
3615            }],
3616            None,
3617        )
3618        .await?;
3619
3620        log::info!(
3621            "Declared table '{}' in manifest at {}",
3622            table_name,
3623            table_uri
3624        );
3625
3626        // For backwards compatibility, only skip vending credentials when explicitly set to false
3627        let vend_credentials = request.vend_credentials.unwrap_or(true);
3628        let storage_options = if vend_credentials {
3629            self.storage_options.clone()
3630        } else {
3631            None
3632        };
3633
3634        Ok(DeclareTableResponse {
3635            location: Some(table_uri),
3636            storage_options,
3637            properties: request.properties,
3638            ..Default::default()
3639        })
3640    }
3641
3642    async fn register_table(&self, request: RegisterTableRequest) -> Result<RegisterTableResponse> {
3643        let table_id = request.id.as_ref().ok_or_else(|| {
3644            lance_core::Error::from(NamespaceError::InvalidInput {
3645                message: "Table ID is required".to_string(),
3646            })
3647        })?;
3648
3649        if table_id.is_empty() {
3650            return Err(NamespaceError::InvalidInput {
3651                message: "Table ID cannot be empty".to_string(),
3652            }
3653            .into());
3654        }
3655
3656        let location = request.location.clone();
3657
3658        // Validate that location is a relative path within the root directory
3659        // We don't allow absolute URIs or paths that escape the root
3660        if location.contains("://") {
3661            return Err(NamespaceError::InvalidInput {
3662                message: format!(
3663                    "Absolute URIs are not allowed for register_table. Location must be a relative path within the root directory: {}",
3664                    location
3665                ),
3666            }
3667            .into());
3668        }
3669
3670        if location.starts_with('/') {
3671            return Err(NamespaceError::InvalidInput {
3672                message: format!(
3673                    "Absolute paths are not allowed for register_table. Location must be a relative path within the root directory: {}",
3674                    location
3675                ),
3676            }
3677            .into());
3678        }
3679
3680        // Check for path traversal attempts
3681        if location.contains("..") {
3682            return Err(NamespaceError::InvalidInput {
3683                message: format!(
3684                    "Path traversal is not allowed. Location must be a relative path within the root directory: {}",
3685                    location
3686                ),
3687            }
3688            .into());
3689        }
3690
3691        let (namespace, table_name) = Self::split_object_id(table_id);
3692        let object_id = Self::build_object_id(&namespace, &table_name);
3693
3694        // Validate that parent namespaces exist (if not root)
3695        if !namespace.is_empty() {
3696            self.validate_namespace_levels_exist(&namespace).await?;
3697        }
3698
3699        // Check if table already exists
3700        if self.manifest_contains_object(&object_id).await? {
3701            return Err(NamespaceError::TableAlreadyExists {
3702                message: object_id.to_string(),
3703            }
3704            .into());
3705        }
3706
3707        // Register the table with its location in the manifest
3708        self.insert_into_manifest(object_id, ObjectType::Table, Some(location.clone()))
3709            .await?;
3710
3711        Ok(RegisterTableResponse {
3712            location: Some(location),
3713            ..Default::default()
3714        })
3715    }
3716
3717    async fn deregister_table(
3718        &self,
3719        request: DeregisterTableRequest,
3720    ) -> Result<DeregisterTableResponse> {
3721        let table_id = request.id.as_ref().ok_or_else(|| {
3722            lance_core::Error::from(NamespaceError::InvalidInput {
3723                message: "Table ID is required".to_string(),
3724            })
3725        })?;
3726
3727        if table_id.is_empty() {
3728            return Err(NamespaceError::InvalidInput {
3729                message: "Table ID cannot be empty".to_string(),
3730            }
3731            .into());
3732        }
3733
3734        let (namespace, table_name) = Self::split_object_id(table_id);
3735        let object_id = Self::build_object_id(&namespace, &table_name);
3736
3737        // Get table info before deleting
3738        let table_info = self.query_manifest_for_table(&object_id).await?;
3739
3740        let table_uri = match table_info {
3741            Some(info) => {
3742                // Delete from manifest only (leave physical data intact)
3743                self.delete_from_manifest(&object_id).boxed().await?;
3744                Self::construct_full_uri(&self.root, &info.location)?
3745            }
3746            None => {
3747                return Err(NamespaceError::TableNotFound {
3748                    message: object_id.to_string(),
3749                }
3750                .into());
3751            }
3752        };
3753
3754        Ok(DeregisterTableResponse {
3755            id: request.id.clone(),
3756            location: Some(table_uri),
3757            ..Default::default()
3758        })
3759    }
3760
3761    /// Add columns to a table.
3762    ///
3763    /// Converts the API `AddColumnsEntry` (SQL expressions) into Lance's
3764    /// `NewColumnTransform::SqlExpressions` and delegates to `Dataset::add_columns`.
3765    async fn alter_table_add_columns(
3766        &self,
3767        request: AlterTableAddColumnsRequest,
3768    ) -> Result<AlterTableAddColumnsResponse> {
3769        let table_id = request
3770            .id
3771            .as_ref()
3772            .ok_or_else(|| Error::invalid_input_source("Table ID is required".into()))?;
3773
3774        if table_id.is_empty() {
3775            return Err(Error::invalid_input_source(
3776                "Table ID cannot be empty".into(),
3777            ));
3778        }
3779
3780        let object_id = Self::str_object_id(table_id);
3781        let table_info = self.query_manifest_for_table(&object_id).boxed().await?;
3782
3783        match table_info {
3784            Some(info) => {
3785                let table_uri = Self::construct_full_uri(&self.root, &info.location)?;
3786                // Use DatasetBuilder with storage options to align with describe_table
3787                // and to support custom storage backends (e.g. S3 with custom endpoints).
3788                let mut builder = DatasetBuilder::from_uri(&table_uri);
3789                if let Some(opts) = &self.storage_options {
3790                    builder = builder.with_storage_options(opts.clone());
3791                }
3792                if let Some(session) = &self.session {
3793                    builder = builder.with_session(session.clone());
3794                }
3795                let mut dataset = builder.load().await.map_err(|e| {
3796                    Error::io_source(box_error(std::io::Error::other(format!(
3797                        "Failed to open dataset: {}",
3798                        e
3799                    ))))
3800                })?;
3801
3802                // Use shared helper to build SQL expressions, ensuring a clear error when expression is missing
3803                let sql_expressions = super::build_sql_expressions(&request.new_columns)?;
3804
3805                dataset
3806                    .add_columns(
3807                        lance::dataset::NewColumnTransform::SqlExpressions(sql_expressions),
3808                        None,
3809                        None,
3810                    )
3811                    .await
3812                    .map_err(|e| {
3813                        // Surface specific commit/conflict errors (CommitConflict,
3814                        // RetryableCommitConflict, IncompatibleTransaction, ...) rather than
3815                        // collapsing every failure into a generic IO error.
3816                        convert_lance_commit_error(&e, "add_columns", Some(&object_id))
3817                    })?;
3818
3819                let version = dataset.version().version as i64;
3820                Ok(AlterTableAddColumnsResponse::new(version))
3821            }
3822            None => Err(NamespaceError::TableNotFound { message: object_id }.into()),
3823        }
3824    }
3825
3826    /// Alter columns in a table (rename, change type, change nullability).
3827    ///
3828    /// Converts the API `AlterColumnsEntry` into Lance's `ColumnAlteration`
3829    /// and delegates to `Dataset::alter_columns`.
3830    async fn alter_table_alter_columns(
3831        &self,
3832        request: AlterTableAlterColumnsRequest,
3833    ) -> Result<AlterTableAlterColumnsResponse> {
3834        let table_id = request
3835            .id
3836            .as_ref()
3837            .ok_or_else(|| Error::invalid_input_source("Table ID is required".into()))?;
3838
3839        if table_id.is_empty() {
3840            return Err(Error::invalid_input_source(
3841                "Table ID cannot be empty".into(),
3842            ));
3843        }
3844
3845        let object_id = Self::str_object_id(table_id);
3846        let table_info = self.query_manifest_for_table(&object_id).boxed().await?;
3847
3848        match table_info {
3849            Some(info) => {
3850                let table_uri = Self::construct_full_uri(&self.root, &info.location)?;
3851                let mut builder = DatasetBuilder::from_uri(&table_uri);
3852                if let Some(opts) = &self.storage_options {
3853                    builder = builder.with_storage_options(opts.clone());
3854                }
3855                if let Some(session) = &self.session {
3856                    builder = builder.with_session(session.clone());
3857                }
3858                let mut dataset = builder.load().await.map_err(|e| {
3859                    Error::io_source(box_error(std::io::Error::other(format!(
3860                        "Failed to open dataset: {}",
3861                        e
3862                    ))))
3863                })?;
3864
3865                // Use shared helper to build column alterations, ensuring a clear error when data_type conversion fails
3866                let alterations = super::build_column_alterations(&request.alterations)?;
3867
3868                dataset.alter_columns(&alterations).await.map_err(|e| {
3869                    convert_lance_commit_error(&e, "alter_columns", Some(&object_id))
3870                })?;
3871
3872                let version = dataset.version().version as i64;
3873                Ok(AlterTableAlterColumnsResponse::new(version))
3874            }
3875            None => Err(NamespaceError::TableNotFound { message: object_id }.into()),
3876        }
3877    }
3878
3879    /// Drop columns from a table.
3880    ///
3881    /// Delegates to `Dataset::drop_columns` with the column names from the request.
3882    async fn alter_table_drop_columns(
3883        &self,
3884        request: AlterTableDropColumnsRequest,
3885    ) -> Result<AlterTableDropColumnsResponse> {
3886        let table_id = request
3887            .id
3888            .as_ref()
3889            .ok_or_else(|| Error::invalid_input_source("Table ID is required".into()))?;
3890
3891        if table_id.is_empty() {
3892            return Err(Error::invalid_input_source(
3893                "Table ID cannot be empty".into(),
3894            ));
3895        }
3896
3897        let object_id = Self::str_object_id(table_id);
3898        let table_info = self.query_manifest_for_table(&object_id).boxed().await?;
3899
3900        match table_info {
3901            Some(info) => {
3902                let table_uri = Self::construct_full_uri(&self.root, &info.location)?;
3903                let mut builder = DatasetBuilder::from_uri(&table_uri);
3904                if let Some(opts) = &self.storage_options {
3905                    builder = builder.with_storage_options(opts.clone());
3906                }
3907                if let Some(session) = &self.session {
3908                    builder = builder.with_session(session.clone());
3909                }
3910                let mut dataset = builder.load().await.map_err(|e| {
3911                    Error::io_source(box_error(std::io::Error::other(format!(
3912                        "Failed to open dataset: {}",
3913                        e
3914                    ))))
3915                })?;
3916
3917                let columns: Vec<&str> = request.columns.iter().map(|s| s.as_str()).collect();
3918                dataset.drop_columns(&columns).await.map_err(|e| {
3919                    convert_lance_commit_error(&e, "drop_columns", Some(&object_id))
3920                })?;
3921
3922                let version = dataset.version().version as i64;
3923                Ok(AlterTableDropColumnsResponse::new(version))
3924            }
3925            None => Err(NamespaceError::TableNotFound { message: object_id }.into()),
3926        }
3927    }
3928}
3929
3930#[cfg(test)]
3931mod tests {
3932    use super::{
3933        BASE_OBJECTS_INDEX_NAME, ConflictResolution, CopyOnWriteMutation,
3934        DatasetConsistencyWrapper, DeleteObjectMutation, LANCE_DATA_DIR, LANCE_INDICES_DIR,
3935        MANIFEST_TABLE_NAME, ManifestBatchBuilder, ManifestEntry, ManifestIndexAccumulator,
3936        ManifestNamespace, ManifestOutputRow, ManifestRowValue, ManifestStreamMutation,
3937        OBJECT_ID_INDEX_NAME, OBJECT_TYPE_INDEX_NAME, ObjectType, has_auto_cleanup_config,
3938    };
3939    use crate::{DirectoryNamespace, DirectoryNamespaceBuilder};
3940    use arrow::datatypes::DataType;
3941    use bytes::Bytes;
3942    use futures::StreamExt;
3943    use lance::dataset::builder::DatasetBuilder;
3944    use lance::dataset::cleanup::CleanupPolicyBuilder;
3945    use lance::index::DatasetIndexExt;
3946    use lance_core::utils::tempfile::TempStdDir;
3947    use lance_io::object_store::{ObjectStore, ObjectStoreParams, ObjectStoreRegistry};
3948    use lance_namespace::LanceNamespace;
3949    use lance_namespace::models::{
3950        CreateNamespaceRequest, CreateTableRequest, DeclareTableRequest, DescribeTableRequest,
3951        DropTableRequest, ListTablesRequest, TableExistsRequest,
3952    };
3953    use lance_table::feature_flags::FLAG_UNKNOWN;
3954    use lance_table::format::Fragment;
3955    use rstest::rstest;
3956    use std::collections::{HashMap, HashSet};
3957    use std::sync::Arc;
3958    use std::time::Duration;
3959    use tokio::sync::oneshot;
3960    use tokio::time::timeout;
3961
3962    async fn create_manifest_namespace(
3963        root: &str,
3964        inline_optimization_enabled: bool,
3965    ) -> ManifestNamespace {
3966        create_manifest_namespace_with_retries(root, inline_optimization_enabled, None).await
3967    }
3968
3969    async fn create_manifest_namespace_with_retries(
3970        root: &str,
3971        inline_optimization_enabled: bool,
3972        commit_retries: Option<u32>,
3973    ) -> ManifestNamespace {
3974        let (object_store, base_path) = ObjectStore::from_uri_and_params(
3975            Arc::new(ObjectStoreRegistry::default()),
3976            root,
3977            &ObjectStoreParams::default(),
3978        )
3979        .await
3980        .unwrap();
3981        ManifestNamespace::from_directory(
3982            root.to_string(),
3983            None,
3984            None,
3985            object_store,
3986            base_path,
3987            true,
3988            inline_optimization_enabled,
3989            commit_retries,
3990        )
3991        .await
3992        .unwrap()
3993    }
3994
3995    struct CommitConflictAfterRewriteMutation {
3996        root: String,
3997        conflict_object_id: String,
3998    }
3999
4000    impl ManifestStreamMutation for CommitConflictAfterRewriteMutation {
4001        type Output = ();
4002
4003        fn process_existing_row(
4004            &mut self,
4005            row: ManifestRowValue,
4006            output: &mut ManifestBatchBuilder,
4007            index_data: &mut ManifestIndexAccumulator,
4008        ) -> lance_core::Result<()> {
4009            output.append(
4010                index_data,
4011                ManifestOutputRow {
4012                    object_id: &row.object_id,
4013                    object_type: row.object_type,
4014                    location: row.location.as_deref(),
4015                    metadata: row.metadata.as_deref(),
4016                    base_objects: row.base_objects.as_deref(),
4017                },
4018            )
4019        }
4020
4021        fn append_rows(
4022            &mut self,
4023            output: &mut ManifestBatchBuilder,
4024            index_data: &mut ManifestIndexAccumulator,
4025        ) -> lance_core::Result<()> {
4026            output.append(
4027                index_data,
4028                ManifestOutputRow {
4029                    object_id: "attempted_table",
4030                    object_type: ObjectType::Table,
4031                    location: Some("attempted_table.lance"),
4032                    metadata: None,
4033                    base_objects: None,
4034                },
4035            )
4036        }
4037
4038        fn finish(&self) -> CopyOnWriteMutation<Self::Output> {
4039            let root = self.root.clone();
4040            let object_id = self.conflict_object_id.clone();
4041            std::thread::spawn(move || {
4042                let runtime = tokio::runtime::Runtime::new().unwrap();
4043                runtime.block_on(async move {
4044                    let writer = create_manifest_namespace(&root, false).await;
4045                    writer
4046                        .insert_into_manifest_with_metadata(
4047                            vec![ManifestEntry {
4048                                object_id,
4049                                object_type: ObjectType::Table,
4050                                location: Some("conflicting_table.lance".to_string()),
4051                                metadata: None,
4052                            }],
4053                            None,
4054                        )
4055                        .await
4056                        .unwrap();
4057                });
4058            })
4059            .join()
4060            .unwrap();
4061            CopyOnWriteMutation::updated(())
4062        }
4063    }
4064
4065    /// A delete mutation that, during staging, has a concurrent writer delete the same
4066    /// object and commit first, so our own commit hits a conflict while the object is
4067    /// already gone — exercising `ConflictResolution::SucceedIfAbsent`.
4068    struct ConcurrentDeleteBeforeCommitMutation {
4069        inner: DeleteObjectMutation,
4070        root: String,
4071        target: String,
4072    }
4073
4074    impl ManifestStreamMutation for ConcurrentDeleteBeforeCommitMutation {
4075        type Output = ();
4076
4077        fn process_existing_row(
4078            &mut self,
4079            row: ManifestRowValue,
4080            output: &mut ManifestBatchBuilder,
4081            index_data: &mut ManifestIndexAccumulator,
4082        ) -> lance_core::Result<()> {
4083            self.inner.process_existing_row(row, output, index_data)
4084        }
4085
4086        fn append_rows(
4087            &mut self,
4088            output: &mut ManifestBatchBuilder,
4089            index_data: &mut ManifestIndexAccumulator,
4090        ) -> lance_core::Result<()> {
4091            self.inner.append_rows(output, index_data)
4092        }
4093
4094        fn finish(&self) -> CopyOnWriteMutation<Self::Output> {
4095            let root = self.root.clone();
4096            let target = self.target.clone();
4097            std::thread::spawn(move || {
4098                let runtime = tokio::runtime::Runtime::new().unwrap();
4099                runtime.block_on(async move {
4100                    let writer = create_manifest_namespace(&root, false).await;
4101                    writer.delete_from_manifest(&target).await.unwrap();
4102                });
4103            })
4104            .join()
4105            .unwrap();
4106            self.inner.finish()
4107        }
4108
4109        fn conflict_resolution(&self) -> ConflictResolution<Self::Output> {
4110            ConflictResolution::SucceedIfAbsent {
4111                object_id: self.target.clone(),
4112                output: (),
4113            }
4114        }
4115    }
4116
4117    async fn manifest_base_objects(
4118        manifest_ns: &ManifestNamespace,
4119    ) -> HashMap<String, Option<Vec<String>>> {
4120        let mut scanner = manifest_ns.manifest_scanner().await.unwrap();
4121        scanner.project(&["object_id", "base_objects"]).unwrap();
4122        let batches = ManifestNamespace::execute_scanner(scanner).await.unwrap();
4123        let mut rows = HashMap::new();
4124        for batch in batches {
4125            let object_ids = ManifestNamespace::get_string_column(&batch, "object_id").unwrap();
4126            let base_objects = ManifestNamespace::base_objects_column_values(&batch).unwrap();
4127            for (row, value) in base_objects.into_iter().enumerate() {
4128                rows.insert(object_ids.value(row).to_string(), value);
4129            }
4130        }
4131        rows
4132    }
4133
4134    async fn manifest_data_paths(manifest_ns: &ManifestNamespace) -> HashSet<String> {
4135        let data_dir = manifest_ns
4136            .base_path
4137            .clone()
4138            .join(MANIFEST_TABLE_NAME)
4139            .join(LANCE_DATA_DIR);
4140        let mut stream = manifest_ns.object_store.read_dir_all(&data_dir, None);
4141        let mut paths = HashSet::new();
4142        while let Some(meta) = stream.next().await.transpose().unwrap() {
4143            paths.insert(meta.location.to_string());
4144        }
4145        paths
4146    }
4147
4148    async fn manifest_index_paths(manifest_ns: &ManifestNamespace) -> HashSet<String> {
4149        let index_dir = manifest_ns
4150            .base_path
4151            .clone()
4152            .join(MANIFEST_TABLE_NAME)
4153            .join(LANCE_INDICES_DIR);
4154        let mut stream = manifest_ns.object_store.read_dir_all(&index_dir, None);
4155        let mut paths = HashSet::new();
4156        while let Some(meta) = stream.next().await.transpose().unwrap() {
4157            paths.insert(meta.location.to_string());
4158        }
4159        paths
4160    }
4161
4162    fn create_test_ipc_data() -> Vec<u8> {
4163        use arrow::array::{Int32Array, StringArray};
4164        use arrow::datatypes::{DataType, Field, Schema};
4165        use arrow::ipc::writer::StreamWriter;
4166        use arrow::record_batch::RecordBatch;
4167        use std::sync::Arc;
4168
4169        let schema = Arc::new(Schema::new(vec![
4170            Field::new("id", DataType::Int32, false),
4171            Field::new("name", DataType::Utf8, false),
4172        ]));
4173
4174        let batch = RecordBatch::try_new(
4175            schema.clone(),
4176            vec![
4177                Arc::new(Int32Array::from(vec![1, 2, 3])),
4178                Arc::new(StringArray::from(vec!["a", "b", "c"])),
4179            ],
4180        )
4181        .unwrap();
4182
4183        let mut buffer = Vec::new();
4184        {
4185            let mut writer = StreamWriter::try_new(&mut buffer, &schema).unwrap();
4186            writer.write(&batch).unwrap();
4187            writer.finish().unwrap();
4188        }
4189        buffer
4190    }
4191
4192    /// Open the `__manifest` dataset directly and set a table-metadata key,
4193    /// simulating a future Lance client that persisted a feature flag.
4194    async fn set_manifest_table_metadata(temp_path: &str, key: &str, value: &str) {
4195        use lance::dataset::builder::DatasetBuilder;
4196        let mut ds = DatasetBuilder::from_uri(format!("{}/{}", temp_path, MANIFEST_TABLE_NAME))
4197            .load()
4198            .await
4199            .unwrap();
4200        ds.update_metadata([(key, value)]).await.unwrap();
4201    }
4202
4203    async fn create_namespace_with_one_table(temp_path: &str) {
4204        let ns = DirectoryNamespaceBuilder::new(temp_path)
4205            .build()
4206            .await
4207            .unwrap();
4208        let mut create_request = CreateTableRequest::new();
4209        create_request.id = Some(vec!["t1".to_string()]);
4210        ns.create_table(create_request, Bytes::from(create_test_ipc_data()))
4211            .await
4212            .unwrap();
4213    }
4214
4215    /// This is a forward-compatibility checker only: it must not set any feature
4216    /// flag, so existing clients keep treating the manifest as compatible.
4217    #[tokio::test]
4218    async fn test_manifest_has_no_feature_flags_by_default() {
4219        use lance::dataset::builder::DatasetBuilder;
4220        let temp_dir = TempStdDir::default();
4221        let temp_path = temp_dir.to_str().unwrap();
4222        create_namespace_with_one_table(temp_path).await;
4223
4224        let ds = DatasetBuilder::from_uri(format!("{}/{}", temp_path, MANIFEST_TABLE_NAME))
4225            .load()
4226            .await
4227            .unwrap();
4228        assert!(
4229            !ds.metadata()
4230                .contains_key(crate::dir::manifest_feature_flags::READER_FEATURE_FLAGS_KEY)
4231        );
4232        assert!(
4233            !ds.metadata()
4234                .contains_key(crate::dir::manifest_feature_flags::WRITER_FEATURE_FLAGS_KEY)
4235        );
4236    }
4237
4238    /// An unknown reader feature flag must block opening the catalog with a clear
4239    /// "please upgrade" error rather than silently degrading to directory listing.
4240    #[tokio::test]
4241    async fn test_unknown_reader_flag_blocks_access() {
4242        let temp_dir = TempStdDir::default();
4243        let temp_path = temp_dir.to_str().unwrap();
4244        create_namespace_with_one_table(temp_path).await;
4245        set_manifest_table_metadata(
4246            temp_path,
4247            crate::dir::manifest_feature_flags::READER_FEATURE_FLAGS_KEY,
4248            "1",
4249        )
4250        .await;
4251
4252        let err = DirectoryNamespaceBuilder::new(temp_path)
4253            .build()
4254            .await
4255            .expect_err("opening a manifest with an unknown reader flag should fail");
4256        assert!(
4257            err.to_string().to_lowercase().contains("upgrade"),
4258            "expected an upgrade error, got: {err}"
4259        );
4260    }
4261
4262    /// An unknown writer feature flag must still allow reads but block writes.
4263    #[tokio::test]
4264    async fn test_unknown_writer_flag_blocks_writes_but_allows_reads() {
4265        let temp_dir = TempStdDir::default();
4266        let temp_path = temp_dir.to_str().unwrap();
4267        create_namespace_with_one_table(temp_path).await;
4268        set_manifest_table_metadata(
4269            temp_path,
4270            crate::dir::manifest_feature_flags::WRITER_FEATURE_FLAGS_KEY,
4271            "1",
4272        )
4273        .await;
4274
4275        let ns = DirectoryNamespaceBuilder::new(temp_path)
4276            .build()
4277            .await
4278            .expect("reads should still be allowed with only a writer flag set");
4279        let mut list_request = ListTablesRequest::new();
4280        list_request.id = Some(vec![]);
4281        assert_eq!(ns.list_tables(list_request).await.unwrap().tables.len(), 1);
4282
4283        // A refused write must not leave an orphaned table dataset behind.
4284        let entries_before = dir_entry_names(temp_path);
4285        let mut create_request = CreateTableRequest::new();
4286        create_request.id = Some(vec!["t2".to_string()]);
4287        let err = ns
4288            .create_table(create_request, Bytes::from(create_test_ipc_data()))
4289            .await
4290            .expect_err("writing through an unknown writer flag should fail");
4291        assert!(
4292            err.to_string().to_lowercase().contains("upgrade"),
4293            "expected an upgrade error, got: {err}"
4294        );
4295        assert_eq!(
4296            entries_before,
4297            dir_entry_names(temp_path),
4298            "a refused create_table must not create an orphaned table directory"
4299        );
4300
4301        // Mutations that go straight through rewrite_manifest (no early
4302        // create_table check) must also be refused: an insert (create_namespace)
4303        // and a delete (drop_table). This proves the writer check is enforced at
4304        // the single copy-on-write chokepoint, not just on the create_table path.
4305        let mut create_ns = CreateNamespaceRequest::new();
4306        create_ns.id = Some(vec!["ns1".to_string()]);
4307        let err = ns
4308            .create_namespace(create_ns)
4309            .await
4310            .expect_err("create_namespace through an unknown writer flag should fail");
4311        assert!(
4312            err.to_string().to_lowercase().contains("upgrade"),
4313            "expected an upgrade error, got: {err}"
4314        );
4315
4316        let mut drop_request = DropTableRequest::new();
4317        drop_request.id = Some(vec!["t1".to_string()]);
4318        let err = ns
4319            .drop_table(drop_request)
4320            .await
4321            .expect_err("drop_table through an unknown writer flag should fail");
4322        assert!(
4323            err.to_string().to_lowercase().contains("upgrade"),
4324            "expected an upgrade error, got: {err}"
4325        );
4326    }
4327
4328    fn dir_entry_names(path: &str) -> std::collections::BTreeSet<String> {
4329        std::fs::read_dir(path)
4330            .unwrap()
4331            .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
4332            .collect()
4333    }
4334
4335    async fn create_manifest_only_table(ns: &DirectoryNamespace, name: &str) {
4336        let mut create_request = CreateTableRequest::new();
4337        create_request.id = Some(vec![name.to_string()]);
4338        ns.create_table(create_request, Bytes::from(create_test_ipc_data()))
4339            .await
4340            .unwrap();
4341    }
4342
4343    fn copy_test_data_to_tmp(table_path: &str) -> std::io::Result<TempStdDir> {
4344        let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4345            .join("../../test_data")
4346            .join(table_path);
4347        let temp_dir = TempStdDir::default();
4348        let mut directories = vec![(source, temp_dir.to_path_buf())];
4349        while let Some((source, destination)) = directories.pop() {
4350            std::fs::create_dir_all(&destination)?;
4351            for entry in std::fs::read_dir(source)? {
4352                let entry = entry?;
4353                let target = destination.join(entry.file_name());
4354                if entry.file_type()?.is_dir() {
4355                    directories.push((entry.path(), target));
4356                } else {
4357                    std::fs::copy(entry.path(), target)?;
4358                }
4359            }
4360        }
4361        Ok(temp_dir)
4362    }
4363
4364    #[derive(Debug)]
4365    enum ManifestAccess {
4366        Get,
4367        GetRefreshed,
4368        GetMut,
4369    }
4370
4371    /// Readers and writers holding a manifest created by a released old writer
4372    /// must observe newer commits after cleanup removes the immediate successor.
4373    #[rstest]
4374    #[case::get(ManifestAccess::Get)]
4375    #[case::get_refreshed(ManifestAccess::GetRefreshed)]
4376    #[case::get_mut(ManifestAccess::GetMut)]
4377    #[tokio::test]
4378    async fn test_legacy_auto_cleanup_manifest_reload_skips_removed_versions(
4379        #[case] access: ManifestAccess,
4380    ) {
4381        let temp_dir = copy_test_data_to_tmp("v6.0.1/dir_manifest/namespace").unwrap();
4382        let temp_path = temp_dir.to_str().unwrap();
4383        let manifest_uri = format!("{}/{}", temp_path, MANIFEST_TABLE_NAME);
4384        // Directory listing would surface tables the manifest misses, so the
4385        // namespaces read only the manifest.
4386        let build_namespace = || {
4387            DirectoryNamespaceBuilder::new(temp_path)
4388                .manifest_enabled(true)
4389                .dir_listing_enabled(false)
4390                .build()
4391        };
4392
4393        let manifest_ds = DatasetBuilder::from_uri(&manifest_uri)
4394            .load()
4395            .await
4396            .unwrap();
4397        assert!(has_auto_cleanup_config(manifest_ds.manifest()));
4398        assert_eq!(
4399            manifest_ds.manifest().config["lance.auto_cleanup.interval"],
4400            "20"
4401        );
4402        assert_eq!(
4403            manifest_ds.manifest().config["lance.auto_cleanup.older_than"],
4404            "14days"
4405        );
4406        let legacy_version = manifest_ds.version().version;
4407        let wrapper = DatasetConsistencyWrapper::new(manifest_ds);
4408        let reader = build_namespace().await.unwrap();
4409        let writer = build_namespace().await.unwrap();
4410        create_manifest_only_table(&writer, "t2").await;
4411        create_manifest_only_table(&writer, "t3").await;
4412
4413        let latest = DatasetBuilder::from_uri(&manifest_uri)
4414            .load()
4415            .await
4416            .unwrap();
4417        let latest_version = latest.version().version;
4418        assert_eq!(latest_version, legacy_version + 2);
4419        assert!(!has_auto_cleanup_config(latest.manifest()));
4420        let policy = CleanupPolicyBuilder::default()
4421            .retain_n_versions(&latest, 1)
4422            .await
4423            .unwrap()
4424            .delete_unverified(true)
4425            .build();
4426        latest.cleanup_with_policy(policy).await.unwrap();
4427        let error = latest
4428            .checkout_version(legacy_version + 1)
4429            .await
4430            .unwrap_err();
4431        assert!(
4432            matches!(error, lance_core::Error::DatasetNotFound { .. }),
4433            "unexpected missing-version error: {error:?}"
4434        );
4435        assert!(error.to_string().contains("not found"));
4436
4437        let reloaded_version = match access {
4438            ManifestAccess::Get => wrapper.get().await.unwrap().version().version,
4439            ManifestAccess::GetRefreshed => {
4440                wrapper.get_refreshed().await.unwrap().version().version
4441            }
4442            ManifestAccess::GetMut => wrapper.get_mut().await.unwrap().version().version,
4443        };
4444        assert_eq!(reloaded_version, latest_version);
4445        assert!(!has_auto_cleanup_config(
4446            wrapper.get().await.unwrap().manifest()
4447        ));
4448
4449        let mut list_request = ListTablesRequest::new();
4450        list_request.id = Some(vec![]);
4451        let mut tables = reader
4452            .list_tables(list_request.clone())
4453            .await
4454            .unwrap()
4455            .tables;
4456        tables.sort();
4457        assert_eq!(tables, vec!["t1", "t2", "t3"]);
4458
4459        // Once refreshed to a policy-free version, the successor fast path must
4460        // still observe subsequent writes.
4461        create_manifest_only_table(&writer, "t4").await;
4462        assert_eq!(
4463            wrapper.get().await.unwrap().version().version,
4464            latest_version + 1
4465        );
4466        let mut tables = reader.list_tables(list_request).await.unwrap().tables;
4467        tables.sort();
4468        assert_eq!(tables, vec!["t1", "t2", "t3", "t4"]);
4469    }
4470
4471    #[tokio::test]
4472    async fn test_manifest_rewrite_preserves_utf8_metadata_and_base_objects() {
4473        let temp_dir = TempStdDir::default();
4474        let temp_path = temp_dir.to_str().unwrap();
4475        let manifest_ns = create_manifest_namespace(temp_path, true).await;
4476
4477        manifest_ns
4478            .insert_into_manifest_with_metadata(
4479                vec![ManifestEntry {
4480                    object_id: "view".to_string(),
4481                    object_type: ObjectType::Table,
4482                    location: Some("view.lance".to_string()),
4483                    metadata: Some(r#"{"kind":"view"}"#.to_string()),
4484                }],
4485                Some(vec!["base_a".to_string(), "base_b".to_string()]),
4486            )
4487            .await
4488            .unwrap();
4489        manifest_ns
4490            .insert_into_manifest_with_metadata(
4491                vec![ManifestEntry {
4492                    object_id: "other".to_string(),
4493                    object_type: ObjectType::Namespace,
4494                    location: None,
4495                    metadata: Some(r#"{"kind":"namespace"}"#.to_string()),
4496                }],
4497                None,
4498            )
4499            .await
4500            .unwrap();
4501
4502        let dataset_guard = manifest_ns.manifest_dataset.get().await.unwrap();
4503        let metadata_field = dataset_guard.schema().field("metadata").unwrap();
4504        assert_eq!(metadata_field.data_type(), DataType::Utf8);
4505        drop(dataset_guard);
4506
4507        let base_objects = manifest_base_objects(&manifest_ns).await;
4508        assert_eq!(
4509            base_objects.get("view").cloned().unwrap(),
4510            Some(vec!["base_a".to_string(), "base_b".to_string()])
4511        );
4512        assert_eq!(base_objects.get("other").cloned().unwrap(), None);
4513    }
4514
4515    #[tokio::test]
4516    async fn test_manifest_rewrite_replacement_indices_are_versioned() {
4517        let temp_dir = TempStdDir::default();
4518        let temp_path = temp_dir.to_str().unwrap();
4519        let manifest_ns = create_manifest_namespace(temp_path, true).await;
4520
4521        manifest_ns
4522            .insert_into_manifest_with_metadata(
4523                vec![ManifestEntry {
4524                    object_id: "table".to_string(),
4525                    object_type: ObjectType::Table,
4526                    location: Some("table.lance".to_string()),
4527                    metadata: None,
4528                }],
4529                Some(vec!["base".to_string()]),
4530            )
4531            .await
4532            .unwrap();
4533
4534        let dataset_guard = manifest_ns.manifest_dataset.get().await.unwrap();
4535        let dataset_version = dataset_guard.version().version;
4536        let indices = dataset_guard.load_indices().await.unwrap();
4537        let names = indices
4538            .iter()
4539            .map(|index| index.name.as_str())
4540            .collect::<HashSet<_>>();
4541        assert!(names.contains(OBJECT_ID_INDEX_NAME));
4542        assert!(names.contains(OBJECT_TYPE_INDEX_NAME));
4543        assert!(names.contains(BASE_OBJECTS_INDEX_NAME));
4544        for index in indices.iter() {
4545            assert_eq!(index.dataset_version, dataset_version);
4546            assert!(!index.fragment_bitmap.as_ref().unwrap().is_empty());
4547        }
4548    }
4549
4550    #[tokio::test]
4551    async fn test_manifest_rewrite_empty_manifest_keeps_replacement_indices_valid() {
4552        let temp_dir = TempStdDir::default();
4553        let temp_path = temp_dir.to_str().unwrap();
4554        let manifest_ns = create_manifest_namespace(temp_path, true).await;
4555
4556        manifest_ns
4557            .insert_into_manifest_with_metadata(
4558                vec![ManifestEntry {
4559                    object_id: "table".to_string(),
4560                    object_type: ObjectType::Table,
4561                    location: Some("table.lance".to_string()),
4562                    metadata: None,
4563                }],
4564                None,
4565            )
4566            .await
4567            .unwrap();
4568        manifest_ns.delete_from_manifest("table").await.unwrap();
4569
4570        assert!(!manifest_ns.manifest_contains_object("table").await.unwrap());
4571        let mut scanner = manifest_ns.manifest_scanner().await.unwrap();
4572        scanner.project(&["object_id"]).unwrap();
4573        let rows = ManifestNamespace::execute_scanner(scanner)
4574            .await
4575            .unwrap()
4576            .into_iter()
4577            .map(|batch| batch.num_rows())
4578            .sum::<usize>();
4579        assert_eq!(rows, 0);
4580
4581        let dataset_guard = manifest_ns.manifest_dataset.get().await.unwrap();
4582        let dataset_version = dataset_guard.version().version;
4583        let indices = dataset_guard.load_indices().await.unwrap();
4584        let names = indices
4585            .iter()
4586            .map(|index| index.name.as_str())
4587            .collect::<HashSet<_>>();
4588        assert!(names.contains(OBJECT_ID_INDEX_NAME));
4589        assert!(names.contains(OBJECT_TYPE_INDEX_NAME));
4590        assert!(names.contains(BASE_OBJECTS_INDEX_NAME));
4591        for index in indices.iter() {
4592            assert_eq!(index.dataset_version, dataset_version);
4593        }
4594    }
4595
4596    #[tokio::test]
4597    async fn test_manifest_rewrite_fragment_bitmap_uses_overwrite_fragment_ids() {
4598        let temp_dir = TempStdDir::default();
4599        let temp_path = temp_dir.to_str().unwrap();
4600        let manifest_ns = create_manifest_namespace(temp_path, false).await;
4601        let dataset_guard = manifest_ns.manifest_dataset.get().await.unwrap();
4602        let fragments = vec![Fragment::new(0), Fragment::new(0), Fragment::new(7)];
4603
4604        let manifest = ManifestNamespace::manifest_from_overwrite_transaction(
4605            dataset_guard.manifest(),
4606            dataset_guard.manifest().schema.clone(),
4607            &fragments,
4608        );
4609
4610        let fragment_ids = manifest
4611            .fragments
4612            .iter()
4613            .map(|fragment| fragment.id)
4614            .collect::<Vec<_>>();
4615        assert_eq!(fragment_ids, vec![0, 1, 7]);
4616        assert_eq!(
4617            ManifestNamespace::manifest_fragment_bitmap(&manifest)
4618                .unwrap()
4619                .into_iter()
4620                .collect::<Vec<_>>(),
4621            vec![0, 1, 7]
4622        );
4623    }
4624
4625    #[tokio::test]
4626    async fn test_manifest_writes_reject_unknown_writer_flag_before_staging() {
4627        let temp_dir = TempStdDir::default();
4628        let temp_path = temp_dir.to_str().unwrap();
4629        let manifest_ns = create_manifest_namespace(temp_path, false).await;
4630        let data_paths_before = manifest_data_paths(&manifest_ns).await;
4631        let original_version = {
4632            let mut dataset = manifest_ns.manifest_dataset.get_mut().await.unwrap();
4633            let mut manifest = dataset.manifest().clone();
4634            manifest.writer_feature_flags |= FLAG_UNKNOWN << 1;
4635            let version = manifest.version;
4636            dataset.manifest = Arc::new(manifest);
4637            version
4638        };
4639
4640        let entries_before = dir_entry_names(temp_path);
4641        let mut declare_request = DeclareTableRequest::new();
4642        declare_request.id = Some(vec!["declared_table".to_string()]);
4643        let error = manifest_ns
4644            .declare_table(declare_request)
4645            .await
4646            .unwrap_err();
4647        assert!(
4648            error.to_string().to_lowercase().contains("upgrade"),
4649            "expected an upgrade error, got: {error}"
4650        );
4651        assert_eq!(dir_entry_names(temp_path), entries_before);
4652
4653        let mut create_request = CreateTableRequest::new();
4654        create_request.id = Some(vec!["new_table".to_string()]);
4655        let error = manifest_ns
4656            .create_table(create_request, Bytes::from(create_test_ipc_data()))
4657            .await
4658            .unwrap_err();
4659        assert!(
4660            error.to_string().to_lowercase().contains("upgrade"),
4661            "expected an upgrade error, got: {error}"
4662        );
4663        assert_eq!(dir_entry_names(temp_path), entries_before);
4664
4665        let error = manifest_ns
4666            .insert_into_manifest_with_metadata(
4667                vec![ManifestEntry {
4668                    object_id: "table".to_string(),
4669                    object_type: ObjectType::Table,
4670                    location: Some("table.lance".to_string()),
4671                    metadata: None,
4672                }],
4673                None,
4674            )
4675            .await
4676            .unwrap_err();
4677
4678        assert!(
4679            error.to_string().to_lowercase().contains("upgrade"),
4680            "expected an upgrade error, got: {error}"
4681        );
4682        assert_eq!(
4683            manifest_ns
4684                .manifest_dataset
4685                .get()
4686                .await
4687                .unwrap()
4688                .version()
4689                .version,
4690            original_version
4691        );
4692        assert_eq!(manifest_data_paths(&manifest_ns).await, data_paths_before);
4693    }
4694
4695    /// A reader that arrives after a waiting `get_mut()` must be served after
4696    /// it. tokio's `RwLock` guarantees that only once the writer is queued on
4697    /// the write lock: if `get_mut()` first queued as a reader to reload, the
4698    /// later reader would be admitted alongside it, and a reader that holds
4699    /// its permit until the writer is done would then block the write lock
4700    /// forever.
4701    #[tokio::test]
4702    async fn test_get_mut_is_served_before_later_readers() {
4703        let temp_dir = TempStdDir::default();
4704        let manifest_ns = create_manifest_namespace(temp_dir.to_str().unwrap(), false).await;
4705        let wrapper = manifest_ns.manifest_dataset.clone();
4706
4707        // Hold the lock exclusively so both tasks below park on their first
4708        // lock request, in spawn order. The current-thread test runtime polls
4709        // each spawned task before this task resumes from `yield_now`.
4710        let blocker = wrapper.0.write().await;
4711        let writer = {
4712            let wrapper = wrapper.clone();
4713            tokio::spawn(async move {
4714                wrapper.get_mut().await.unwrap();
4715            })
4716        };
4717        tokio::task::yield_now().await;
4718
4719        // The late reader takes a single read permit and keeps it until told
4720        // to let go. A raw lock rather than `get()` so that it never releases
4721        // and re-acquires the permit around a reload probe, which would leave
4722        // a window for the writer's request to slip in ahead of it.
4723        let (release_tx, release_rx) = oneshot::channel::<()>();
4724        let late_reader = {
4725            let wrapper = wrapper.clone();
4726            tokio::spawn(async move {
4727                let guard = wrapper.0.read().await;
4728                release_rx.await.unwrap();
4729                drop(guard);
4730            })
4731        };
4732        tokio::task::yield_now().await;
4733
4734        drop(blocker);
4735        timeout(Duration::from_secs(10), writer)
4736            .await
4737            .expect("get_mut() waited on a reader that arrived after it")
4738            .unwrap();
4739        release_tx.send(()).unwrap();
4740        timeout(Duration::from_secs(10), late_reader)
4741            .await
4742            .expect("the late reader did not finish after the writer released the lock")
4743            .unwrap();
4744    }
4745
4746    #[tokio::test]
4747    async fn test_manifest_noop_delete_uses_latest_snapshot() {
4748        let temp_dir = TempStdDir::default();
4749        let temp_path = temp_dir.to_str().unwrap();
4750        let stale_ns = create_manifest_namespace(temp_path, false).await;
4751        let writer_ns = create_manifest_namespace(temp_path, false).await;
4752
4753        writer_ns
4754            .insert_into_manifest_with_metadata(
4755                vec![ManifestEntry {
4756                    object_id: "late_table".to_string(),
4757                    object_type: ObjectType::Table,
4758                    location: Some("late_table.lance".to_string()),
4759                    metadata: None,
4760                }],
4761                None,
4762            )
4763            .await
4764            .unwrap();
4765
4766        stale_ns.delete_from_manifest("late_table").await.unwrap();
4767
4768        let check_ns = create_manifest_namespace(temp_path, false).await;
4769        assert!(
4770            !check_ns
4771                .manifest_contains_object("late_table")
4772                .await
4773                .unwrap()
4774        );
4775    }
4776
4777    #[tokio::test]
4778    async fn test_manifest_noop_delete_cleans_uncommitted_data_file() {
4779        let temp_dir = TempStdDir::default();
4780        let temp_path = temp_dir.to_str().unwrap();
4781        let manifest_ns = create_manifest_namespace(temp_path, false).await;
4782
4783        manifest_ns
4784            .insert_into_manifest_with_metadata(
4785                vec![ManifestEntry {
4786                    object_id: "table".to_string(),
4787                    object_type: ObjectType::Table,
4788                    location: Some("table.lance".to_string()),
4789                    metadata: None,
4790                }],
4791                None,
4792            )
4793            .await
4794            .unwrap();
4795
4796        let before = manifest_data_paths(&manifest_ns).await;
4797        assert!(!before.is_empty());
4798
4799        manifest_ns
4800            .delete_from_manifest("missing_table")
4801            .await
4802            .unwrap();
4803
4804        let after = manifest_data_paths(&manifest_ns).await;
4805        assert_eq!(after, before);
4806    }
4807
4808    #[tokio::test]
4809    async fn test_manifest_final_commit_failure_cleans_uncommitted_rewrite_files() {
4810        let temp_dir = TempStdDir::default();
4811        let temp_path = temp_dir.to_str().unwrap();
4812        let manifest_ns = create_manifest_namespace_with_retries(temp_path, true, Some(0)).await;
4813
4814        manifest_ns
4815            .insert_into_manifest_with_metadata(
4816                vec![ManifestEntry {
4817                    object_id: "table".to_string(),
4818                    object_type: ObjectType::Table,
4819                    location: Some("table.lance".to_string()),
4820                    metadata: None,
4821                }],
4822                None,
4823            )
4824            .await
4825            .unwrap();
4826
4827        let before_data_paths = manifest_data_paths(&manifest_ns).await;
4828        let before_index_paths = manifest_index_paths(&manifest_ns).await;
4829
4830        let result = manifest_ns
4831            .rewrite_manifest("Failed to test manifest cleanup", || {
4832                CommitConflictAfterRewriteMutation {
4833                    root: temp_path.to_string(),
4834                    conflict_object_id: "conflicting_table".to_string(),
4835                }
4836            })
4837            .await;
4838        assert!(result.is_err());
4839
4840        let after_data_paths = manifest_data_paths(&manifest_ns).await;
4841        assert!(before_data_paths.is_subset(&after_data_paths));
4842        assert_eq!(after_data_paths.len(), before_data_paths.len() + 1);
4843        assert_eq!(manifest_index_paths(&manifest_ns).await, before_index_paths);
4844        assert!(
4845            manifest_ns
4846                .manifest_contains_object("conflicting_table")
4847                .await
4848                .unwrap()
4849        );
4850        assert!(
4851            !manifest_ns
4852                .manifest_contains_object("attempted_table")
4853                .await
4854                .unwrap()
4855        );
4856    }
4857
4858    #[tokio::test]
4859    async fn test_manifest_commit_visible_on_memory_store() {
4860        // Regression: the commit must use the same object store the manifest dataset reads
4861        // from. On `memory://` the namespace store and the dataset store can be different
4862        // in-memory instances, so a commit written to the wrong one is invisible to reads
4863        // (manifests as stale version -> endless conflict / "not found").
4864        let manifest_ns = create_manifest_namespace("memory://test_commit_visible", false).await;
4865        manifest_ns
4866            .insert_into_manifest_with_metadata(
4867                vec![ManifestEntry {
4868                    object_id: "table".to_string(),
4869                    object_type: ObjectType::Table,
4870                    location: Some("table.lance".to_string()),
4871                    metadata: None,
4872                }],
4873                None,
4874            )
4875            .await
4876            .unwrap();
4877        assert!(manifest_ns.manifest_contains_object("table").await.unwrap());
4878        // A second sequential commit must not falsely conflict.
4879        manifest_ns
4880            .insert_into_manifest_with_metadata(
4881                vec![ManifestEntry {
4882                    object_id: "table2".to_string(),
4883                    object_type: ObjectType::Table,
4884                    location: Some("table2.lance".to_string()),
4885                    metadata: None,
4886                }],
4887                None,
4888            )
4889            .await
4890            .unwrap();
4891        assert!(
4892            manifest_ns
4893                .manifest_contains_object("table2")
4894                .await
4895                .unwrap()
4896        );
4897    }
4898
4899    #[tokio::test]
4900    async fn test_manifest_commit_uses_inline_transaction() {
4901        let temp_dir = TempStdDir::default();
4902        let temp_path = temp_dir.to_str().unwrap();
4903        let manifest_ns = create_manifest_namespace(temp_path, false).await;
4904
4905        manifest_ns
4906            .insert_into_manifest_with_metadata(
4907                vec![ManifestEntry {
4908                    object_id: "table".to_string(),
4909                    object_type: ObjectType::Table,
4910                    location: Some("table.lance".to_string()),
4911                    metadata: None,
4912                }],
4913                None,
4914            )
4915            .await
4916            .unwrap();
4917
4918        let dataset_guard = manifest_ns.manifest_dataset.get().await.unwrap();
4919        let manifest = dataset_guard.manifest();
4920        // The overwrite transaction is embedded inline in the manifest, never written as a
4921        // separate _transactions/*.txn file.
4922        assert!(manifest.transaction_section.is_some());
4923        assert!(manifest.transaction_file.is_none());
4924    }
4925
4926    #[tokio::test]
4927    async fn test_manifest_commit_landed_attributes_data_file() {
4928        let temp_dir = TempStdDir::default();
4929        let temp_path = temp_dir.to_str().unwrap();
4930        let manifest_ns = create_manifest_namespace(temp_path, false).await;
4931
4932        manifest_ns
4933            .insert_into_manifest_with_metadata(
4934                vec![ManifestEntry {
4935                    object_id: "table".to_string(),
4936                    object_type: ObjectType::Table,
4937                    location: Some("table.lance".to_string()),
4938                    metadata: None,
4939                }],
4940                None,
4941            )
4942            .await
4943            .unwrap();
4944
4945        let dataset = Arc::new(manifest_ns.manifest_dataset.get().await.unwrap().clone());
4946        let version = dataset.manifest().version;
4947        let our_files = dataset
4948            .manifest()
4949            .fragments
4950            .iter()
4951            .flat_map(|fragment| fragment.files.iter())
4952            .map(|file| file.path.clone())
4953            .collect::<HashSet<_>>();
4954        assert!(!our_files.is_empty());
4955
4956        // The committed version references our data file => attributed to us (a lost-ack
4957        // commit must be treated as success, not cleaned up).
4958        assert!(
4959            manifest_ns
4960                .manifest_commit_landed(&dataset, version, &our_files)
4961                .await
4962        );
4963        // A different file set is not attributed to us.
4964        let other = HashSet::from(["missing.lance".to_string()]);
4965        assert!(
4966            !manifest_ns
4967                .manifest_commit_landed(&dataset, version, &other)
4968                .await
4969        );
4970        // A version that does not exist did not land.
4971        assert!(
4972            !manifest_ns
4973                .manifest_commit_landed(&dataset, version + 100, &our_files)
4974                .await
4975        );
4976    }
4977
4978    #[tokio::test]
4979    async fn test_manifest_delete_conflict_with_concurrent_delete_succeeds() {
4980        let temp_dir = TempStdDir::default();
4981        let temp_path = temp_dir.to_str().unwrap();
4982        let manifest_ns = create_manifest_namespace_with_retries(temp_path, false, Some(0)).await;
4983
4984        manifest_ns
4985            .insert_into_manifest_with_metadata(
4986                vec![ManifestEntry {
4987                    object_id: "table".to_string(),
4988                    object_type: ObjectType::Table,
4989                    location: Some("table.lance".to_string()),
4990                    metadata: None,
4991                }],
4992                None,
4993            )
4994            .await
4995            .unwrap();
4996        assert!(manifest_ns.manifest_contains_object("table").await.unwrap());
4997
4998        // A concurrent writer deletes "table" and commits first, so our own delete commit
4999        // conflicts while "table" is already gone. Native resolution treats the goal as
5000        // achieved and succeeds instead of erroring or retrying forever.
5001        let result = manifest_ns
5002            .rewrite_manifest("Failed to delete from manifest", || {
5003                ConcurrentDeleteBeforeCommitMutation {
5004                    inner: DeleteObjectMutation {
5005                        object_id: "table".to_string(),
5006                        deleted: false,
5007                    },
5008                    root: temp_path.to_string(),
5009                    target: "table".to_string(),
5010                }
5011            })
5012            .await;
5013
5014        assert!(result.is_ok(), "delete should succeed: {result:?}");
5015        assert!(!manifest_ns.manifest_contains_object("table").await.unwrap());
5016    }
5017
5018    #[rstest]
5019    #[case::with_optimization(true)]
5020    #[case::without_optimization(false)]
5021    #[tokio::test]
5022    async fn test_manifest_namespace_basic_create_and_list(#[case] inline_optimization: bool) {
5023        let temp_dir = TempStdDir::default();
5024        let temp_path = temp_dir.to_str().unwrap();
5025
5026        // Create a DirectoryNamespace with manifest enabled (default)
5027        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5028            .inline_optimization_enabled(inline_optimization)
5029            .build()
5030            .await
5031            .unwrap();
5032
5033        // Verify we can list tables (should be empty)
5034        let mut request = ListTablesRequest::new();
5035        request.id = Some(vec![]);
5036        let response = dir_namespace.list_tables(request).await.unwrap();
5037        assert_eq!(response.tables.len(), 0);
5038
5039        // Create a test table
5040        let buffer = create_test_ipc_data();
5041        let mut create_request = CreateTableRequest::new();
5042        create_request.id = Some(vec!["test_table".to_string()]);
5043
5044        let _response = dir_namespace
5045            .create_table(create_request, Bytes::from(buffer))
5046            .await
5047            .unwrap();
5048
5049        // List tables again - should see our new table
5050        let mut request = ListTablesRequest::new();
5051        request.id = Some(vec![]);
5052        let response = dir_namespace.list_tables(request).await.unwrap();
5053        assert_eq!(response.tables.len(), 1);
5054        assert_eq!(response.tables[0], "test_table");
5055    }
5056
5057    #[rstest]
5058    #[case::with_optimization(true)]
5059    #[case::without_optimization(false)]
5060    #[tokio::test]
5061    async fn test_manifest_namespace_table_exists(#[case] inline_optimization: bool) {
5062        let temp_dir = TempStdDir::default();
5063        let temp_path = temp_dir.to_str().unwrap();
5064
5065        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5066            .inline_optimization_enabled(inline_optimization)
5067            .build()
5068            .await
5069            .unwrap();
5070
5071        // Check non-existent table
5072        let mut request = TableExistsRequest::new();
5073        request.id = Some(vec!["nonexistent".to_string()]);
5074        let result = dir_namespace.table_exists(request).await;
5075        assert!(result.is_err());
5076
5077        // Create table
5078        let buffer = create_test_ipc_data();
5079        let mut create_request = CreateTableRequest::new();
5080        create_request.id = Some(vec!["test_table".to_string()]);
5081        dir_namespace
5082            .create_table(create_request, Bytes::from(buffer))
5083            .await
5084            .unwrap();
5085
5086        // Check existing table
5087        let mut request = TableExistsRequest::new();
5088        request.id = Some(vec!["test_table".to_string()]);
5089        let result = dir_namespace.table_exists(request).await;
5090        assert!(result.is_ok());
5091
5092        // A namespace's id is not a table. A root id would be answered from
5093        // the directory listing, so check a child id.
5094        let mut create_ns_req = CreateNamespaceRequest::new();
5095        create_ns_req.id = Some(vec!["ns".to_string()]);
5096        dir_namespace.create_namespace(create_ns_req).await.unwrap();
5097        let mut create_ns_req = CreateNamespaceRequest::new();
5098        create_ns_req.id = Some(vec!["ns".to_string(), "child".to_string()]);
5099        dir_namespace.create_namespace(create_ns_req).await.unwrap();
5100        let mut request = TableExistsRequest::new();
5101        request.id = Some(vec!["ns".to_string(), "child".to_string()]);
5102        let err = dir_namespace.table_exists(request).await.unwrap_err();
5103        assert!(err.to_string().contains("Table not found"), "{err}");
5104    }
5105
5106    #[rstest]
5107    #[case::with_optimization(true)]
5108    #[case::without_optimization(false)]
5109    #[tokio::test]
5110    async fn test_manifest_namespace_describe_table(#[case] inline_optimization: bool) {
5111        let temp_dir = TempStdDir::default();
5112        let temp_path = temp_dir.to_str().unwrap();
5113
5114        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5115            .inline_optimization_enabled(inline_optimization)
5116            .build()
5117            .await
5118            .unwrap();
5119
5120        // Describe non-existent table
5121        let mut request = DescribeTableRequest::new();
5122        request.id = Some(vec!["nonexistent".to_string()]);
5123        let result = dir_namespace.describe_table(request).await;
5124        assert!(result.is_err());
5125
5126        // Create table
5127        let buffer = create_test_ipc_data();
5128        let mut create_request = CreateTableRequest::new();
5129        create_request.id = Some(vec!["test_table".to_string()]);
5130        dir_namespace
5131            .create_table(create_request, Bytes::from(buffer))
5132            .await
5133            .unwrap();
5134
5135        // Describe existing table
5136        let mut request = DescribeTableRequest::new();
5137        request.id = Some(vec!["test_table".to_string()]);
5138        let response = dir_namespace.describe_table(request).await.unwrap();
5139        assert!(response.location.is_some());
5140        assert!(response.location.unwrap().contains("test_table"));
5141    }
5142
5143    #[rstest]
5144    #[case::with_optimization(true)]
5145    #[case::without_optimization(false)]
5146    #[tokio::test]
5147    async fn test_manifest_namespace_drop_table(#[case] inline_optimization: bool) {
5148        let temp_dir = TempStdDir::default();
5149        let temp_path = temp_dir.to_str().unwrap();
5150
5151        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5152            .inline_optimization_enabled(inline_optimization)
5153            .build()
5154            .await
5155            .unwrap();
5156
5157        // Create table
5158        let buffer = create_test_ipc_data();
5159        let mut create_request = CreateTableRequest::new();
5160        create_request.id = Some(vec!["test_table".to_string()]);
5161        dir_namespace
5162            .create_table(create_request, Bytes::from(buffer))
5163            .await
5164            .unwrap();
5165
5166        // Verify table exists
5167        let mut request = ListTablesRequest::new();
5168        request.id = Some(vec![]);
5169        let response = dir_namespace.list_tables(request).await.unwrap();
5170        assert_eq!(response.tables.len(), 1);
5171
5172        // Drop table
5173        let mut drop_request = DropTableRequest::new();
5174        drop_request.id = Some(vec!["test_table".to_string()]);
5175        let _response = dir_namespace.drop_table(drop_request).await.unwrap();
5176
5177        // Verify table is gone
5178        let mut request = ListTablesRequest::new();
5179        request.id = Some(vec![]);
5180        let response = dir_namespace.list_tables(request).await.unwrap();
5181        assert_eq!(response.tables.len(), 0);
5182    }
5183
5184    #[tokio::test]
5185    async fn test_list_tables_pagination_limit_zero() {
5186        let temp_dir = TempStdDir::default();
5187        let temp_path = temp_dir.to_str().unwrap();
5188
5189        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5190            .build()
5191            .await
5192            .unwrap();
5193
5194        let buffer = create_test_ipc_data();
5195        let mut create_request = CreateTableRequest::new();
5196        create_request.id = Some(vec!["alpha".to_string()]);
5197        dir_namespace
5198            .create_table(create_request, Bytes::from(buffer))
5199            .await
5200            .unwrap();
5201
5202        let response = dir_namespace
5203            .list_tables(ListTablesRequest {
5204                id: Some(vec![]),
5205                limit: Some(0),
5206                ..Default::default()
5207            })
5208            .await
5209            .unwrap();
5210
5211        assert!(response.tables.is_empty());
5212        assert!(response.page_token.is_none());
5213    }
5214
5215    #[rstest]
5216    #[case::with_optimization(true)]
5217    #[case::without_optimization(false)]
5218    #[tokio::test]
5219    async fn test_manifest_namespace_multiple_tables(#[case] inline_optimization: bool) {
5220        let temp_dir = TempStdDir::default();
5221        let temp_path = temp_dir.to_str().unwrap();
5222
5223        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5224            .inline_optimization_enabled(inline_optimization)
5225            .build()
5226            .await
5227            .unwrap();
5228
5229        // Create multiple tables
5230        let buffer = create_test_ipc_data();
5231        for i in 1..=3 {
5232            let mut create_request = CreateTableRequest::new();
5233            create_request.id = Some(vec![format!("table{}", i)]);
5234            dir_namespace
5235                .create_table(create_request, Bytes::from(buffer.clone()))
5236                .await
5237                .unwrap();
5238        }
5239
5240        // List all tables
5241        let mut request = ListTablesRequest::new();
5242        request.id = Some(vec![]);
5243        let response = dir_namespace.list_tables(request).await.unwrap();
5244        assert_eq!(response.tables.len(), 3);
5245        assert!(response.tables.contains(&"table1".to_string()));
5246        assert!(response.tables.contains(&"table2".to_string()));
5247        assert!(response.tables.contains(&"table3".to_string()));
5248    }
5249
5250    #[rstest]
5251    #[case::with_optimization(true)]
5252    #[case::without_optimization(false)]
5253    #[tokio::test]
5254    async fn test_directory_only_mode(#[case] inline_optimization: bool) {
5255        let temp_dir = TempStdDir::default();
5256        let temp_path = temp_dir.to_str().unwrap();
5257
5258        // Create a DirectoryNamespace with manifest disabled
5259        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5260            .manifest_enabled(false)
5261            .inline_optimization_enabled(inline_optimization)
5262            .build()
5263            .await
5264            .unwrap();
5265
5266        // Verify we can list tables (should be empty)
5267        let mut request = ListTablesRequest::new();
5268        request.id = Some(vec![]);
5269        let response = dir_namespace.list_tables(request).await.unwrap();
5270        assert_eq!(response.tables.len(), 0);
5271
5272        // Create a test table
5273        let buffer = create_test_ipc_data();
5274        let mut create_request = CreateTableRequest::new();
5275        create_request.id = Some(vec!["test_table".to_string()]);
5276
5277        // Create table - this should use directory-only mode
5278        let _response = dir_namespace
5279            .create_table(create_request, Bytes::from(buffer))
5280            .await
5281            .unwrap();
5282
5283        // List tables - should see our new table
5284        let mut request = ListTablesRequest::new();
5285        request.id = Some(vec![]);
5286        let response = dir_namespace.list_tables(request).await.unwrap();
5287        assert_eq!(response.tables.len(), 1);
5288        assert_eq!(response.tables[0], "test_table");
5289    }
5290
5291    #[rstest]
5292    #[case::with_optimization(true)]
5293    #[case::without_optimization(false)]
5294    #[tokio::test]
5295    async fn test_dual_mode_merge(#[case] inline_optimization: bool) {
5296        let temp_dir = TempStdDir::default();
5297        let temp_path = temp_dir.to_str().unwrap();
5298
5299        // Create a DirectoryNamespace with both manifest and directory enabled
5300        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5301            .manifest_enabled(true)
5302            .dir_listing_enabled(true)
5303            .inline_optimization_enabled(inline_optimization)
5304            .build()
5305            .await
5306            .unwrap();
5307
5308        // Create tables through manifest
5309        let buffer = create_test_ipc_data();
5310        let mut create_request = CreateTableRequest::new();
5311        create_request.id = Some(vec!["table1".to_string()]);
5312        dir_namespace
5313            .create_table(create_request, Bytes::from(buffer))
5314            .await
5315            .unwrap();
5316
5317        // List tables - should see table from both manifest and directory
5318        let mut request = ListTablesRequest::new();
5319        request.id = Some(vec![]);
5320        let response = dir_namespace.list_tables(request).await.unwrap();
5321        assert_eq!(response.tables.len(), 1);
5322        assert_eq!(response.tables[0], "table1");
5323    }
5324
5325    #[rstest]
5326    #[case::with_optimization(true)]
5327    #[case::without_optimization(false)]
5328    #[tokio::test]
5329    async fn test_manifest_only_mode(#[case] inline_optimization: bool) {
5330        let temp_dir = TempStdDir::default();
5331        let temp_path = temp_dir.to_str().unwrap();
5332
5333        // Create a DirectoryNamespace with only manifest enabled
5334        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5335            .manifest_enabled(true)
5336            .dir_listing_enabled(false)
5337            .inline_optimization_enabled(inline_optimization)
5338            .build()
5339            .await
5340            .unwrap();
5341
5342        // Create table
5343        let buffer = create_test_ipc_data();
5344        let mut create_request = CreateTableRequest::new();
5345        create_request.id = Some(vec!["test_table".to_string()]);
5346        dir_namespace
5347            .create_table(create_request, Bytes::from(buffer))
5348            .await
5349            .unwrap();
5350
5351        // List tables - should only use manifest
5352        let mut request = ListTablesRequest::new();
5353        request.id = Some(vec![]);
5354        let response = dir_namespace.list_tables(request).await.unwrap();
5355        assert_eq!(response.tables.len(), 1);
5356        assert_eq!(response.tables[0], "test_table");
5357    }
5358
5359    #[rstest]
5360    #[case::with_optimization(true)]
5361    #[case::without_optimization(false)]
5362    #[tokio::test]
5363    async fn test_drop_nonexistent_table(#[case] inline_optimization: bool) {
5364        let temp_dir = TempStdDir::default();
5365        let temp_path = temp_dir.to_str().unwrap();
5366
5367        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5368            .inline_optimization_enabled(inline_optimization)
5369            .build()
5370            .await
5371            .unwrap();
5372
5373        // Try to drop non-existent table
5374        let mut drop_request = DropTableRequest::new();
5375        drop_request.id = Some(vec!["nonexistent".to_string()]);
5376        let result = dir_namespace.drop_table(drop_request).await;
5377        assert!(result.is_err());
5378    }
5379
5380    #[rstest]
5381    #[case::with_optimization(true)]
5382    #[case::without_optimization(false)]
5383    #[tokio::test]
5384    async fn test_create_duplicate_table_fails(#[case] inline_optimization: bool) {
5385        let temp_dir = TempStdDir::default();
5386        let temp_path = temp_dir.to_str().unwrap();
5387
5388        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5389            .inline_optimization_enabled(inline_optimization)
5390            .build()
5391            .await
5392            .unwrap();
5393
5394        // Create table
5395        let buffer = create_test_ipc_data();
5396        let mut create_request = CreateTableRequest::new();
5397        create_request.id = Some(vec!["test_table".to_string()]);
5398        dir_namespace
5399            .create_table(create_request, Bytes::from(buffer.clone()))
5400            .await
5401            .unwrap();
5402
5403        // Try to create table with same name - should fail
5404        let mut create_request = CreateTableRequest::new();
5405        create_request.id = Some(vec!["test_table".to_string()]);
5406        let result = dir_namespace
5407            .create_table(create_request, Bytes::from(buffer))
5408            .await;
5409        assert!(result.is_err());
5410    }
5411
5412    #[rstest]
5413    #[case::with_optimization(true)]
5414    #[case::without_optimization(false)]
5415    #[tokio::test]
5416    async fn test_create_child_namespace(#[case] inline_optimization: bool) {
5417        use lance_namespace::models::{
5418            CreateNamespaceRequest, ListNamespacesRequest, NamespaceExistsRequest,
5419        };
5420
5421        let temp_dir = TempStdDir::default();
5422        let temp_path = temp_dir.to_str().unwrap();
5423
5424        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5425            .inline_optimization_enabled(inline_optimization)
5426            .build()
5427            .await
5428            .unwrap();
5429
5430        // Create a child namespace
5431        let mut create_req = CreateNamespaceRequest::new();
5432        create_req.id = Some(vec!["ns1".to_string()]);
5433        let result = dir_namespace.create_namespace(create_req).await;
5434        assert!(
5435            result.is_ok(),
5436            "Failed to create child namespace: {:?}",
5437            result.err()
5438        );
5439
5440        // Verify namespace exists
5441        let exists_req = NamespaceExistsRequest {
5442            id: Some(vec!["ns1".to_string()]),
5443            ..Default::default()
5444        };
5445        let result = dir_namespace.namespace_exists(exists_req).await;
5446        assert!(result.is_ok(), "Namespace should exist");
5447
5448        // List child namespaces of root
5449        let list_req = ListNamespacesRequest {
5450            id: Some(vec![]),
5451            page_token: None,
5452            limit: None,
5453            ..Default::default()
5454        };
5455        let result = dir_namespace.list_namespaces(list_req).await;
5456        assert!(result.is_ok());
5457        let namespaces = result.unwrap();
5458        assert_eq!(namespaces.namespaces.len(), 1);
5459        assert_eq!(namespaces.namespaces[0], "ns1");
5460    }
5461
5462    #[rstest]
5463    #[case::with_optimization(true)]
5464    #[case::without_optimization(false)]
5465    #[tokio::test]
5466    async fn test_create_nested_namespace(#[case] inline_optimization: bool) {
5467        use lance_namespace::models::{
5468            CreateNamespaceRequest, ListNamespacesRequest, NamespaceExistsRequest,
5469        };
5470
5471        let temp_dir = TempStdDir::default();
5472        let temp_path = temp_dir.to_str().unwrap();
5473
5474        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5475            .inline_optimization_enabled(inline_optimization)
5476            .build()
5477            .await
5478            .unwrap();
5479
5480        // Create parent namespace
5481        let mut create_req = CreateNamespaceRequest::new();
5482        create_req.id = Some(vec!["parent".to_string()]);
5483        dir_namespace.create_namespace(create_req).await.unwrap();
5484
5485        // Create nested child namespace
5486        let mut create_req = CreateNamespaceRequest::new();
5487        create_req.id = Some(vec!["parent".to_string(), "child".to_string()]);
5488        let result = dir_namespace.create_namespace(create_req).await;
5489        assert!(
5490            result.is_ok(),
5491            "Failed to create nested namespace: {:?}",
5492            result.err()
5493        );
5494
5495        // Verify nested namespace exists
5496        let exists_req = NamespaceExistsRequest {
5497            id: Some(vec!["parent".to_string(), "child".to_string()]),
5498            ..Default::default()
5499        };
5500        let result = dir_namespace.namespace_exists(exists_req).await;
5501        assert!(result.is_ok(), "Nested namespace should exist");
5502
5503        // List child namespaces of parent
5504        let list_req = ListNamespacesRequest {
5505            id: Some(vec!["parent".to_string()]),
5506            page_token: None,
5507            limit: None,
5508            ..Default::default()
5509        };
5510        let result = dir_namespace.list_namespaces(list_req).await;
5511        assert!(result.is_ok());
5512        let namespaces = result.unwrap();
5513        assert_eq!(namespaces.namespaces.len(), 1);
5514        assert_eq!(namespaces.namespaces[0], "child");
5515    }
5516
5517    #[rstest]
5518    #[case::with_optimization(true)]
5519    #[case::without_optimization(false)]
5520    #[tokio::test]
5521    async fn test_create_namespace_without_parent_fails(#[case] inline_optimization: bool) {
5522        use lance_namespace::models::CreateNamespaceRequest;
5523
5524        let temp_dir = TempStdDir::default();
5525        let temp_path = temp_dir.to_str().unwrap();
5526
5527        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5528            .inline_optimization_enabled(inline_optimization)
5529            .build()
5530            .await
5531            .unwrap();
5532
5533        // Try to create nested namespace without parent
5534        let mut create_req = CreateNamespaceRequest::new();
5535        create_req.id = Some(vec!["nonexistent_parent".to_string(), "child".to_string()]);
5536        let result = dir_namespace.create_namespace(create_req).await;
5537        assert!(result.is_err(), "Should fail when parent doesn't exist");
5538
5539        // A table is not a parent namespace.
5540        let mut create_table_req = CreateTableRequest::new();
5541        create_table_req.id = Some(vec!["t".to_string()]);
5542        dir_namespace
5543            .create_table(create_table_req, Bytes::from(create_test_ipc_data()))
5544            .await
5545            .unwrap();
5546        let mut create_req = CreateNamespaceRequest::new();
5547        create_req.id = Some(vec!["t".to_string(), "child".to_string()]);
5548        let err = dir_namespace
5549            .create_namespace(create_req)
5550            .await
5551            .unwrap_err();
5552        assert!(err.to_string().contains("Namespace not found"), "{err}");
5553    }
5554
5555    #[rstest]
5556    #[case::with_optimization(true)]
5557    #[case::without_optimization(false)]
5558    #[tokio::test]
5559    async fn test_drop_child_namespace(#[case] inline_optimization: bool) {
5560        use lance_namespace::models::{
5561            CreateNamespaceRequest, DropNamespaceRequest, NamespaceExistsRequest,
5562        };
5563
5564        let temp_dir = TempStdDir::default();
5565        let temp_path = temp_dir.to_str().unwrap();
5566
5567        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5568            .inline_optimization_enabled(inline_optimization)
5569            .build()
5570            .await
5571            .unwrap();
5572
5573        // Create a child namespace
5574        let mut create_req = CreateNamespaceRequest::new();
5575        create_req.id = Some(vec!["ns1".to_string()]);
5576        dir_namespace.create_namespace(create_req).await.unwrap();
5577
5578        // Drop the namespace
5579        let mut drop_req = DropNamespaceRequest::new();
5580        drop_req.id = Some(vec!["ns1".to_string()]);
5581        let result = dir_namespace.drop_namespace(drop_req).await;
5582        assert!(
5583            result.is_ok(),
5584            "Failed to drop namespace: {:?}",
5585            result.err()
5586        );
5587
5588        // Verify namespace no longer exists
5589        let exists_req = NamespaceExistsRequest {
5590            id: Some(vec!["ns1".to_string()]),
5591            ..Default::default()
5592        };
5593        let result = dir_namespace.namespace_exists(exists_req).await;
5594        assert!(result.is_err(), "Namespace should not exist after drop");
5595    }
5596
5597    #[rstest]
5598    #[case::with_optimization(true)]
5599    #[case::without_optimization(false)]
5600    #[tokio::test]
5601    async fn test_drop_namespace_with_children_fails(#[case] inline_optimization: bool) {
5602        use lance_namespace::models::{
5603            CreateNamespaceRequest, DropNamespaceRequest, NamespaceExistsRequest,
5604        };
5605
5606        let temp_dir = TempStdDir::default();
5607        let temp_path = temp_dir.to_str().unwrap();
5608
5609        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5610            .inline_optimization_enabled(inline_optimization)
5611            .build()
5612            .await
5613            .unwrap();
5614
5615        // Create parent and child namespaces
5616        let mut create_req = CreateNamespaceRequest::new();
5617        create_req.id = Some(vec!["parent".to_string()]);
5618        dir_namespace.create_namespace(create_req).await.unwrap();
5619
5620        let mut create_req = CreateNamespaceRequest::new();
5621        create_req.id = Some(vec!["parent".to_string(), "child".to_string()]);
5622        dir_namespace.create_namespace(create_req).await.unwrap();
5623
5624        // Try to drop parent namespace - should fail because it has children
5625        let mut drop_req = DropNamespaceRequest::new();
5626        drop_req.id = Some(vec!["parent".to_string()]);
5627        let result = dir_namespace.drop_namespace(drop_req).await;
5628        assert!(result.is_err(), "Should fail when namespace has children");
5629
5630        // A table's id is not a namespace. Dropping it as one must leave the
5631        // table in place.
5632        let table_id = vec!["parent".to_string(), "t".to_string()];
5633        let mut create_table_req = CreateTableRequest::new();
5634        create_table_req.id = Some(table_id.clone());
5635        dir_namespace
5636            .create_table(create_table_req, Bytes::from(create_test_ipc_data()))
5637            .await
5638            .unwrap();
5639        let mut drop_req = DropNamespaceRequest::new();
5640        drop_req.id = Some(table_id.clone());
5641        let err = dir_namespace.drop_namespace(drop_req).await.unwrap_err();
5642        assert!(err.to_string().contains("Namespace not found"), "{err}");
5643        let exists_req = NamespaceExistsRequest {
5644            id: Some(table_id.clone()),
5645            ..Default::default()
5646        };
5647        let err = dir_namespace
5648            .namespace_exists(exists_req)
5649            .await
5650            .unwrap_err();
5651        assert!(err.to_string().contains("Namespace not found"), "{err}");
5652        let mut request = TableExistsRequest::new();
5653        request.id = Some(table_id);
5654        dir_namespace.table_exists(request).await.unwrap();
5655    }
5656
5657    #[rstest]
5658    #[case::with_optimization(true)]
5659    #[case::without_optimization(false)]
5660    #[tokio::test]
5661    async fn test_create_table_in_child_namespace(#[case] inline_optimization: bool) {
5662        use lance_namespace::models::{
5663            CreateNamespaceRequest, CreateTableRequest, ListTablesRequest,
5664        };
5665
5666        let temp_dir = TempStdDir::default();
5667        let temp_path = temp_dir.to_str().unwrap();
5668
5669        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5670            .inline_optimization_enabled(inline_optimization)
5671            .build()
5672            .await
5673            .unwrap();
5674
5675        // Create a child namespace
5676        let mut create_ns_req = CreateNamespaceRequest::new();
5677        create_ns_req.id = Some(vec!["ns1".to_string()]);
5678        dir_namespace.create_namespace(create_ns_req).await.unwrap();
5679
5680        // Create a table in the child namespace
5681        let buffer = create_test_ipc_data();
5682        let mut create_table_req = CreateTableRequest::new();
5683        create_table_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
5684        let result = dir_namespace
5685            .create_table(create_table_req, Bytes::from(buffer))
5686            .await;
5687        assert!(
5688            result.is_ok(),
5689            "Failed to create table in child namespace: {:?}",
5690            result.err()
5691        );
5692
5693        // List tables in the namespace
5694        let list_req = ListTablesRequest {
5695            id: Some(vec!["ns1".to_string()]),
5696            page_token: None,
5697            limit: None,
5698            ..Default::default()
5699        };
5700        let result = dir_namespace.list_tables(list_req).await;
5701        assert!(result.is_ok());
5702        let tables = result.unwrap();
5703        assert_eq!(tables.tables.len(), 1);
5704        assert_eq!(tables.tables[0], "table1");
5705
5706        // We refuse a name that a namespace holds, before we write any data.
5707        let taken = vec!["ns1".to_string(), "taken".to_string()];
5708        let mut create_ns_req = CreateNamespaceRequest::new();
5709        create_ns_req.id = Some(taken.clone());
5710        dir_namespace.create_namespace(create_ns_req).await.unwrap();
5711        let mut create_table_req = CreateTableRequest::new();
5712        create_table_req.id = Some(taken);
5713        let err = dir_namespace
5714            .create_table(create_table_req, Bytes::from(create_test_ipc_data()))
5715            .await
5716            .unwrap_err();
5717        assert!(err.to_string().contains("Table already exists"), "{err}");
5718        assert!(
5719            !dir_entry_names(temp_path)
5720                .iter()
5721                .any(|name| name.ends_with("_ns1$taken"))
5722        );
5723    }
5724
5725    #[rstest]
5726    #[case::with_optimization(true)]
5727    #[case::without_optimization(false)]
5728    #[tokio::test]
5729    async fn test_describe_child_namespace(#[case] inline_optimization: bool) {
5730        use lance_namespace::models::{CreateNamespaceRequest, DescribeNamespaceRequest};
5731
5732        let temp_dir = TempStdDir::default();
5733        let temp_path = temp_dir.to_str().unwrap();
5734
5735        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
5736            .inline_optimization_enabled(inline_optimization)
5737            .build()
5738            .await
5739            .unwrap();
5740
5741        // Create a child namespace with properties
5742        let mut properties = std::collections::HashMap::new();
5743        properties.insert("key1".to_string(), "value1".to_string());
5744
5745        let mut create_req = CreateNamespaceRequest::new();
5746        create_req.id = Some(vec!["ns1".to_string()]);
5747        create_req.properties = Some(properties.clone());
5748        dir_namespace.create_namespace(create_req).await.unwrap();
5749
5750        // Describe the namespace
5751        let describe_req = DescribeNamespaceRequest {
5752            id: Some(vec!["ns1".to_string()]),
5753            ..Default::default()
5754        };
5755        let result = dir_namespace.describe_namespace(describe_req).await;
5756        assert!(
5757            result.is_ok(),
5758            "Failed to describe namespace: {:?}",
5759            result.err()
5760        );
5761        let response = result.unwrap();
5762        assert!(response.properties.is_some());
5763        assert_eq!(
5764            response.properties.unwrap().get("key1"),
5765            Some(&"value1".to_string())
5766        );
5767    }
5768
5769    #[rstest]
5770    #[case::with_optimization(true)]
5771    #[case::without_optimization(false)]
5772    #[tokio::test]
5773    async fn test_concurrent_create_and_drop_single_instance(#[case] inline_optimization: bool) {
5774        use futures::future::join_all;
5775        use std::sync::Arc;
5776
5777        let temp_dir = TempStdDir::default();
5778        let temp_path = temp_dir.to_str().unwrap();
5779
5780        let dir_namespace = Arc::new(
5781            DirectoryNamespaceBuilder::new(temp_path)
5782                .inline_optimization_enabled(inline_optimization)
5783                .build()
5784                .await
5785                .unwrap(),
5786        );
5787
5788        // Initialize namespace first - create parent namespace to ensure __manifest table
5789        // is created before concurrent operations
5790        let mut create_ns_request = CreateNamespaceRequest::new();
5791        create_ns_request.id = Some(vec!["test_ns".to_string()]);
5792        dir_namespace
5793            .create_namespace(create_ns_request)
5794            .await
5795            .unwrap();
5796
5797        let num_tables = 10;
5798        let mut handles = Vec::new();
5799
5800        for i in 0..num_tables {
5801            let ns = dir_namespace.clone();
5802            let handle = async move {
5803                let table_name = format!("concurrent_table_{}", i);
5804                let table_id = vec!["test_ns".to_string(), table_name.clone()];
5805                let buffer = create_test_ipc_data();
5806
5807                // Create table
5808                let mut create_request = CreateTableRequest::new();
5809                create_request.id = Some(table_id.clone());
5810                ns.create_table(create_request, Bytes::from(buffer))
5811                    .await
5812                    .unwrap_or_else(|e| panic!("Failed to create table {}: {}", table_name, e));
5813
5814                // Drop table
5815                let mut drop_request = DropTableRequest::new();
5816                drop_request.id = Some(table_id);
5817                ns.drop_table(drop_request)
5818                    .await
5819                    .unwrap_or_else(|e| panic!("Failed to drop table {}: {}", table_name, e));
5820
5821                Ok::<_, lance_core::Error>(())
5822            };
5823            handles.push(handle);
5824        }
5825
5826        let results = join_all(handles).await;
5827        for result in results {
5828            assert!(result.is_ok(), "All concurrent operations should succeed");
5829        }
5830
5831        // Verify all tables are dropped
5832        let mut request = ListTablesRequest::new();
5833        request.id = Some(vec!["test_ns".to_string()]);
5834        let response = dir_namespace.list_tables(request).await.unwrap();
5835        assert_eq!(response.tables.len(), 0, "All tables should be dropped");
5836    }
5837
5838    #[rstest]
5839    #[case::with_optimization(true)]
5840    #[case::without_optimization(false)]
5841    #[tokio::test]
5842    async fn test_concurrent_create_and_drop_multiple_instances(#[case] inline_optimization: bool) {
5843        use futures::future::join_all;
5844
5845        let temp_dir = TempStdDir::default();
5846        let temp_path = temp_dir.to_str().unwrap().to_string();
5847
5848        // Initialize namespace first with a single instance to ensure __manifest
5849        // table is created and parent namespace exists before concurrent operations
5850        let init_ns = DirectoryNamespaceBuilder::new(&temp_path)
5851            .inline_optimization_enabled(inline_optimization)
5852            .build()
5853            .await
5854            .unwrap();
5855        let mut create_ns_request = CreateNamespaceRequest::new();
5856        create_ns_request.id = Some(vec!["test_ns".to_string()]);
5857        init_ns.create_namespace(create_ns_request).await.unwrap();
5858
5859        let num_tables = 10;
5860        let mut handles = Vec::new();
5861
5862        for i in 0..num_tables {
5863            let path = temp_path.clone();
5864            let handle = async move {
5865                // Each task creates its own namespace instance
5866                let ns = DirectoryNamespaceBuilder::new(&path)
5867                    .inline_optimization_enabled(inline_optimization)
5868                    .build()
5869                    .await
5870                    .unwrap();
5871
5872                let table_name = format!("multi_ns_table_{}", i);
5873                let table_id = vec!["test_ns".to_string(), table_name.clone()];
5874                let buffer = create_test_ipc_data();
5875
5876                // Create table
5877                let mut create_request = CreateTableRequest::new();
5878                create_request.id = Some(table_id.clone());
5879                ns.create_table(create_request, Bytes::from(buffer))
5880                    .await
5881                    .unwrap_or_else(|e| panic!("Failed to create table {}: {}", table_name, e));
5882
5883                // Drop table
5884                let mut drop_request = DropTableRequest::new();
5885                drop_request.id = Some(table_id);
5886                ns.drop_table(drop_request)
5887                    .await
5888                    .unwrap_or_else(|e| panic!("Failed to drop table {}: {}", table_name, e));
5889
5890                Ok::<_, lance_core::Error>(())
5891            };
5892            handles.push(handle);
5893        }
5894
5895        let results = join_all(handles).await;
5896        for result in results {
5897            assert!(result.is_ok(), "All concurrent operations should succeed");
5898        }
5899
5900        // Verify with a fresh namespace instance
5901        let verify_ns = DirectoryNamespaceBuilder::new(&temp_path)
5902            .inline_optimization_enabled(inline_optimization)
5903            .build()
5904            .await
5905            .unwrap();
5906
5907        let mut request = ListTablesRequest::new();
5908        request.id = Some(vec!["test_ns".to_string()]);
5909        let response = verify_ns.list_tables(request).await.unwrap();
5910        assert_eq!(response.tables.len(), 0, "All tables should be dropped");
5911    }
5912
5913    #[rstest]
5914    #[case::with_optimization(true)]
5915    #[case::without_optimization(false)]
5916    #[tokio::test]
5917    async fn test_concurrent_create_then_drop_from_different_instance(
5918        #[case] inline_optimization: bool,
5919    ) {
5920        use futures::future::join_all;
5921
5922        let temp_dir = TempStdDir::default();
5923        let temp_path = temp_dir.to_str().unwrap().to_string();
5924
5925        // Initialize namespace first with a single instance to ensure __manifest
5926        // table is created and parent namespace exists before concurrent operations
5927        let init_ns = DirectoryNamespaceBuilder::new(&temp_path)
5928            .inline_optimization_enabled(inline_optimization)
5929            .build()
5930            .await
5931            .unwrap();
5932        let mut create_ns_request = CreateNamespaceRequest::new();
5933        create_ns_request.id = Some(vec!["test_ns".to_string()]);
5934        init_ns.create_namespace(create_ns_request).await.unwrap();
5935
5936        let num_tables = 10;
5937
5938        // Phase 1: Create all tables concurrently using separate namespace instances
5939        let mut create_handles = Vec::new();
5940        for i in 0..num_tables {
5941            let path = temp_path.clone();
5942            let handle = async move {
5943                let ns = DirectoryNamespaceBuilder::new(&path)
5944                    .inline_optimization_enabled(inline_optimization)
5945                    .build()
5946                    .await
5947                    .unwrap();
5948
5949                let table_name = format!("cross_instance_table_{}", i);
5950                let table_id = vec!["test_ns".to_string(), table_name.clone()];
5951                let buffer = create_test_ipc_data();
5952
5953                let mut create_request = CreateTableRequest::new();
5954                create_request.id = Some(table_id);
5955                ns.create_table(create_request, Bytes::from(buffer))
5956                    .await
5957                    .unwrap_or_else(|e| panic!("Failed to create table {}: {}", table_name, e));
5958
5959                Ok::<_, lance_core::Error>(())
5960            };
5961            create_handles.push(handle);
5962        }
5963
5964        let create_results = join_all(create_handles).await;
5965        for result in create_results {
5966            assert!(result.is_ok(), "All create operations should succeed");
5967        }
5968
5969        // Phase 2: Drop all tables concurrently using NEW namespace instances
5970        let mut drop_handles = Vec::new();
5971        for i in 0..num_tables {
5972            let path = temp_path.clone();
5973            let handle = async move {
5974                let ns = DirectoryNamespaceBuilder::new(&path)
5975                    .inline_optimization_enabled(inline_optimization)
5976                    .build()
5977                    .await
5978                    .unwrap();
5979
5980                let table_name = format!("cross_instance_table_{}", i);
5981                let table_id = vec!["test_ns".to_string(), table_name.clone()];
5982
5983                let mut drop_request = DropTableRequest::new();
5984                drop_request.id = Some(table_id);
5985                ns.drop_table(drop_request)
5986                    .await
5987                    .unwrap_or_else(|e| panic!("Failed to drop table {}: {}", table_name, e));
5988
5989                Ok::<_, lance_core::Error>(())
5990            };
5991            drop_handles.push(handle);
5992        }
5993
5994        let drop_results = join_all(drop_handles).await;
5995        for result in drop_results {
5996            assert!(result.is_ok(), "All drop operations should succeed");
5997        }
5998
5999        // Verify all tables are dropped
6000        let verify_ns = DirectoryNamespaceBuilder::new(&temp_path)
6001            .inline_optimization_enabled(inline_optimization)
6002            .build()
6003            .await
6004            .unwrap();
6005
6006        let mut request = ListTablesRequest::new();
6007        request.id = Some(vec!["test_ns".to_string()]);
6008        let response = verify_ns.list_tables(request).await.unwrap();
6009        assert_eq!(response.tables.len(), 0, "All tables should be dropped");
6010    }
6011
6012    #[test]
6013    fn test_construct_full_uri_with_cloud_urls() {
6014        // Test S3-style URL with nested path (no trailing slash)
6015        let s3_result =
6016            ManifestNamespace::construct_full_uri("s3://bucket/path/subdir", "table.lance")
6017                .unwrap();
6018        assert_eq!(
6019            s3_result, "s3://bucket/path/subdir/table.lance",
6020            "S3 URL should correctly append table name to nested path"
6021        );
6022
6023        // Test Azure-style URL with nested path (no trailing slash)
6024        let az_result =
6025            ManifestNamespace::construct_full_uri("az://container/path/subdir", "table.lance")
6026                .unwrap();
6027        assert_eq!(
6028            az_result, "az://container/path/subdir/table.lance",
6029            "Azure URL should correctly append table name to nested path"
6030        );
6031
6032        // Test GCS-style URL with nested path (no trailing slash)
6033        let gs_result =
6034            ManifestNamespace::construct_full_uri("gs://bucket/path/subdir", "table.lance")
6035                .unwrap();
6036        assert_eq!(
6037            gs_result, "gs://bucket/path/subdir/table.lance",
6038            "GCS URL should correctly append table name to nested path"
6039        );
6040
6041        // Test with deeper nesting
6042        let deep_result =
6043            ManifestNamespace::construct_full_uri("s3://bucket/a/b/c/d", "my_table.lance").unwrap();
6044        assert_eq!(
6045            deep_result, "s3://bucket/a/b/c/d/my_table.lance",
6046            "Deeply nested path should work correctly"
6047        );
6048
6049        // Test with root-level path (single segment after bucket)
6050        let shallow_result =
6051            ManifestNamespace::construct_full_uri("s3://bucket", "table.lance").unwrap();
6052        assert_eq!(
6053            shallow_result, "s3://bucket/table.lance",
6054            "Single-level nested path should work correctly"
6055        );
6056
6057        // Test that URLs with trailing slash already work (no regression)
6058        let trailing_slash_result =
6059            ManifestNamespace::construct_full_uri("s3://bucket/path/subdir/", "table.lance")
6060                .unwrap();
6061        assert_eq!(
6062            trailing_slash_result, "s3://bucket/path/subdir/table.lance",
6063            "URL with existing trailing slash should still work"
6064        );
6065
6066        // Test that URLs with empty query string don't include trailing "?"
6067        // This is important because URL::to_string() can add "?" for empty queries
6068        let empty_query_result =
6069            ManifestNamespace::construct_full_uri("s3://bucket/path?", "table.lance").unwrap();
6070        assert_eq!(
6071            empty_query_result, "s3://bucket/path/table.lance",
6072            "URL with empty query string should not include trailing '?'"
6073        );
6074
6075        // Test that URLs with actual query parameters have them stripped
6076        // (query parameters are not meaningful for storage paths)
6077        let query_param_result =
6078            ManifestNamespace::construct_full_uri("s3://bucket/path?param=value", "table.lance")
6079                .unwrap();
6080        assert_eq!(
6081            query_param_result, "s3://bucket/path/table.lance",
6082            "URL with query parameters should have them stripped"
6083        );
6084    }
6085
6086    #[test]
6087    fn test_construct_full_uri_with_dollar_sign() {
6088        let result =
6089            ManifestNamespace::construct_full_uri("/tmp/root", "hash_workspace$test_table")
6090                .unwrap();
6091
6092        assert!(
6093            result.ends_with("/tmp/root/hash_workspace$test_table"),
6094            "local file URI should preserve dollar signs without adding empty path segments: {}",
6095            result
6096        );
6097        assert!(
6098            !result.contains("//hash_workspace$test_table"),
6099            "local file URI should not add a double slash before table directory: {}",
6100            result
6101        );
6102    }
6103
6104    #[test]
6105    fn test_construct_full_uri_with_nested_relative_location() {
6106        let result =
6107            ManifestNamespace::construct_full_uri("/tmp/root", "workspace/physical_table.lance")
6108                .unwrap();
6109
6110        assert!(
6111            result.ends_with("/tmp/root/workspace/physical_table.lance"),
6112            "nested relative location should preserve path separators: {}",
6113            result
6114        );
6115        assert!(
6116            !result.contains("%2Fphysical_table.lance"),
6117            "nested relative location should not encode path separators: {}",
6118            result
6119        );
6120    }
6121
6122    /// Test that concurrent create_table calls for the same table name don't
6123    /// create duplicate entries in the manifest. Uses two independent
6124    /// ManifestNamespace instances pointing at the same directory to simulate
6125    /// two separate OS processes racing on table creation. Copy-on-write rewrite
6126    /// retries ensure the second operation detects the duplicate after retrying
6127    /// against the latest data.
6128    #[tokio::test]
6129    async fn test_concurrent_create_table_no_duplicates() {
6130        let temp_dir = TempStdDir::default();
6131        let temp_path = temp_dir.to_str().unwrap();
6132
6133        // Two independent namespace instances = two separate "processes"
6134        // sharing the same underlying filesystem directory.
6135        let ns1 = DirectoryNamespaceBuilder::new(temp_path)
6136            .inline_optimization_enabled(false)
6137            .build()
6138            .await
6139            .unwrap();
6140        let ns2 = DirectoryNamespaceBuilder::new(temp_path)
6141            .inline_optimization_enabled(false)
6142            .build()
6143            .await
6144            .unwrap();
6145
6146        let buffer = create_test_ipc_data();
6147
6148        let mut req1 = CreateTableRequest::new();
6149        req1.id = Some(vec!["race_table".to_string()]);
6150        let mut req2 = CreateTableRequest::new();
6151        req2.id = Some(vec!["race_table".to_string()]);
6152
6153        // Launch both create_table calls concurrently
6154        let (result1, result2) = tokio::join!(
6155            ns1.create_table(req1, Bytes::from(buffer.clone())),
6156            ns2.create_table(req2, Bytes::from(buffer.clone())),
6157        );
6158
6159        // Exactly one should succeed and one should fail
6160        let success_count = [&result1, &result2].iter().filter(|r| r.is_ok()).count();
6161        let failure_count = [&result1, &result2].iter().filter(|r| r.is_err()).count();
6162        assert_eq!(
6163            success_count, 1,
6164            "Exactly one create should succeed, got: result1={:?}, result2={:?}",
6165            result1, result2
6166        );
6167        assert_eq!(
6168            failure_count, 1,
6169            "Exactly one create should fail, got: result1={:?}, result2={:?}",
6170            result1, result2
6171        );
6172
6173        // Verify only one table entry exists in the manifest
6174        let ns_check = DirectoryNamespaceBuilder::new(temp_path)
6175            .inline_optimization_enabled(false)
6176            .build()
6177            .await
6178            .unwrap();
6179        let mut list_request = ListTablesRequest::new();
6180        list_request.id = Some(vec![]);
6181        let response = ns_check.list_tables(list_request).await.unwrap();
6182        assert_eq!(
6183            response.tables.len(),
6184            1,
6185            "Should have exactly 1 table, found: {:?}",
6186            response.tables
6187        );
6188        assert_eq!(response.tables[0], "race_table");
6189
6190        // Also verify describe_table works (no "found 2" error)
6191        let mut describe_request = DescribeTableRequest::new();
6192        describe_request.id = Some(vec!["race_table".to_string()]);
6193        let describe_result = ns_check.describe_table(describe_request).await;
6194        assert!(
6195            describe_result.is_ok(),
6196            "describe_table should not fail with duplicate entries: {:?}",
6197            describe_result
6198        );
6199    }
6200
6201    // --- apply_pagination unit tests ---
6202
6203    fn names(v: &[&str]) -> Vec<String> {
6204        v.iter().map(|s| s.to_string()).collect()
6205    }
6206
6207    #[test]
6208    fn test_apply_pagination_no_token_no_limit() {
6209        let mut n = names(&["b", "a", "c"]);
6210        let next = ManifestNamespace::apply_pagination(&mut n, None, None);
6211        assert_eq!(n, names(&["a", "b", "c"]));
6212        assert_eq!(next, None);
6213    }
6214
6215    #[test]
6216    fn test_apply_pagination_limit_truncates_and_returns_token() {
6217        let mut n = names(&["c", "a", "b"]);
6218        let next = ManifestNamespace::apply_pagination(&mut n, None, Some(2));
6219        assert_eq!(n, names(&["a", "b"]));
6220        assert_eq!(next, Some("b".to_string()));
6221    }
6222
6223    #[test]
6224    fn test_apply_pagination_limit_zero_returns_empty_no_token() {
6225        let mut n = names(&["a", "b", "c"]);
6226        let next = ManifestNamespace::apply_pagination(&mut n, None, Some(0));
6227        assert!(n.is_empty());
6228        assert_eq!(next, None);
6229    }
6230
6231    #[test]
6232    fn test_apply_pagination_page_token_in_list() {
6233        // "b" is in the list; should start from "c" (strict >)
6234        let mut n = names(&["a", "b", "c", "d"]);
6235        let next = ManifestNamespace::apply_pagination(&mut n, Some("b".to_string()), None);
6236        assert_eq!(n, names(&["c", "d"]));
6237        assert_eq!(next, None);
6238    }
6239
6240    #[test]
6241    fn test_apply_pagination_page_token_past_all_items() {
6242        let mut n = names(&["a", "b", "c"]);
6243        let next = ManifestNamespace::apply_pagination(&mut n, Some("z".to_string()), None);
6244        assert!(n.is_empty());
6245        assert_eq!(next, None);
6246    }
6247
6248    #[test]
6249    fn test_apply_pagination_token_and_limit_combined() {
6250        let mut n = names(&["a", "b", "c", "d", "e"]);
6251        let next = ManifestNamespace::apply_pagination(&mut n, Some("b".to_string()), Some(2));
6252        assert_eq!(n, names(&["c", "d"]));
6253        assert_eq!(next, Some("d".to_string()));
6254    }
6255
6256    #[rstest]
6257    #[case::with_optimization(true)]
6258    #[case::without_optimization(false)]
6259    #[tokio::test]
6260    async fn test_alter_table_add_columns(#[case] inline_optimization: bool) {
6261        use lance_namespace::models::{
6262            AddColumnsEntry, AlterTableAddColumnsRequest, DescribeTableRequest,
6263        };
6264
6265        let temp_dir = TempStdDir::default();
6266        let temp_path = temp_dir.to_str().unwrap();
6267
6268        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6269            .inline_optimization_enabled(inline_optimization)
6270            .build()
6271            .await
6272            .unwrap();
6273
6274        // Create a table with id and name columns
6275        let buffer = create_test_ipc_data();
6276        let mut create_request = CreateTableRequest::new();
6277        create_request.id = Some(vec!["test_table".to_string()]);
6278        dir_namespace
6279            .create_table(create_request, Bytes::from(buffer))
6280            .await
6281            .unwrap();
6282
6283        // Add a new column using SQL expression
6284        let mut new_col = AddColumnsEntry::new("doubled_id".to_string());
6285        new_col.expression = Some(Some("id * 2".to_string()));
6286        let mut add_request = AlterTableAddColumnsRequest::new(vec![new_col]);
6287        add_request.id = Some(vec!["test_table".to_string()]);
6288
6289        let response = dir_namespace
6290            .alter_table_add_columns(add_request)
6291            .await
6292            .unwrap();
6293        // Version should have incremented
6294        assert!(response.version > 1);
6295
6296        // Verify the column was added by describing the table with detailed metadata
6297        let mut describe_request = DescribeTableRequest::new();
6298        describe_request.id = Some(vec!["test_table".to_string()]);
6299        describe_request.load_detailed_metadata = Some(true);
6300        let describe_response = dir_namespace
6301            .describe_table(describe_request)
6302            .await
6303            .unwrap();
6304        assert!(describe_response.schema.is_some());
6305
6306        let schema = describe_response.schema.unwrap();
6307        let field_names: Vec<&str> = schema.fields.iter().map(|f| f.name.as_str()).collect();
6308        assert!(
6309            field_names.contains(&"doubled_id"),
6310            "Column 'doubled_id' should exist after add_columns, got: {:?}",
6311            field_names
6312        );
6313    }
6314
6315    #[rstest]
6316    #[case::with_optimization(true)]
6317    #[case::without_optimization(false)]
6318    #[tokio::test]
6319    async fn test_alter_table_add_columns_missing_id(#[case] inline_optimization: bool) {
6320        use lance_namespace::models::{AddColumnsEntry, AlterTableAddColumnsRequest};
6321
6322        let temp_dir = TempStdDir::default();
6323        let temp_path = temp_dir.to_str().unwrap();
6324
6325        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6326            .inline_optimization_enabled(inline_optimization)
6327            .build()
6328            .await
6329            .unwrap();
6330
6331        // Request without ID should fail
6332        let new_col = AddColumnsEntry::new("col".to_string());
6333        let request = AlterTableAddColumnsRequest::new(vec![new_col]);
6334        let result = dir_namespace.alter_table_add_columns(request).await;
6335        assert!(result.is_err(), "Should fail when table ID is missing");
6336    }
6337
6338    #[rstest]
6339    #[case::with_optimization(true)]
6340    #[case::without_optimization(false)]
6341    #[tokio::test]
6342    async fn test_alter_table_add_columns_nonexistent_table(#[case] inline_optimization: bool) {
6343        use lance_namespace::models::{AddColumnsEntry, AlterTableAddColumnsRequest};
6344
6345        let temp_dir = TempStdDir::default();
6346        let temp_path = temp_dir.to_str().unwrap();
6347
6348        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6349            .inline_optimization_enabled(inline_optimization)
6350            .build()
6351            .await
6352            .unwrap();
6353
6354        // Request with non-existent table should fail
6355        let new_col = AddColumnsEntry::new("col".to_string());
6356        let mut request = AlterTableAddColumnsRequest::new(vec![new_col]);
6357        request.id = Some(vec!["nonexistent".to_string()]);
6358        let result = dir_namespace.alter_table_add_columns(request).await;
6359        assert!(result.is_err(), "Should fail when table does not exist");
6360    }
6361
6362    #[rstest]
6363    #[case::with_optimization(true)]
6364    #[case::without_optimization(false)]
6365    #[tokio::test]
6366    async fn test_alter_table_alter_columns_rename(#[case] inline_optimization: bool) {
6367        use lance_namespace::models::{
6368            AlterColumnsEntry, AlterTableAlterColumnsRequest, DescribeTableRequest,
6369        };
6370
6371        let temp_dir = TempStdDir::default();
6372        let temp_path = temp_dir.to_str().unwrap();
6373
6374        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6375            .inline_optimization_enabled(inline_optimization)
6376            .build()
6377            .await
6378            .unwrap();
6379
6380        // Create a table
6381        let buffer = create_test_ipc_data();
6382        let mut create_request = CreateTableRequest::new();
6383        create_request.id = Some(vec!["test_table".to_string()]);
6384        dir_namespace
6385            .create_table(create_request, Bytes::from(buffer))
6386            .await
6387            .unwrap();
6388
6389        // Rename the "name" column to "full_name"
6390        let mut entry = AlterColumnsEntry::new("name".to_string());
6391        entry.rename = Some(Some("full_name".to_string()));
6392        let mut alter_request = AlterTableAlterColumnsRequest::new(vec![entry]);
6393        alter_request.id = Some(vec!["test_table".to_string()]);
6394
6395        let response = dir_namespace
6396            .alter_table_alter_columns(alter_request)
6397            .await
6398            .unwrap();
6399        assert!(response.version > 1);
6400
6401        // Verify the column was renamed
6402        let mut describe_request = DescribeTableRequest::new();
6403        describe_request.id = Some(vec!["test_table".to_string()]);
6404        describe_request.load_detailed_metadata = Some(true);
6405        let describe_response = dir_namespace
6406            .describe_table(describe_request)
6407            .await
6408            .unwrap();
6409        assert!(describe_response.schema.is_some());
6410
6411        let schema = describe_response.schema.unwrap();
6412        let field_names: Vec<&str> = schema.fields.iter().map(|f| f.name.as_str()).collect();
6413        assert!(
6414            field_names.contains(&"full_name"),
6415            "Column should be renamed to 'full_name', got: {:?}",
6416            field_names
6417        );
6418        assert!(
6419            !field_names.contains(&"name"),
6420            "Old column name 'name' should no longer exist, got: {:?}",
6421            field_names
6422        );
6423    }
6424
6425    #[rstest]
6426    #[case::with_optimization(true)]
6427    #[case::without_optimization(false)]
6428    #[tokio::test]
6429    async fn test_alter_table_alter_columns_missing_id(#[case] inline_optimization: bool) {
6430        use lance_namespace::models::{AlterColumnsEntry, AlterTableAlterColumnsRequest};
6431
6432        let temp_dir = TempStdDir::default();
6433        let temp_path = temp_dir.to_str().unwrap();
6434
6435        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6436            .inline_optimization_enabled(inline_optimization)
6437            .build()
6438            .await
6439            .unwrap();
6440
6441        let entry = AlterColumnsEntry::new("name".to_string());
6442        let request = AlterTableAlterColumnsRequest::new(vec![entry]);
6443        let result = dir_namespace.alter_table_alter_columns(request).await;
6444        assert!(result.is_err(), "Should fail when table ID is missing");
6445    }
6446
6447    #[rstest]
6448    #[case::with_optimization(true)]
6449    #[case::without_optimization(false)]
6450    #[tokio::test]
6451    async fn test_alter_table_drop_columns(#[case] inline_optimization: bool) {
6452        use lance_namespace::models::{AlterTableDropColumnsRequest, DescribeTableRequest};
6453
6454        let temp_dir = TempStdDir::default();
6455        let temp_path = temp_dir.to_str().unwrap();
6456
6457        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6458            .inline_optimization_enabled(inline_optimization)
6459            .build()
6460            .await
6461            .unwrap();
6462
6463        // Create a table with id and name columns
6464        let buffer = create_test_ipc_data();
6465        let mut create_request = CreateTableRequest::new();
6466        create_request.id = Some(vec!["test_table".to_string()]);
6467        dir_namespace
6468            .create_table(create_request, Bytes::from(buffer))
6469            .await
6470            .unwrap();
6471
6472        // Drop the "name" column
6473        let mut drop_request = AlterTableDropColumnsRequest::new(vec!["name".to_string()]);
6474        drop_request.id = Some(vec!["test_table".to_string()]);
6475
6476        let response = dir_namespace
6477            .alter_table_drop_columns(drop_request)
6478            .await
6479            .unwrap();
6480        assert!(response.version > 1);
6481
6482        // Verify the column was dropped
6483        let mut describe_request = DescribeTableRequest::new();
6484        describe_request.id = Some(vec!["test_table".to_string()]);
6485        describe_request.load_detailed_metadata = Some(true);
6486        let describe_response = dir_namespace
6487            .describe_table(describe_request)
6488            .await
6489            .unwrap();
6490        assert!(describe_response.schema.is_some());
6491
6492        let schema = describe_response.schema.unwrap();
6493        let field_names: Vec<&str> = schema.fields.iter().map(|f| f.name.as_str()).collect();
6494        assert!(
6495            !field_names.contains(&"name"),
6496            "Column 'name' should have been dropped, got: {:?}",
6497            field_names
6498        );
6499        assert!(
6500            field_names.contains(&"id"),
6501            "Column 'id' should still exist, got: {:?}",
6502            field_names
6503        );
6504    }
6505
6506    #[rstest]
6507    #[case::with_optimization(true)]
6508    #[case::without_optimization(false)]
6509    #[tokio::test]
6510    async fn test_alter_table_drop_columns_missing_id(#[case] inline_optimization: bool) {
6511        use lance_namespace::models::AlterTableDropColumnsRequest;
6512
6513        let temp_dir = TempStdDir::default();
6514        let temp_path = temp_dir.to_str().unwrap();
6515
6516        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6517            .inline_optimization_enabled(inline_optimization)
6518            .build()
6519            .await
6520            .unwrap();
6521
6522        let request = AlterTableDropColumnsRequest::new(vec!["col".to_string()]);
6523        let result = dir_namespace.alter_table_drop_columns(request).await;
6524        assert!(result.is_err(), "Should fail when table ID is missing");
6525    }
6526
6527    #[rstest]
6528    #[case::with_optimization(true)]
6529    #[case::without_optimization(false)]
6530    #[tokio::test]
6531    async fn test_alter_table_drop_columns_nonexistent_table(#[case] inline_optimization: bool) {
6532        use lance_namespace::models::AlterTableDropColumnsRequest;
6533
6534        let temp_dir = TempStdDir::default();
6535        let temp_path = temp_dir.to_str().unwrap();
6536
6537        let dir_namespace = DirectoryNamespaceBuilder::new(temp_path)
6538            .inline_optimization_enabled(inline_optimization)
6539            .build()
6540            .await
6541            .unwrap();
6542
6543        let mut request = AlterTableDropColumnsRequest::new(vec!["col".to_string()]);
6544        request.id = Some(vec!["nonexistent".to_string()]);
6545        let result = dir_namespace.alter_table_drop_columns(request).await;
6546        assert!(result.is_err(), "Should fail when table does not exist");
6547    }
6548}