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