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