Skip to main content

lance_namespace_impls/
dir.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Directory-based Lance Namespace implementation.
5//!
6//! This module provides a directory-based implementation of the Lance namespace
7//! that stores tables as Lance datasets in a filesystem directory structure.
8
9pub mod manifest;
10pub mod manifest_feature_flags;
11
12use arrow::array::Float32Array;
13use arrow::record_batch::RecordBatchIterator;
14use arrow_ipc::reader::StreamReader;
15use async_trait::async_trait;
16use bytes::Bytes;
17use futures::{StreamExt, TryStreamExt};
18use lance::dataset::builder::DatasetBuilder;
19use lance::dataset::refs::check_valid_branch;
20use lance::dataset::scanner::Scanner;
21use lance::dataset::statistics::DatasetStatisticsExt;
22use lance::dataset::transaction::{Operation, Transaction};
23use lance::dataset::{
24    Dataset, MergeInsertBuilder, WhenMatched, WhenNotMatched, WhenNotMatchedBySource, WriteMode,
25    WriteParams,
26};
27use lance::index::{DatasetIndexExt, IndexParams, vector::VectorIndexParams};
28use lance::session::Session;
29use lance_index::scalar::{
30    BuiltinIndexType, FullTextSearchQuery, InvertedIndexParams, ScalarIndexParams,
31};
32use lance_index::vector::{
33    bq::RQBuildParams, hnsw::builder::HnswBuildParams, ivf::IvfBuildParams, pq::PQBuildParams,
34    sq::builder::SQBuildParams,
35};
36use lance_index::{IndexType, is_system_index};
37use lance_io::object_store::{ObjectStore, ObjectStoreParams, ObjectStoreRegistry};
38use lance_linalg::distance::MetricType;
39use lance_table::io::commit::{ManifestNamingScheme, VERSIONS_DIR};
40use object_store::ObjectStoreExt;
41use object_store::path::Path;
42use object_store::{Error as ObjectStoreError, ObjectStore as OSObjectStore, PutMode, PutOptions};
43use std::collections::HashMap;
44use std::io::Cursor;
45use std::sync::{Arc, Mutex};
46
47use crate::context::DynamicContextProvider;
48use lance_namespace::models::{
49    AnalyzeTableQueryPlanRequest, BatchDeleteTableVersionsRequest,
50    BatchDeleteTableVersionsResponse, BranchContents as ModelBranchContents, CountTableRowsRequest,
51    CreateNamespaceRequest, CreateNamespaceResponse, CreateTableBranchRequest,
52    CreateTableBranchResponse, CreateTableIndexRequest, CreateTableIndexResponse,
53    CreateTableRequest, CreateTableResponse, CreateTableScalarIndexResponse, CreateTableTagRequest,
54    CreateTableTagResponse, CreateTableVersionRequest, CreateTableVersionResponse,
55    DeclareTableRequest, DeclareTableResponse, DeleteTableBranchRequest, DeleteTableBranchResponse,
56    DeleteTableTagRequest, DeleteTableTagResponse, DescribeNamespaceRequest,
57    DescribeNamespaceResponse, DescribeTableIndexStatsRequest, DescribeTableIndexStatsResponse,
58    DescribeTableRequest, DescribeTableResponse, DescribeTableVersionRequest,
59    DescribeTableVersionResponse, DescribeTransactionRequest, DescribeTransactionResponse,
60    DropNamespaceRequest, DropNamespaceResponse, DropTableIndexRequest, DropTableIndexResponse,
61    DropTableRequest, DropTableResponse, ExplainTableQueryPlanRequest, FragmentStats,
62    FragmentSummary, GetTableStatsRequest, GetTableStatsResponse, GetTableTagVersionRequest,
63    GetTableTagVersionResponse, Identity, IndexContent, InsertIntoTableRequest,
64    InsertIntoTableResponse, ListNamespacesRequest, ListNamespacesResponse,
65    ListTableBranchesRequest, ListTableBranchesResponse, ListTableIndicesRequest,
66    ListTableIndicesResponse, ListTableTagsRequest, ListTableTagsResponse,
67    ListTableVersionsRequest, ListTableVersionsResponse, ListTablesRequest, ListTablesResponse,
68    MergeInsertIntoTableRequest, MergeInsertIntoTableResponse, NamespaceExistsRequest,
69    QueryTableRequest, QueryTableRequestColumns, QueryTableRequestVector, RestoreTableRequest,
70    RestoreTableResponse, TableExistsRequest, TableVersion, TagContents as ModelTagContents,
71    UpdateTableSchemaMetadataRequest, UpdateTableSchemaMetadataResponse, UpdateTableTagRequest,
72    UpdateTableTagResponse,
73};
74
75use lance_core::{Error, Result};
76use lance_namespace::LanceNamespace;
77use lance_namespace::error::NamespaceError;
78use lance_namespace::schema::arrow_schema_to_json;
79
80use crate::credentials::{
81    CredentialVendor, create_credential_vendor_for_location, has_credential_vendor_config,
82};
83
84/// Thread-safe metrics tracker for namespace operations.
85///
86/// Tracks the count of each API operation when `ops_metrics_enabled` is true.
87/// Use `retrieve()` to get a snapshot of all operation counts.
88#[derive(Debug, Default)]
89pub struct OpsMetrics {
90    counters: Mutex<HashMap<String, u64>>,
91}
92
93impl OpsMetrics {
94    /// Increment the counter for an operation.
95    pub fn increment(&self, operation: &str) {
96        if let Ok(mut counters) = self.counters.lock() {
97            *counters.entry(operation.to_string()).or_insert(0) += 1;
98        }
99    }
100
101    /// Get a snapshot of all operation counts.
102    pub fn retrieve(&self) -> HashMap<String, u64> {
103        self.counters.lock().map(|c| c.clone()).unwrap_or_default()
104    }
105
106    /// Reset all counters to zero.
107    pub fn reset(&self) {
108        if let Ok(mut counters) = self.counters.lock() {
109            counters.clear();
110        }
111    }
112}
113
114/// Result of checking table status atomically.
115///
116/// This struct captures the state of a table directory in a single snapshot,
117/// avoiding race conditions between checking existence and other status flags.
118pub(crate) struct TableStatus {
119    /// Whether the table directory exists (has any files)
120    pub(crate) exists: bool,
121    /// Whether the table has a `.lance-deregistered` marker file
122    pub(crate) is_deregistered: bool,
123    /// Whether the table has a `.lance-reserved` marker file (declared but not written)
124    pub(crate) has_reserved_file: bool,
125}
126
127enum DirectoryIndexParams {
128    Scalar {
129        index_type: IndexType,
130        params: ScalarIndexParams,
131    },
132    Inverted(InvertedIndexParams),
133    Vector {
134        index_type: IndexType,
135        params: VectorIndexParams,
136    },
137}
138
139impl DirectoryIndexParams {
140    fn index_type(&self) -> IndexType {
141        match self {
142            Self::Scalar { index_type, .. } | Self::Vector { index_type, .. } => *index_type,
143            Self::Inverted(_) => IndexType::Inverted,
144        }
145    }
146
147    fn params(&self) -> &dyn IndexParams {
148        match self {
149            Self::Scalar { params, .. } => params,
150            Self::Inverted(params) => params,
151            Self::Vector { params, .. } => params,
152        }
153    }
154}
155
156/// Builder for creating a DirectoryNamespace.
157///
158/// This builder provides a fluent API for configuring and establishing
159/// connections to directory-based Lance namespaces.
160///
161/// # Examples
162///
163/// ```no_run
164/// # use lance_namespace_impls::DirectoryNamespaceBuilder;
165/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
166/// // Create a local directory namespace
167/// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
168///     .build()
169///     .await?;
170/// # Ok(())
171/// # }
172/// ```
173///
174/// ```no_run
175/// # use lance_namespace_impls::DirectoryNamespaceBuilder;
176/// # use lance::session::Session;
177/// # use std::sync::Arc;
178/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
179/// // Create with custom storage options and session
180/// let session = Arc::new(Session::default());
181/// let namespace = DirectoryNamespaceBuilder::new("s3://bucket/path")
182///     .storage_option("region", "us-west-2")
183///     .storage_option("access_key_id", "key")
184///     .session(session)
185///     .build()
186///     .await?;
187/// # Ok(())
188/// # }
189/// ```
190#[derive(Clone)]
191pub struct DirectoryNamespaceBuilder {
192    root: String,
193    storage_options: Option<HashMap<String, String>>,
194    session: Option<Arc<Session>>,
195    manifest_enabled: bool,
196    dir_listing_enabled: bool,
197    inline_optimization_enabled: bool,
198    table_version_tracking_enabled: bool,
199    /// When true, enables migration mode where the namespace checks the manifest first
200    /// before falling back to directory listing for root-level tables. When false (default),
201    /// root-level tables use directory listing directly without checking the manifest,
202    /// avoiding extra object store calls.
203    dir_listing_to_manifest_migration_enabled: bool,
204    credential_vendor_properties: HashMap<String, String>,
205    context_provider: Option<Arc<dyn DynamicContextProvider>>,
206    commit_retries: Option<u32>,
207    /// When true, returns input storage options in describe_table/declare_table responses
208    /// when no credential vendor is configured. Useful for testing. Default: false.
209    vend_input_storage_options: bool,
210    /// When set, adds expires_at_millis to vended storage options. The value is calculated
211    /// as current_time_millis + this interval. This allows clients to know when to refresh
212    /// credentials by calling describe_table again. Only effective when vend_input_storage_options
213    /// is true.
214    vend_input_storage_options_refresh_interval_millis: Option<u64>,
215    /// When true, tracks operation metrics. Default: false.
216    ops_metrics_enabled: bool,
217}
218
219impl std::fmt::Debug for DirectoryNamespaceBuilder {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        f.debug_struct("DirectoryNamespaceBuilder")
222            .field("root", &self.root)
223            .field("storage_options", &self.storage_options)
224            .field("manifest_enabled", &self.manifest_enabled)
225            .field("dir_listing_enabled", &self.dir_listing_enabled)
226            .field(
227                "inline_optimization_enabled",
228                &self.inline_optimization_enabled,
229            )
230            .field(
231                "table_version_tracking_enabled",
232                &self.table_version_tracking_enabled,
233            )
234            .field(
235                "dir_listing_to_manifest_migration_enabled",
236                &self.dir_listing_to_manifest_migration_enabled,
237            )
238            .field(
239                "context_provider",
240                &self.context_provider.as_ref().map(|_| "Some(...)"),
241            )
242            .field(
243                "vend_input_storage_options",
244                &self.vend_input_storage_options,
245            )
246            .field(
247                "vend_input_storage_options_refresh_interval_millis",
248                &self.vend_input_storage_options_refresh_interval_millis,
249            )
250            .field("ops_metrics_enabled", &self.ops_metrics_enabled)
251            .finish()
252    }
253}
254
255impl DirectoryNamespaceBuilder {
256    /// Create a new DirectoryNamespaceBuilder with the specified root path.
257    ///
258    /// # Arguments
259    ///
260    /// * `root` - Root directory path (local path or cloud URI like s3://bucket/path)
261    pub fn new(root: impl Into<String>) -> Self {
262        Self {
263            root: root.into().trim_end_matches('/').to_string(),
264            storage_options: None,
265            session: None,
266            manifest_enabled: true,
267            dir_listing_enabled: true, // Default to enabled for backwards compatibility
268            inline_optimization_enabled: true,
269            table_version_tracking_enabled: false, // Default to disabled
270            dir_listing_to_manifest_migration_enabled: false, // Default to disabled
271            credential_vendor_properties: HashMap::new(),
272            context_provider: None,
273            commit_retries: None,
274            vend_input_storage_options: false,
275            vend_input_storage_options_refresh_interval_millis: None,
276            ops_metrics_enabled: false,
277        }
278    }
279
280    /// Enable or disable manifest-based listing.
281    ///
282    /// When enabled (default), the namespace uses a `__manifest` table to track tables.
283    /// When disabled, relies solely on directory scanning.
284    pub fn manifest_enabled(mut self, enabled: bool) -> Self {
285        self.manifest_enabled = enabled;
286        self
287    }
288
289    /// Enable or disable directory-based listing fallback.
290    ///
291    /// When enabled (default), falls back to directory scanning for tables not in the manifest.
292    /// When disabled, only consults the manifest table.
293    pub fn dir_listing_enabled(mut self, enabled: bool) -> Self {
294        self.dir_listing_enabled = enabled;
295        self
296    }
297
298    /// Enable or disable migration mode from directory listing to manifest.
299    ///
300    /// When enabled, root-level table operations check the manifest first before
301    /// falling back to directory listing. When disabled (default), root-level tables
302    /// use directory listing directly, avoiding extra object store calls.
303    /// Only relevant when both `manifest_enabled` and `dir_listing_enabled` are true.
304    pub fn dir_listing_to_manifest_migration_enabled(mut self, enabled: bool) -> Self {
305        self.dir_listing_to_manifest_migration_enabled = enabled;
306        self
307    }
308
309    /// Enable or disable replacement index maintenance for the __manifest table.
310    ///
311    /// When enabled (default), copy-on-write manifest rewrites build replacement indices
312    /// for fast reads. When disabled, rewrites only replace data files.
313    pub fn inline_optimization_enabled(mut self, enabled: bool) -> Self {
314        self.inline_optimization_enabled = enabled;
315        self
316    }
317
318    /// Enable or disable table version tracking through the namespace.
319    ///
320    /// When enabled, `describe_table` returns `managed_versioning: true` to indicate
321    /// that commits should go through the namespace's table version APIs rather than
322    /// direct object store operations.
323    ///
324    /// When disabled (default), `managed_versioning` is not set.
325    pub fn table_version_tracking_enabled(mut self, enabled: bool) -> Self {
326        self.table_version_tracking_enabled = enabled;
327        self
328    }
329
330    /// Create a DirectoryNamespaceBuilder from properties HashMap.
331    ///
332    /// This method parses a properties map into builder configuration.
333    /// It expects:
334    /// - `root`: The root directory path (required)
335    /// - `manifest_enabled`: Enable manifest-based table tracking (optional, default: true)
336    /// - `dir_listing_enabled`: Enable directory listing for table discovery (optional, default: true)
337    /// - `inline_optimization_enabled`: Enable replacement indices on __manifest rewrites (optional, default: true)
338    /// - `storage.*`: Storage options (optional, prefix will be stripped)
339    ///
340    /// Credential vendor properties (prefixed with `credential_vendor.`, prefix is stripped):
341    /// - `credential_vendor.enabled`: Set to "true" to enable credential vending (required)
342    /// - `credential_vendor.permission`: Permission level: read, write, or admin (default: read)
343    ///
344    /// AWS-specific properties (for s3:// locations):
345    /// - `credential_vendor.aws_role_arn`: AWS IAM role ARN (required for AWS)
346    /// - `credential_vendor.aws_external_id`: AWS external ID (optional)
347    /// - `credential_vendor.aws_region`: AWS region (optional)
348    /// - `credential_vendor.aws_role_session_name`: AWS role session name (optional)
349    /// - `credential_vendor.aws_duration_millis`: Credential duration in ms (default: 3600000, range: 15min-12hrs)
350    ///
351    /// GCP-specific properties (for gs:// locations):
352    /// - `credential_vendor.gcp_service_account`: Service account to impersonate (optional)
353    /// - `credential_vendor.gcp_workload_identity_provider`: Workload Identity Provider for OIDC token exchange (optional)
354    /// - `credential_vendor.gcp_impersonation_service_account`: Service account to impersonate after workload identity exchange (optional)
355    ///
356    /// Note: GCP uses Application Default Credentials (ADC). To use a service account key file,
357    /// set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable before starting.
358    /// GCP token duration cannot be configured; it's determined by the STS endpoint (typically 1 hour).
359    ///
360    /// Azure-specific properties (for az:// locations):
361    /// - `credential_vendor.azure_account_name`: Azure storage account name (required for Azure)
362    /// - `credential_vendor.azure_tenant_id`: Azure tenant ID (optional)
363    /// - `credential_vendor.azure_federated_client_id`: Client ID used for workload identity federation (optional)
364    /// - `credential_vendor.azure_duration_millis`: Credential duration in ms (default: 3600000, up to 7 days)
365    ///
366    /// # Arguments
367    ///
368    /// * `properties` - Configuration properties
369    /// * `session` - Optional Lance session to reuse object store registry
370    ///
371    /// # Returns
372    ///
373    /// Returns a `DirectoryNamespaceBuilder` instance.
374    ///
375    /// # Errors
376    ///
377    /// Returns an error if the `root` property is missing.
378    ///
379    /// # Examples
380    ///
381    /// ```no_run
382    /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
383    /// # use std::collections::HashMap;
384    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
385    /// let mut properties = HashMap::new();
386    /// properties.insert("root".to_string(), "/path/to/data".to_string());
387    /// properties.insert("manifest_enabled".to_string(), "true".to_string());
388    /// properties.insert("dir_listing_enabled".to_string(), "false".to_string());
389    /// properties.insert("storage.region".to_string(), "us-west-2".to_string());
390    ///
391    /// let namespace = DirectoryNamespaceBuilder::from_properties(properties, None)?
392    ///     .build()
393    ///     .await?;
394    /// # Ok(())
395    /// # }
396    /// ```
397    pub fn from_properties(
398        properties: HashMap<String, String>,
399        session: Option<Arc<Session>>,
400    ) -> Result<Self> {
401        // Extract root from properties (required)
402        let root = properties.get("root").cloned().ok_or_else(|| {
403            lance_core::Error::from(NamespaceError::InvalidInput {
404                message: "Missing required property 'root' for directory namespace".to_string(),
405            })
406        })?;
407
408        // Extract storage options (properties prefixed with "storage.")
409        let storage_options: HashMap<String, String> = properties
410            .iter()
411            .filter_map(|(k, v)| {
412                k.strip_prefix("storage.")
413                    .map(|key| (key.to_string(), v.clone()))
414            })
415            .collect();
416
417        let storage_options = if storage_options.is_empty() {
418            None
419        } else {
420            Some(storage_options)
421        };
422
423        // Extract manifest_enabled (default: true)
424        let manifest_enabled = properties
425            .get("manifest_enabled")
426            .and_then(|v| v.parse::<bool>().ok())
427            .unwrap_or(true);
428
429        // Extract dir_listing_enabled (default: true)
430        let dir_listing_enabled = properties
431            .get("dir_listing_enabled")
432            .and_then(|v| v.parse::<bool>().ok())
433            .unwrap_or(true);
434
435        // Extract inline_optimization_enabled (default: true)
436        let inline_optimization_enabled = properties
437            .get("inline_optimization_enabled")
438            .and_then(|v| v.parse::<bool>().ok())
439            .unwrap_or(true);
440
441        // Extract table_version_tracking_enabled (default: false)
442        let table_version_tracking_enabled = properties
443            .get("table_version_tracking_enabled")
444            .and_then(|v| v.parse::<bool>().ok())
445            .unwrap_or(false);
446
447        // Extract dir_listing_to_manifest_migration_enabled (default: false)
448        let dir_listing_to_manifest_migration_enabled = properties
449            .get("dir_listing_to_manifest_migration_enabled")
450            .and_then(|v| v.parse::<bool>().ok())
451            .unwrap_or(false);
452
453        // Extract credential vendor properties (properties prefixed with "credential_vendor.")
454        // The prefix is stripped to get short property names
455        // The build() method will check if enabled=true before creating the vendor
456        let credential_vendor_properties: HashMap<String, String> = properties
457            .iter()
458            .filter_map(|(k, v)| {
459                k.strip_prefix("credential_vendor.")
460                    .map(|key| (key.to_string(), v.clone()))
461            })
462            .collect();
463
464        let commit_retries = properties
465            .get("commit_retries")
466            .and_then(|v| v.parse::<u32>().ok());
467
468        // Extract vend_input_storage_options (default: false)
469        let vend_input_storage_options = properties
470            .get("vend_input_storage_options")
471            .and_then(|v| v.parse::<bool>().ok())
472            .unwrap_or(false);
473
474        // Extract vend_input_storage_options_refresh_interval_millis (optional)
475        let vend_input_storage_options_refresh_interval_millis = properties
476            .get("vend_input_storage_options_refresh_interval_millis")
477            .and_then(|v| v.parse::<u64>().ok());
478
479        // Extract ops_metrics_enabled (default: false)
480        let ops_metrics_enabled = properties
481            .get("ops_metrics_enabled")
482            .and_then(|v| v.parse::<bool>().ok())
483            .unwrap_or(false);
484
485        Ok(Self {
486            root: root.trim_end_matches('/').to_string(),
487            storage_options,
488            session,
489            manifest_enabled,
490            dir_listing_enabled,
491            inline_optimization_enabled,
492            table_version_tracking_enabled,
493            dir_listing_to_manifest_migration_enabled,
494            credential_vendor_properties,
495            context_provider: None,
496            commit_retries,
497            vend_input_storage_options,
498            vend_input_storage_options_refresh_interval_millis,
499            ops_metrics_enabled,
500        })
501    }
502
503    /// Add a storage option.
504    ///
505    /// # Arguments
506    ///
507    /// * `key` - Storage option key (e.g., "region", "access_key_id")
508    /// * `value` - Storage option value
509    pub fn storage_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
510        self.storage_options
511            .get_or_insert_with(HashMap::new)
512            .insert(key.into(), value.into());
513        self
514    }
515
516    /// Add multiple storage options.
517    ///
518    /// # Arguments
519    ///
520    /// * `options` - HashMap of storage options to add
521    pub fn storage_options(mut self, options: HashMap<String, String>) -> Self {
522        self.storage_options
523            .get_or_insert_with(HashMap::new)
524            .extend(options);
525        self
526    }
527
528    /// Set the Lance session to use for this namespace.
529    ///
530    /// When a session is provided, the namespace will reuse the session's
531    /// object store registry, allowing multiple namespaces and datasets
532    /// to share the same underlying storage connections.
533    ///
534    /// # Arguments
535    ///
536    /// * `session` - Arc-wrapped Lance session
537    pub fn session(mut self, session: Arc<Session>) -> Self {
538        self.session = Some(session);
539        self
540    }
541
542    /// Set the number of retries for commit operations on the manifest table.
543    /// If not set, defaults to [`lance_table::io::commit::CommitConfig`] default (20).
544    pub fn commit_retries(mut self, retries: u32) -> Self {
545        self.commit_retries = Some(retries);
546        self
547    }
548
549    /// Add a credential vendor property.
550    ///
551    /// Use short property names without the `credential_vendor.` prefix.
552    /// Common properties: `enabled`, `permission`.
553    /// AWS properties: `aws_role_arn`, `aws_external_id`, `aws_region`, `aws_role_session_name`, `aws_duration_millis`.
554    /// GCP properties: `gcp_service_account`, `gcp_workload_identity_provider`, `gcp_impersonation_service_account`.
555    /// Azure properties: `azure_account_name`, `azure_tenant_id`, `azure_federated_client_id`, `azure_duration_millis`.
556    ///
557    /// # Arguments
558    ///
559    /// * `key` - Property key (e.g., "enabled", "aws_role_arn")
560    /// * `value` - Property value
561    ///
562    /// # Example
563    ///
564    /// ```no_run
565    /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
566    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
567    /// let namespace = DirectoryNamespaceBuilder::new("s3://my-bucket/data")
568    ///     .credential_vendor_property("enabled", "true")
569    ///     .credential_vendor_property("aws_role_arn", "arn:aws:iam::123456789012:role/MyRole")
570    ///     .credential_vendor_property("permission", "read")
571    ///     .build()
572    ///     .await?;
573    /// # Ok(())
574    /// # }
575    /// ```
576    pub fn credential_vendor_property(
577        mut self,
578        key: impl Into<String>,
579        value: impl Into<String>,
580    ) -> Self {
581        self.credential_vendor_properties
582            .insert(key.into(), value.into());
583        self
584    }
585
586    /// Add multiple credential vendor properties.
587    ///
588    /// Use short property names without the `credential_vendor.` prefix.
589    ///
590    /// # Arguments
591    ///
592    /// * `properties` - HashMap of credential vendor properties to add
593    pub fn credential_vendor_properties(mut self, properties: HashMap<String, String>) -> Self {
594        self.credential_vendor_properties.extend(properties);
595        self
596    }
597
598    /// Set a dynamic context provider for per-request context.
599    ///
600    /// The provider can be used to generate additional context for operations.
601    /// For DirectoryNamespace, the context is stored but not directly used
602    /// in operations (unlike RestNamespace where it's converted to HTTP headers).
603    ///
604    /// # Arguments
605    ///
606    /// * `provider` - The context provider implementation
607    pub fn context_provider(mut self, provider: Arc<dyn DynamicContextProvider>) -> Self {
608        self.context_provider = Some(provider);
609        self
610    }
611
612    /// Enable or disable returning input storage options in responses.
613    ///
614    /// When enabled, `describe_table` and `declare_table` will return the storage
615    /// options passed to the builder when no credential vendor is configured.
616    /// This is useful for testing scenarios where you want to pass storage options
617    /// through to clients.
618    ///
619    /// Default is false (storage options are not returned unless credential vending is configured).
620    pub fn vend_input_storage_options(mut self, enabled: bool) -> Self {
621        self.vend_input_storage_options = enabled;
622        self
623    }
624
625    /// Set the refresh interval for vended input storage options.
626    ///
627    /// When set, vended storage options will include an `expires_at_millis` field
628    /// calculated as `current_time_millis + interval_millis`. This allows clients
629    /// to know when to refresh credentials by calling `describe_table` again.
630    ///
631    /// This only has effect when `vend_input_storage_options` is enabled.
632    ///
633    /// # Arguments
634    ///
635    /// * `interval_millis` - The refresh interval in milliseconds
636    pub fn vend_input_storage_options_refresh_interval_millis(
637        mut self,
638        interval_millis: u64,
639    ) -> Self {
640        self.vend_input_storage_options_refresh_interval_millis = Some(interval_millis);
641        self
642    }
643
644    /// Enable or disable operation metrics tracking.
645    ///
646    /// When enabled, the namespace will track how many times each API operation
647    /// is called. Use `retrieve_ops_metrics()` on the built namespace to get
648    /// the current counts.
649    ///
650    /// Default is false.
651    pub fn ops_metrics_enabled(mut self, enabled: bool) -> Self {
652        self.ops_metrics_enabled = enabled;
653        self
654    }
655
656    /// Build the DirectoryNamespace.
657    ///
658    /// # Returns
659    ///
660    /// Returns a `DirectoryNamespace` instance.
661    ///
662    /// # Errors
663    ///
664    /// Returns an error if:
665    /// - The root path is invalid
666    /// - Connection to the storage backend fails
667    /// - Storage options are invalid
668    pub async fn build(self) -> Result<DirectoryNamespace> {
669        let (object_store, base_path) =
670            Self::initialize_object_store(&self.root, &self.storage_options, &self.session).await?;
671
672        let manifest_ns = if self.manifest_enabled {
673            match manifest::ManifestNamespace::from_directory(
674                self.root.clone(),
675                self.storage_options.clone(),
676                self.session.clone(),
677                object_store.clone(),
678                base_path.clone(),
679                self.dir_listing_enabled,
680                self.inline_optimization_enabled,
681                self.commit_retries,
682            )
683            .await
684            {
685                Ok(ns) => Some(Arc::new(ns)),
686                Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
687                    // The manifest exists but was written with a feature flag this
688                    // build does not understand. Refuse rather than silently
689                    // degrading to a directory-listing view that ignores it.
690                    return Err(e);
691                }
692                Err(e) => {
693                    // Failed to initialize manifest namespace, fall back to directory listing only
694                    log::warn!(
695                        "Failed to initialize manifest namespace, falling back to directory listing only: {}",
696                        e
697                    );
698                    None
699                }
700            }
701        } else {
702            None
703        };
704
705        // Create credential vendor once during initialization if enabled
706        let credential_vendor = if has_credential_vendor_config(&self.credential_vendor_properties)
707        {
708            create_credential_vendor_for_location(&self.root, &self.credential_vendor_properties)
709                .await?
710                .map(Arc::from)
711        } else {
712            None
713        };
714
715        let ops_metrics = if self.ops_metrics_enabled {
716            Some(Arc::new(OpsMetrics::default()))
717        } else {
718            None
719        };
720
721        Ok(DirectoryNamespace {
722            root: self.root,
723            storage_options: self.storage_options,
724            session: self.session,
725            object_store,
726            base_path,
727            manifest_ns,
728            dir_listing_enabled: self.dir_listing_enabled,
729            dir_listing_to_manifest_migration_enabled: self
730                .dir_listing_to_manifest_migration_enabled,
731            table_version_tracking_enabled: self.table_version_tracking_enabled,
732            credential_vendor,
733            context_provider: self.context_provider,
734            vend_input_storage_options: self.vend_input_storage_options,
735            vend_input_storage_options_refresh_interval_millis: self
736                .vend_input_storage_options_refresh_interval_millis,
737            ops_metrics,
738        })
739    }
740
741    /// Initialize the Lance ObjectStore based on the configuration
742    async fn initialize_object_store(
743        root: &str,
744        storage_options: &Option<HashMap<String, String>>,
745        session: &Option<Arc<Session>>,
746    ) -> Result<(Arc<ObjectStore>, Path)> {
747        // Build ObjectStoreParams from storage options
748        let accessor = storage_options.clone().map(|opts| {
749            Arc::new(lance_io::object_store::StorageOptionsAccessor::with_static_options(opts))
750        });
751        let params = ObjectStoreParams {
752            storage_options_accessor: accessor,
753            ..Default::default()
754        };
755
756        // Use object store registry from session if provided, otherwise create a new one
757        let registry = if let Some(session) = session {
758            session.store_registry()
759        } else {
760            Arc::new(ObjectStoreRegistry::default())
761        };
762
763        // Use Lance's object store factory to create from URI
764        let (object_store, base_path) = ObjectStore::from_uri_and_params(registry, root, &params)
765            .await
766            .map_err(|e| {
767                lance_core::Error::from(NamespaceError::Internal {
768                    message: format!("Failed to create object store: {:?}", e),
769                })
770            })?;
771
772        Ok((object_store, base_path))
773    }
774}
775
776/// Directory-based implementation of Lance Namespace.
777///
778/// This implementation stores tables as Lance datasets in a directory structure.
779/// It supports local filesystems and cloud storage backends through Lance's object store.
780///
781/// ## Manifest-based Listing
782///
783/// When `manifest_enabled=true`, the namespace uses a special `__manifest` Lance table to track tables
784/// instead of scanning the filesystem. This provides:
785/// - Better performance for listing operations
786/// - Ability to track table metadata
787/// - Foundation for future features like namespaces and table renaming
788///
789/// When `dir_listing_enabled=true`, the namespace falls back to directory scanning for tables not
790/// found in the manifest, enabling gradual migration.
791///
792/// ## Credential Vending
793///
794/// When credential vendor properties are configured, `describe_table` will vend temporary
795/// credentials based on the table location URI. The vendor type is auto-selected:
796/// - `s3://` locations use AWS STS AssumeRole
797/// - `gs://` locations use GCP OAuth2 tokens
798/// - `az://` locations use Azure SAS tokens
799pub struct DirectoryNamespace {
800    root: String,
801    storage_options: Option<HashMap<String, String>>,
802    session: Option<Arc<Session>>,
803    object_store: Arc<ObjectStore>,
804    base_path: Path,
805    manifest_ns: Option<Arc<manifest::ManifestNamespace>>,
806    dir_listing_enabled: bool,
807    /// When true, root-level table operations check the manifest first before
808    /// falling back to directory listing. When false, root-level tables skip
809    /// the manifest check and use directory listing directly.
810    dir_listing_to_manifest_migration_enabled: bool,
811    /// When true, `describe_table` returns `managed_versioning: true` to indicate
812    /// commits should go through namespace table version APIs.
813    table_version_tracking_enabled: bool,
814    /// Credential vendor created once during initialization.
815    /// Used to vend temporary credentials for table access.
816    credential_vendor: Option<Arc<dyn CredentialVendor>>,
817    /// Dynamic context provider for per-request context.
818    /// Stored but not directly used in operations (available for future extensions).
819    #[allow(dead_code)]
820    context_provider: Option<Arc<dyn DynamicContextProvider>>,
821    /// When true, returns input storage options in responses when no credential vendor is configured.
822    vend_input_storage_options: bool,
823    /// Refresh interval in milliseconds for vended input storage options.
824    /// When set, expires_at_millis is added to storage options.
825    vend_input_storage_options_refresh_interval_millis: Option<u64>,
826    /// Operation metrics tracker, created when ops_metrics_enabled is true.
827    ops_metrics: Option<Arc<OpsMetrics>>,
828}
829
830impl std::fmt::Debug for DirectoryNamespace {
831    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832        write!(f, "{}", self.namespace_id())
833    }
834}
835
836impl std::fmt::Display for DirectoryNamespace {
837    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838        write!(f, "{}", self.namespace_id())
839    }
840}
841
842/// Describes the version ranges to delete for a single table.
843/// Used by `batch_delete_table_versions` and `delete_physical_version_files`.
844struct TableDeleteEntry {
845    table_id: Option<Vec<String>>,
846    ranges: Vec<(i64, i64)>,
847}
848
849impl DirectoryNamespace {
850    /// Apply pagination to a list of table names
851    ///
852    /// Sorts the list alphabetically and applies pagination using page_token (start_after) and limit.
853    ///
854    /// # Arguments
855    /// * `names` - The vector of table names to paginate
856    /// * `page_token` - Skip items until finding one greater than this value (start_after semantics)
857    /// * `limit` - Maximum number of items to keep
858    ///
859    /// # Returns
860    /// The next page token (last item in this page) if more results exist beyond the limit,
861    /// or `None` if this is the last page.
862    fn apply_pagination(
863        names: &mut Vec<String>,
864        page_token: Option<String>,
865        limit: Option<i32>,
866    ) -> Option<String> {
867        // Sort alphabetically for consistent ordering
868        names.sort();
869
870        // Apply page_token filtering (start_after semantics)
871        if let Some(start_after) = page_token {
872            if let Some(index) = names
873                .iter()
874                .position(|name| name.as_str() > start_after.as_str())
875            {
876                names.drain(0..index);
877            } else {
878                names.clear();
879            }
880        }
881
882        // Apply limit and compute next page token
883        if let Some(limit) = limit
884            && limit >= 0
885        {
886            let limit = limit as usize;
887            if names.len() > limit {
888                let next_page_token = if limit > 0 {
889                    Some(names[limit - 1].clone())
890                } else {
891                    None
892                };
893                names.truncate(limit);
894                return next_page_token;
895            }
896        }
897
898        None
899    }
900
901    /// List tables using directory scanning (fallback method)
902    async fn list_directory_tables(&self) -> Result<Vec<String>> {
903        let mut tables = Vec::new();
904        let entries = self
905            .object_store
906            .read_dir(self.base_path.clone())
907            .await
908            .map_err(|e| {
909                lance_core::Error::from(NamespaceError::Internal {
910                    message: format!("Failed to list directory: {:?}", e),
911                })
912            })?;
913
914        for entry in entries {
915            let path = entry.trim_end_matches('/');
916            if !path.ends_with(".lance") {
917                continue;
918            }
919
920            let table_name = &path[..path.len() - 6];
921
922            // Use atomic check to skip deregistered tables.
923            let status = self.check_table_status(table_name).await;
924            if status.is_deregistered {
925                continue;
926            }
927
928            tables.push(table_name.to_string());
929        }
930
931        Ok(tables)
932    }
933
934    /// Validate that the namespace ID represents the root namespace
935    fn validate_root_namespace_id(id: &Option<Vec<String>>) -> Result<()> {
936        if let Some(id) = id
937            && !id.is_empty()
938        {
939            return Err(NamespaceError::Unsupported {
940                message: format!(
941                    "Directory namespace only supports root namespace operations, but got namespace ID: {:?}. Expected empty ID.",
942                    id
943                ),
944            }
945            .into());
946        }
947        Ok(())
948    }
949
950    /// Extract table name from table ID
951    fn table_name_from_id(id: &Option<Vec<String>>) -> Result<String> {
952        let id = id.as_ref().ok_or_else(|| {
953            lance_core::Error::from(NamespaceError::InvalidInput {
954                message: "Directory namespace table ID cannot be empty".to_string(),
955            })
956        })?;
957
958        if id.len() != 1 {
959            return Err(NamespaceError::Unsupported {
960                message: format!(
961                    "Multi-level table IDs are only supported when manifest mode is enabled, but got: {:?}",
962                    id
963                ),
964            }
965            .into());
966        }
967
968        Ok(id[0].clone())
969    }
970
971    fn format_table_id(table_id: &[String]) -> String {
972        format!(
973            "table id '{}'",
974            manifest::ManifestNamespace::str_object_id(table_id)
975        )
976    }
977
978    fn format_table_id_from_request(id: &Option<Vec<String>>) -> String {
979        id.as_ref()
980            .map(|table_id| Self::format_table_id(table_id))
981            .unwrap_or_else(|| "table id '<unknown>'".to_string())
982    }
983
984    async fn resolve_table_location(&self, id: &Option<Vec<String>>) -> Result<String> {
985        let mut describe_req = DescribeTableRequest::new();
986        describe_req.id = id.clone();
987        describe_req.load_detailed_metadata = Some(false);
988
989        // Use internal impl to avoid counting this as an external API call
990        let describe_resp = self.describe_table_impl(describe_req).await?;
991
992        describe_resp.location.ok_or_else(|| {
993            lance_core::Error::from(NamespaceError::TableNotFound {
994                message: format!("Table location not found for: {:?}", id),
995            })
996        })
997    }
998
999    /// Map a Lance ref-related error returned by `Dataset::tags()` operations into
1000    /// the appropriate `NamespaceError` for tag APIs (create/get/update/delete).
1001    fn map_tag_error(err: lance_core::Error, tag: &str, table_uri: &str) -> lance_core::Error {
1002        match err {
1003            lance_core::Error::RefNotFound { .. } => NamespaceError::TableTagNotFound {
1004                message: format!("tag '{}' for table at '{}'", tag, table_uri),
1005            }
1006            .into(),
1007            lance_core::Error::RefConflict { .. } => NamespaceError::TableTagAlreadyExists {
1008                message: format!("tag '{}' for table at '{}'", tag, table_uri),
1009            }
1010            .into(),
1011            lance_core::Error::InvalidRef { message } => NamespaceError::InvalidInput {
1012                message: format!("invalid tag '{}': {}", tag, message),
1013            }
1014            .into(),
1015            lance_core::Error::VersionNotFound { message } => {
1016                NamespaceError::TableVersionNotFound {
1017                    message: format!(
1018                        "version referenced by tag '{}' not found for table at '{}': {}",
1019                        tag, table_uri, message
1020                    ),
1021                }
1022                .into()
1023            }
1024            other => NamespaceError::Internal {
1025                message: format!(
1026                    "tag operation failed for tag '{}' on table at '{}': {}",
1027                    tag, table_uri, other
1028                ),
1029            }
1030            .into(),
1031        }
1032    }
1033
1034    /// Map lance-core ref errors from branch operations to namespace errors.
1035    ///
1036    /// `RefConflict` is intentionally not handled here: create-time duplicates are rejected by
1037    /// the existence pre-check before `create_branch` runs, and delete maps its own `RefConflict`
1038    /// (branch still has dependents) inline.
1039    fn map_branch_error(
1040        err: lance_core::Error,
1041        branch: &str,
1042        table_uri: &str,
1043    ) -> lance_core::Error {
1044        match err {
1045            lance_core::Error::RefNotFound { .. } => NamespaceError::TableBranchNotFound {
1046                message: format!("branch '{}' for table at '{}'", branch, table_uri),
1047            }
1048            .into(),
1049            lance_core::Error::InvalidRef { message } => NamespaceError::InvalidInput {
1050                message: format!("invalid branch '{}': {}", branch, message),
1051            }
1052            .into(),
1053            lance_core::Error::VersionNotFound { message } => {
1054                NamespaceError::TableVersionNotFound {
1055                    message: format!(
1056                        "source version for branch '{}' not found for table at '{}': {}",
1057                        branch, table_uri, message
1058                    ),
1059                }
1060                .into()
1061            }
1062            other => NamespaceError::Internal {
1063                message: format!(
1064                    "branch operation failed for branch '{}' on table at '{}': {}",
1065                    branch, table_uri, other
1066                ),
1067            }
1068            .into(),
1069        }
1070    }
1071
1072    async fn table_has_actual_manifests(&self, table_name: &str) -> Result<bool> {
1073        manifest::ManifestNamespace::path_has_actual_manifests(
1074            &self.object_store,
1075            &self.table_path(table_name),
1076        )
1077        .await
1078    }
1079
1080    async fn filter_declared_tables(
1081        &self,
1082        tables: Vec<String>,
1083        include_declared: bool,
1084    ) -> Result<Vec<String>> {
1085        if include_declared {
1086            return Ok(tables);
1087        }
1088
1089        let mut stream = futures::stream::iter(tables.into_iter().map(|table_name| async move {
1090            // `include_declared=false` is an explicit opt-in. We still pay one `_versions/` probe
1091            // per table here so declared-state is derived from actual manifests. This is linear in
1092            // the total number of listed tables, but we probe a bounded number concurrently.
1093            if self.table_has_actual_manifests(&table_name).await? {
1094                Ok::<Option<String>, Error>(Some(table_name))
1095            } else {
1096                Ok::<Option<String>, Error>(None)
1097            }
1098        }))
1099        .buffered(manifest::DECLARED_FILTER_CONCURRENCY);
1100
1101        let mut filtered = Vec::new();
1102        while let Some(result) = stream.next().await {
1103            if let Some(table_name) = result? {
1104                filtered.push(table_name);
1105            }
1106        }
1107        Ok(filtered)
1108    }
1109
1110    fn ipc_reader_from_request_data(
1111        request_data: &Bytes,
1112        operation: &str,
1113    ) -> Result<(
1114        Box<dyn arrow::record_batch::RecordBatchReader + Send>,
1115        usize,
1116    )> {
1117        if request_data.is_empty() {
1118            return Err(NamespaceError::InvalidInput {
1119                message: format!(
1120                    "Request data (Arrow IPC stream) is required for {}",
1121                    operation
1122                ),
1123            }
1124            .into());
1125        }
1126
1127        let cursor = Cursor::new(request_data.as_ref());
1128        let stream_reader =
1129            StreamReader::try_new(cursor, None).map_err(|e| NamespaceError::InvalidInput {
1130                message: format!("Invalid Arrow IPC stream: {}", e),
1131            })?;
1132        let arrow_schema = stream_reader.schema();
1133
1134        let mut num_rows = 0usize;
1135        let mut batches = Vec::new();
1136        for batch_result in stream_reader {
1137            let batch = batch_result.map_err(|e| NamespaceError::Internal {
1138                message: format!("Failed to read batch from IPC stream: {}", e),
1139            })?;
1140            num_rows += batch.num_rows();
1141            batches.push(batch);
1142        }
1143
1144        let reader: Box<dyn arrow::record_batch::RecordBatchReader + Send> = if batches.is_empty() {
1145            let batch = arrow::record_batch::RecordBatch::new_empty(arrow_schema.clone());
1146            Box::new(RecordBatchIterator::new(vec![Ok(batch)], arrow_schema))
1147        } else {
1148            let batch_results: Vec<_> = batches.into_iter().map(Ok).collect();
1149            Box::new(RecordBatchIterator::new(batch_results, arrow_schema))
1150        };
1151
1152        Ok((reader, num_rows))
1153    }
1154
1155    async fn table_uri_has_actual_manifests(&self, table_uri: &str) -> Result<bool> {
1156        let table_path = self.object_store_path_from_uri(table_uri)?;
1157        manifest::ManifestNamespace::path_has_actual_manifests(&self.object_store, &table_path)
1158            .await
1159    }
1160
1161    fn object_store_path_from_uri(&self, uri: &str) -> Result<Path> {
1162        let registry = self
1163            .session
1164            .as_ref()
1165            .map(|session| session.store_registry())
1166            .unwrap_or_else(|| Arc::new(ObjectStoreRegistry::default()));
1167        ObjectStore::extract_path_from_uri(registry, uri)
1168    }
1169
1170    /// Normalize and validate a branch selector: `None`, empty, and `main` mean
1171    /// the main branch; any other name is validated with lance's
1172    /// `check_valid_branch` (lance skips this on the open path) so it cannot
1173    /// escape the table root via `..`.
1174    fn normalized_branch(branch: Option<&str>) -> Result<Option<&str>> {
1175        match branch.filter(|b| !b.is_empty() && *b != "main") {
1176            Some(branch) => {
1177                check_valid_branch(branch).map_err(|e| {
1178                    lance_core::Error::from(NamespaceError::InvalidInput {
1179                        message: format!("invalid branch name '{}': {}", branch, e),
1180                    })
1181                })?;
1182                Ok(Some(branch))
1183            }
1184            None => Ok(None),
1185        }
1186    }
1187
1188    async fn open_validated_branch(&self, table_uri: &str, branch: &str) -> Result<Dataset> {
1189        let dataset = self
1190            .configured_builder(table_uri)
1191            .with_branch(branch, None)
1192            .load()
1193            .await
1194            .map_err(|e| {
1195                lance_core::Error::from(NamespaceError::TableNotFound {
1196                    message: format!(
1197                        "branch '{}' not found for table at '{}': {}",
1198                        branch, table_uri, e
1199                    ),
1200                })
1201            })?;
1202        dataset.branches().get(branch).await.map_err(|_| {
1203            lance_core::Error::from(NamespaceError::TableNotFound {
1204                message: format!("branch '{}' not found for table at '{}'", branch, table_uri),
1205            })
1206        })?;
1207        Ok(dataset)
1208    }
1209
1210    async fn resolve_branch_location(&self, table_uri: &str, branch: &str) -> Result<String> {
1211        Ok(self
1212            .open_validated_branch(table_uri, branch)
1213            .await?
1214            .branch_location()
1215            .uri)
1216    }
1217
1218    fn validate_dir_only_properties(
1219        properties: Option<&HashMap<String, String>>,
1220        operation: &str,
1221    ) -> Result<()> {
1222        // Dir-only mode has no metadata catalog, so non-empty table properties would be accepted
1223        // and then lost. Reject them instead. Request-level storage options are different: they
1224        // directly affect the current write and remain supported in dir-only mode.
1225        if properties.is_some_and(|properties| !properties.is_empty()) {
1226            return Err(NamespaceError::Unsupported {
1227                message: format!(
1228                    "{} with non-empty table properties requires manifest_enabled=true",
1229                    operation
1230                ),
1231            }
1232            .into());
1233        }
1234        Ok(())
1235    }
1236
1237    async fn write_reader_to_table(
1238        &self,
1239        table_uri: &str,
1240        reader: Box<dyn arrow::record_batch::RecordBatchReader + Send>,
1241        mode: WriteMode,
1242        extra_storage_options: Option<HashMap<String, String>>,
1243    ) -> Result<Dataset> {
1244        // Insert and merge-insert request models do not carry request-level storage options,
1245        // so these writes intentionally use the namespace-level storage options only.
1246        let mut merged_storage_options = self.storage_options.clone().unwrap_or_default();
1247        if let Some(extra_storage_options) = extra_storage_options {
1248            merged_storage_options.extend(extra_storage_options);
1249        }
1250        let store_params = (!merged_storage_options.is_empty()).then(|| ObjectStoreParams {
1251            storage_options_accessor: Some(Arc::new(
1252                lance_io::object_store::StorageOptionsAccessor::with_static_options(
1253                    merged_storage_options,
1254                ),
1255            )),
1256            ..Default::default()
1257        });
1258
1259        let write_params = WriteParams {
1260            mode,
1261            store_params,
1262            session: self.session.clone(),
1263            ..Default::default()
1264        };
1265
1266        let dataset = Dataset::write(reader, table_uri, Some(write_params))
1267            .await
1268            .map_err(|e| NamespaceError::Internal {
1269                message: format!("Failed to write table at '{}': {}", table_uri, e),
1270            })?;
1271
1272        Ok(dataset)
1273    }
1274
1275    /// Logical table version parsed from a manifest filename, or `None` for
1276    /// non-manifest / detached entries. Delegates to lance's scheme detection so
1277    /// version listing and deletion stay consistent with the on-disk format.
1278    fn manifest_version_from_filename(filename: &str) -> Option<u64> {
1279        ManifestNamingScheme::detect_scheme(filename)?.parse_version(filename)
1280    }
1281
1282    async fn list_table_versions_from_storage(
1283        &self,
1284        table_uri: &str,
1285        descending: bool,
1286        limit: Option<i32>,
1287    ) -> Result<Vec<TableVersion>> {
1288        let table_path = self.object_store_path_from_uri(table_uri)?;
1289        let versions_dir = table_path.clone().join(VERSIONS_DIR);
1290        let manifest_metas: Vec<_> = self
1291            .object_store
1292            .read_dir_all(&versions_dir, None)
1293            .try_collect()
1294            .await
1295            .map_err(|e| {
1296                lance_core::Error::from(NamespaceError::Internal {
1297                    message: format!(
1298                        "Failed to list manifest files for table at '{}': {}",
1299                        table_uri, e
1300                    ),
1301                })
1302            })?;
1303
1304        let is_v2_naming = manifest_metas
1305            .first()
1306            .is_some_and(|meta| meta.location.filename().is_some_and(|f| f.len() == 29));
1307
1308        let mut table_versions: Vec<TableVersion> = manifest_metas
1309            .into_iter()
1310            .filter_map(|meta| {
1311                let filename = meta.location.filename()?;
1312                let actual_version = Self::manifest_version_from_filename(filename)?;
1313
1314                Some(TableVersion {
1315                    version: actual_version as i64,
1316                    manifest_path: meta.location.to_string(),
1317                    manifest_size: Some(meta.size as i64),
1318                    e_tag: meta.e_tag,
1319                    timestamp_millis: Some(meta.last_modified.timestamp_millis()),
1320                    metadata: None,
1321                })
1322            })
1323            .collect();
1324
1325        let list_is_ordered = self.object_store.list_is_lexically_ordered;
1326
1327        let needs_sort = if list_is_ordered {
1328            if is_v2_naming {
1329                !descending
1330            } else {
1331                descending
1332            }
1333        } else {
1334            true
1335        };
1336
1337        if needs_sort {
1338            if descending {
1339                table_versions.sort_by(|a, b| b.version.cmp(&a.version));
1340            } else {
1341                table_versions.sort_by(|a, b| a.version.cmp(&b.version));
1342            }
1343        }
1344
1345        if let Some(limit) = limit {
1346            table_versions.truncate(limit as usize);
1347        }
1348
1349        Ok(table_versions)
1350    }
1351
1352    /// Internal describe_table implementation that doesn't record metrics.
1353    /// Used by both the public describe_table (which records metrics) and
1354    /// internal callers like resolve_table_location (which shouldn't).
1355    async fn describe_table_impl(
1356        &self,
1357        request: DescribeTableRequest,
1358    ) -> Result<DescribeTableResponse> {
1359        let is_root_level = request.id.as_ref().is_some_and(|id| id.len() == 1);
1360        let skip_manifest_for_root = self.dir_listing_enabled
1361            && is_root_level
1362            && !self.dir_listing_to_manifest_migration_enabled;
1363        if let Some(ref manifest_ns) = self.manifest_ns
1364            && !skip_manifest_for_root
1365        {
1366            match manifest_ns.describe_table(request.clone()).await {
1367                Ok(mut response) => {
1368                    if let Some(ref table_uri) = response.table_uri {
1369                        // For backwards compatibility, only skip vending credentials when explicitly set to false
1370                        let vend = request.vend_credentials.unwrap_or(true);
1371                        let identity = request.identity.as_deref();
1372                        response.storage_options = self
1373                            .get_storage_options_for_table(table_uri, vend, identity)
1374                            .await?;
1375                    }
1376                    // Set managed_versioning flag when table_version_tracking_enabled
1377                    if self.table_version_tracking_enabled {
1378                        response.managed_versioning = Some(true);
1379                    }
1380                    return Ok(response);
1381                }
1382                Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
1383                    // An incompatible manifest must surface "please upgrade"
1384                    // rather than degrading to a directory-listing view.
1385                    return Err(e);
1386                }
1387                Err(_) if self.dir_listing_enabled && is_root_level => {
1388                    // Fall through to directory check only for single-level IDs
1389                }
1390                Err(e) => return Err(e),
1391            }
1392        }
1393
1394        let table_name = Self::table_name_from_id(&request.id)?;
1395        let table_id = Self::format_table_id_from_request(&request.id);
1396        let table_uri = self.table_full_uri(&table_name);
1397
1398        // Atomically check table existence and deregistration status
1399        let status = self.check_table_status(&table_name).await;
1400
1401        if !status.exists {
1402            return Err(NamespaceError::TableNotFound {
1403                message: table_id.clone(),
1404            }
1405            .into());
1406        }
1407
1408        if status.is_deregistered {
1409            return Err(NamespaceError::TableNotFound {
1410                message: format!("Table is deregistered: {}", table_id),
1411            }
1412            .into());
1413        }
1414
1415        let load_detailed_metadata = request.load_detailed_metadata.unwrap_or(false);
1416        let should_check_declared =
1417            load_detailed_metadata || request.check_declared.unwrap_or(false);
1418        // For backwards compatibility, only skip vending credentials when explicitly set to false
1419        let vend_credentials = request.vend_credentials.unwrap_or(true);
1420        let identity = request.identity.as_deref();
1421        let is_only_declared = if should_check_declared {
1422            if status.has_reserved_file {
1423                Some(!self.table_has_actual_manifests(&table_name).await?)
1424            } else {
1425                Some(false)
1426            }
1427        } else {
1428            None
1429        };
1430
1431        if !load_detailed_metadata {
1432            let storage_options = self
1433                .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1434                .await?;
1435            return Ok(DescribeTableResponse {
1436                table: Some(table_name),
1437                namespace: request.id.as_ref().map(|id| {
1438                    if id.len() > 1 {
1439                        id[..id.len() - 1].to_vec()
1440                    } else {
1441                        vec![]
1442                    }
1443                }),
1444                location: Some(table_uri.clone()),
1445                table_uri: Some(table_uri),
1446                storage_options,
1447                is_only_declared,
1448                managed_versioning: if self.table_version_tracking_enabled {
1449                    Some(true)
1450                } else {
1451                    None
1452                },
1453                ..Default::default()
1454            });
1455        }
1456
1457        if is_only_declared == Some(true) {
1458            let storage_options = self
1459                .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1460                .await?;
1461            return Ok(DescribeTableResponse {
1462                table: Some(table_name),
1463                namespace: request.id.as_ref().map(|id| {
1464                    if id.len() > 1 {
1465                        id[..id.len() - 1].to_vec()
1466                    } else {
1467                        vec![]
1468                    }
1469                }),
1470                location: Some(table_uri.clone()),
1471                table_uri: Some(table_uri),
1472                storage_options,
1473                is_only_declared,
1474                managed_versioning: if self.table_version_tracking_enabled {
1475                    Some(true)
1476                } else {
1477                    None
1478                },
1479                ..Default::default()
1480            });
1481        }
1482
1483        // Try to load the dataset to get real information
1484        // Use DatasetBuilder with storage options to support S3 with custom endpoints
1485        let mut builder = DatasetBuilder::from_uri(&table_uri);
1486        if let Some(opts) = &self.storage_options {
1487            builder = builder.with_storage_options(opts.clone());
1488        }
1489        if let Some(sess) = &self.session {
1490            builder = builder.with_session(sess.clone());
1491        }
1492        match builder.load().await {
1493            Ok(mut dataset) => {
1494                // If a specific version is requested, checkout that version
1495                if let Some(requested_version) = request.version {
1496                    dataset = dataset
1497                        .checkout_version(requested_version as u64)
1498                        .await
1499                        .map_err(|e| {
1500                            lance_core::Error::from(NamespaceError::TableVersionNotFound {
1501                                message: format!(
1502                                    "Version {} not found for table '{}': {}",
1503                                    requested_version, table_name, e
1504                                ),
1505                            })
1506                        })?;
1507                }
1508
1509                let version_info = dataset.version();
1510                let lance_schema = dataset.schema();
1511                let arrow_schema: arrow_schema::Schema = lance_schema.into();
1512                let json_schema = arrow_schema_to_json(&arrow_schema)?;
1513                let storage_options = self
1514                    .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1515                    .await?;
1516
1517                // Convert BTreeMap to HashMap for the response
1518                let metadata: std::collections::HashMap<String, String> =
1519                    version_info.metadata.into_iter().collect();
1520
1521                Ok(DescribeTableResponse {
1522                    table: Some(table_name),
1523                    namespace: request.id.as_ref().map(|id| {
1524                        if id.len() > 1 {
1525                            id[..id.len() - 1].to_vec()
1526                        } else {
1527                            vec![]
1528                        }
1529                    }),
1530                    version: Some(version_info.version as i64),
1531                    location: Some(table_uri.clone()),
1532                    table_uri: Some(table_uri),
1533                    schema: Some(Box::new(json_schema)),
1534                    storage_options,
1535                    metadata: Some(metadata),
1536                    is_only_declared,
1537                    managed_versioning: if self.table_version_tracking_enabled {
1538                        Some(true)
1539                    } else {
1540                        None
1541                    },
1542                    ..Default::default()
1543                })
1544            }
1545            Err(err) => {
1546                if manifest::ManifestNamespace::is_not_found_load_error(&err)
1547                    && is_only_declared == Some(true)
1548                {
1549                    let storage_options = self
1550                        .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1551                        .await?;
1552                    Ok(DescribeTableResponse {
1553                        table: Some(table_name),
1554                        namespace: request.id.as_ref().map(|id| {
1555                            if id.len() > 1 {
1556                                id[..id.len() - 1].to_vec()
1557                            } else {
1558                                vec![]
1559                            }
1560                        }),
1561                        location: Some(table_uri.clone()),
1562                        table_uri: Some(table_uri),
1563                        storage_options,
1564                        is_only_declared,
1565                        managed_versioning: if self.table_version_tracking_enabled {
1566                            Some(true)
1567                        } else {
1568                            None
1569                        },
1570                        ..Default::default()
1571                    })
1572                } else {
1573                    Err(NamespaceError::Internal {
1574                        message: format!(
1575                            "Table directory exists but cannot load dataset {}: {:?}",
1576                            table_name, err
1577                        ),
1578                    }
1579                    .into())
1580                }
1581            }
1582        }
1583    }
1584
1585    /// Build a `DatasetBuilder` for `table_uri` with this namespace's storage
1586    /// options and session applied. Callers add version/branch scoping.
1587    fn configured_builder(&self, table_uri: &str) -> DatasetBuilder {
1588        let mut builder = DatasetBuilder::from_uri(table_uri);
1589        if let Some(opts) = &self.storage_options {
1590            builder = builder.with_storage_options(opts.clone());
1591        }
1592        if let Some(sess) = &self.session {
1593            builder = builder.with_session(sess.clone());
1594        }
1595        builder
1596    }
1597
1598    async fn load_dataset(
1599        &self,
1600        table_uri: &str,
1601        version: Option<i64>,
1602        operation: &str,
1603    ) -> Result<Dataset> {
1604        if let Some(version) = version
1605            && version < 0
1606        {
1607            return Err(NamespaceError::InvalidInput {
1608                message: format!(
1609                    "Table version for {} must be non-negative, got {}",
1610                    operation, version
1611                ),
1612            }
1613            .into());
1614        }
1615
1616        let builder = self.configured_builder(table_uri);
1617
1618        let dataset = builder.load().await.map_err(|e| {
1619            lance_core::Error::from(NamespaceError::TableNotFound {
1620                message: format!(
1621                    "Failed to open table at '{}' for {}: {}",
1622                    table_uri, operation, e
1623                ),
1624            })
1625        })?;
1626
1627        if let Some(version) = version {
1628            return dataset.checkout_version(version as u64).await.map_err(|e| {
1629                lance_core::Error::from(NamespaceError::TableVersionNotFound {
1630                    message: format!(
1631                        "Failed to checkout version {} for table at '{}' during {}: {}",
1632                        version, table_uri, operation, e
1633                    ),
1634                })
1635            });
1636        }
1637
1638        Ok(dataset)
1639    }
1640
1641    fn parse_index_type(index_type: &str) -> Result<IndexType> {
1642        match index_type.trim().to_ascii_uppercase().as_str() {
1643            "SCALAR" | "BTREE" => Ok(IndexType::BTree),
1644            "BITMAP" => Ok(IndexType::Bitmap),
1645            "LABEL_LIST" | "LABELLIST" => Ok(IndexType::LabelList),
1646            "INVERTED" | "FTS" => Ok(IndexType::Inverted),
1647            "NGRAM" => Ok(IndexType::NGram),
1648            "ZONEMAP" | "ZONE_MAP" => Ok(IndexType::ZoneMap),
1649            "BLOOMFILTER" | "BLOOM_FILTER" => Ok(IndexType::BloomFilter),
1650            "RTREE" | "R_TREE" => Ok(IndexType::RTree),
1651            "VECTOR" | "IVF_PQ" => Ok(IndexType::IvfPq),
1652            "IVF_FLAT" => Ok(IndexType::IvfFlat),
1653            "IVF_SQ" => Ok(IndexType::IvfSq),
1654            "IVF_RQ" => Ok(IndexType::IvfRq),
1655            "IVF_HNSW_FLAT" => Ok(IndexType::IvfHnswFlat),
1656            "IVF_HNSW_SQ" => Ok(IndexType::IvfHnswSq),
1657            "IVF_HNSW_PQ" => Ok(IndexType::IvfHnswPq),
1658            other => Err(NamespaceError::InvalidInput {
1659                message: format!("Unsupported index_type '{}'", other),
1660            }
1661            .into()),
1662        }
1663    }
1664
1665    fn parse_metric_type(distance_type: Option<&str>) -> Result<MetricType> {
1666        let distance_type = distance_type.unwrap_or("l2");
1667        MetricType::try_from(distance_type).map_err(|e| {
1668            lance_core::Error::from(NamespaceError::InvalidInput {
1669                message: format!(
1670                    "Unsupported distance_type '{}' for vector index: {}",
1671                    distance_type, e
1672                ),
1673            })
1674        })
1675    }
1676
1677    fn build_index_params(request: &CreateTableIndexRequest) -> Result<DirectoryIndexParams> {
1678        let index_type = Self::parse_index_type(&request.index_type)?;
1679        Ok(match index_type {
1680            IndexType::BTree => DirectoryIndexParams::Scalar {
1681                index_type,
1682                params: ScalarIndexParams::for_builtin(BuiltinIndexType::BTree),
1683            },
1684            IndexType::Bitmap => DirectoryIndexParams::Scalar {
1685                index_type,
1686                params: ScalarIndexParams::for_builtin(BuiltinIndexType::Bitmap),
1687            },
1688            IndexType::LabelList => DirectoryIndexParams::Scalar {
1689                index_type,
1690                params: ScalarIndexParams::for_builtin(BuiltinIndexType::LabelList),
1691            },
1692            IndexType::NGram => DirectoryIndexParams::Scalar {
1693                index_type,
1694                params: ScalarIndexParams::for_builtin(BuiltinIndexType::NGram),
1695            },
1696            IndexType::ZoneMap => DirectoryIndexParams::Scalar {
1697                index_type,
1698                params: ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap),
1699            },
1700            IndexType::BloomFilter => DirectoryIndexParams::Scalar {
1701                index_type,
1702                params: ScalarIndexParams::for_builtin(BuiltinIndexType::BloomFilter),
1703            },
1704            IndexType::RTree => DirectoryIndexParams::Scalar {
1705                index_type,
1706                params: ScalarIndexParams::for_builtin(BuiltinIndexType::RTree),
1707            },
1708            IndexType::Inverted => {
1709                let mut params = InvertedIndexParams::default();
1710                if let Some(with_position) = request.with_position {
1711                    params = params.with_position(with_position);
1712                }
1713                if let Some(base_tokenizer) = &request.base_tokenizer {
1714                    params = params.base_tokenizer(base_tokenizer.clone());
1715                }
1716                if let Some(language) = &request.language {
1717                    params = params.language(language)?;
1718                }
1719                if let Some(max_token_length) = request.max_token_length {
1720                    if max_token_length < 0 {
1721                        return Err(NamespaceError::InvalidInput {
1722                            message: format!(
1723                                "FTS max_token_length must be non-negative, got {}",
1724                                max_token_length
1725                            ),
1726                        }
1727                        .into());
1728                    }
1729                    params = params.max_token_length(Some(max_token_length as usize));
1730                }
1731                if let Some(lower_case) = request.lower_case {
1732                    params = params.lower_case(lower_case);
1733                }
1734                if let Some(stem) = request.stem {
1735                    params = params.stem(stem);
1736                }
1737                if let Some(remove_stop_words) = request.remove_stop_words {
1738                    params = params.remove_stop_words(remove_stop_words);
1739                }
1740                if let Some(ascii_folding) = request.ascii_folding {
1741                    params = params.ascii_folding(ascii_folding);
1742                }
1743                DirectoryIndexParams::Inverted(params)
1744            }
1745            IndexType::IvfFlat => DirectoryIndexParams::Vector {
1746                index_type,
1747                params: VectorIndexParams::with_ivf_flat_params(
1748                    Self::parse_metric_type(request.distance_type.as_deref())?,
1749                    IvfBuildParams::default(),
1750                ),
1751            },
1752            IndexType::IvfPq => DirectoryIndexParams::Vector {
1753                index_type,
1754                params: VectorIndexParams::with_ivf_pq_params(
1755                    Self::parse_metric_type(request.distance_type.as_deref())?,
1756                    IvfBuildParams::default(),
1757                    PQBuildParams::default(),
1758                ),
1759            },
1760            IndexType::IvfSq => DirectoryIndexParams::Vector {
1761                index_type,
1762                params: VectorIndexParams::with_ivf_sq_params(
1763                    Self::parse_metric_type(request.distance_type.as_deref())?,
1764                    IvfBuildParams::default(),
1765                    SQBuildParams::default(),
1766                ),
1767            },
1768            IndexType::IvfRq => DirectoryIndexParams::Vector {
1769                index_type,
1770                params: VectorIndexParams::with_ivf_rq_params(
1771                    Self::parse_metric_type(request.distance_type.as_deref())?,
1772                    IvfBuildParams::default(),
1773                    RQBuildParams::default(),
1774                ),
1775            },
1776            IndexType::IvfHnswFlat => DirectoryIndexParams::Vector {
1777                index_type,
1778                params: VectorIndexParams::ivf_hnsw(
1779                    Self::parse_metric_type(request.distance_type.as_deref())?,
1780                    IvfBuildParams::default(),
1781                    HnswBuildParams::default(),
1782                ),
1783            },
1784            IndexType::IvfHnswSq => DirectoryIndexParams::Vector {
1785                index_type,
1786                params: VectorIndexParams::with_ivf_hnsw_sq_params(
1787                    Self::parse_metric_type(request.distance_type.as_deref())?,
1788                    IvfBuildParams::default(),
1789                    HnswBuildParams::default(),
1790                    SQBuildParams::default(),
1791                ),
1792            },
1793            IndexType::IvfHnswPq => DirectoryIndexParams::Vector {
1794                index_type,
1795                params: VectorIndexParams::with_ivf_hnsw_pq_params(
1796                    Self::parse_metric_type(request.distance_type.as_deref())?,
1797                    IvfBuildParams::default(),
1798                    HnswBuildParams::default(),
1799                    PQBuildParams::default(),
1800                ),
1801            },
1802            other => {
1803                return Err(NamespaceError::InvalidInput {
1804                    message: format!("Unsupported index type for namespace API: {}", other),
1805                }
1806                .into());
1807            }
1808        })
1809    }
1810
1811    fn paginate_indices(
1812        indices: &mut Vec<IndexContent>,
1813        page_token: Option<String>,
1814        limit: Option<i32>,
1815    ) -> Option<String> {
1816        indices.sort_by(|a, b| a.index_name.cmp(&b.index_name));
1817
1818        if let Some(start_after) = page_token {
1819            if let Some(index) = indices
1820                .iter()
1821                .position(|index| index.index_name.as_str() > start_after.as_str())
1822            {
1823                indices.drain(0..index);
1824            } else {
1825                indices.clear();
1826            }
1827        }
1828
1829        let mut next_page_token = None;
1830        if let Some(limit) = limit
1831            && limit >= 0
1832        {
1833            let limit = limit as usize;
1834            if limit > 0 && indices.len() > limit {
1835                next_page_token = Some(indices[limit - 1].index_name.clone());
1836            }
1837            indices.truncate(limit);
1838        }
1839        if indices.is_empty() {
1840            None
1841        } else {
1842            next_page_token
1843        }
1844    }
1845
1846    fn transaction_operation_name(transaction: &Transaction) -> String {
1847        match &transaction.operation {
1848            Operation::CreateIndex {
1849                new_indices,
1850                removed_indices,
1851            } if new_indices.is_empty() && !removed_indices.is_empty() => "DropIndex".to_string(),
1852            _ => transaction.operation.to_string(),
1853        }
1854    }
1855
1856    fn transaction_response(
1857        version: u64,
1858        transaction: &Transaction,
1859    ) -> DescribeTransactionResponse {
1860        let mut properties = transaction
1861            .transaction_properties
1862            .as_ref()
1863            .map(|properties| (**properties).clone())
1864            .unwrap_or_default();
1865        properties.insert("uuid".to_string(), transaction.uuid.clone());
1866        properties.insert("version".to_string(), version.to_string());
1867        properties.insert(
1868            "read_version".to_string(),
1869            transaction.read_version.to_string(),
1870        );
1871        properties.insert(
1872            "operation".to_string(),
1873            Self::transaction_operation_name(transaction),
1874        );
1875        if let Some(tag) = &transaction.tag {
1876            properties.insert("tag".to_string(), tag.clone());
1877        }
1878
1879        DescribeTransactionResponse {
1880            status: "SUCCEEDED".to_string(),
1881            properties: Some(properties),
1882        }
1883    }
1884
1885    fn describe_table_index_stats_response(
1886        stats: &serde_json::Value,
1887    ) -> DescribeTableIndexStatsResponse {
1888        let get_i64 = |key: &str| {
1889            stats.get(key).and_then(|value| {
1890                value
1891                    .as_i64()
1892                    .or_else(|| value.as_u64().and_then(|v| i64::try_from(v).ok()))
1893            })
1894        };
1895
1896        DescribeTableIndexStatsResponse {
1897            distance_type: stats
1898                .get("distance_type")
1899                .and_then(|value| value.as_str())
1900                .map(str::to_string),
1901            index_type: stats
1902                .get("index_type")
1903                .and_then(|value| value.as_str())
1904                .map(str::to_string),
1905            num_indexed_rows: get_i64("num_indexed_rows"),
1906            num_unindexed_rows: get_i64("num_unindexed_rows"),
1907            num_indices: get_i64("num_indices").and_then(|value| i32::try_from(value).ok()),
1908        }
1909    }
1910
1911    /// When transaction_id is not parseable as a version number (i.e. it's a UUID),
1912    /// find_transaction iterates through every version in reverse, reading each
1913    /// transaction file from storage. For tables with many versions this will
1914    /// be extremely slow — each iteration is a separate I/O call.
1915    async fn find_transaction(&self, dataset: &Dataset, id: &str) -> Result<(u64, Transaction)> {
1916        if let Ok(version) = id.parse::<u64>() {
1917            let transaction = dataset
1918                .read_transaction_by_version(version)
1919                .await
1920                .map_err(|e| {
1921                    lance_core::Error::from(NamespaceError::TransactionNotFound {
1922                        message: format!(
1923                            "Failed to read transaction for version {}: {}",
1924                            version, e
1925                        ),
1926                    })
1927                })?
1928                .ok_or_else(|| {
1929                    lance_core::Error::from(NamespaceError::TransactionNotFound {
1930                        message: format!("version {}", version),
1931                    })
1932                })?;
1933            return Ok((version, transaction));
1934        }
1935
1936        let versions = dataset.versions().await.map_err(|e| {
1937            lance_core::Error::from(NamespaceError::Internal {
1938                message: format!(
1939                    "Failed to list table versions while resolving transaction '{}': {}",
1940                    id, e
1941                ),
1942            })
1943        })?;
1944
1945        for version in versions.into_iter().rev() {
1946            if let Some(transaction) = dataset
1947                .read_transaction_by_version(version.version)
1948                .await
1949                .map_err(|e| {
1950                    lance_core::Error::from(NamespaceError::Internal {
1951                        message: format!(
1952                            "Failed to read transaction for version {} while resolving '{}': {}",
1953                            version.version, id, e
1954                        ),
1955                    })
1956                })?
1957                && transaction.uuid == id
1958            {
1959                return Ok((version.version, transaction));
1960            }
1961        }
1962
1963        Err(NamespaceError::TransactionNotFound {
1964            message: id.to_string(),
1965        }
1966        .into())
1967    }
1968
1969    fn table_full_uri(&self, table_name: &str) -> String {
1970        format!("{}/{}.lance", &self.root, table_name)
1971    }
1972
1973    /// Get the object store path for a table (relative to base_path)
1974    fn table_path(&self, table_name: &str) -> Path {
1975        self.base_path
1976            .clone()
1977            .join(format!("{}.lance", table_name).as_str())
1978    }
1979
1980    /// Get the reserved file path for a table
1981    fn table_reserved_file_path(&self, table_name: &str) -> Path {
1982        self.base_path
1983            .clone()
1984            .join(format!("{}.lance", table_name).as_str())
1985            .join(".lance-reserved")
1986    }
1987
1988    /// Get the deregistered marker file path for a table
1989    fn table_deregistered_file_path(&self, table_name: &str) -> Path {
1990        self.base_path
1991            .clone()
1992            .join(format!("{}.lance", table_name).as_str())
1993            .join(".lance-deregistered")
1994    }
1995
1996    /// Atomically check table existence and deregistration status.
1997    ///
1998    /// This performs a single directory listing to get a consistent snapshot of the
1999    /// table's state, avoiding race conditions between checking existence and
2000    /// checking deregistration status.
2001    pub(crate) async fn check_table_status(&self, table_name: &str) -> TableStatus {
2002        let table_path = self.table_path(table_name);
2003        match self.object_store.read_dir(table_path).await {
2004            Ok(entries) => {
2005                let exists = !entries.is_empty();
2006                let is_deregistered = entries.iter().any(|e| e.ends_with(".lance-deregistered"));
2007                let has_reserved_file = entries.iter().any(|e| e.ends_with(".lance-reserved"));
2008                TableStatus {
2009                    exists,
2010                    is_deregistered,
2011                    has_reserved_file,
2012                }
2013            }
2014            Err(_) => TableStatus {
2015                exists: false,
2016                is_deregistered: false,
2017                has_reserved_file: false,
2018            },
2019        }
2020    }
2021
2022    async fn put_marker_file_atomic(
2023        &self,
2024        path: &Path,
2025        file_description: &str,
2026    ) -> std::result::Result<(), String> {
2027        let put_opts = PutOptions {
2028            mode: PutMode::Create,
2029            ..Default::default()
2030        };
2031
2032        match self
2033            .object_store
2034            .inner
2035            .put_opts(path, bytes::Bytes::new().into(), put_opts)
2036            .await
2037        {
2038            Ok(_) => Ok(()),
2039            Err(ObjectStoreError::AlreadyExists { .. })
2040            | Err(ObjectStoreError::Precondition { .. }) => {
2041                Err(format!("{} already exists", file_description))
2042            }
2043            Err(e) => Err(format!("Failed to create {}: {:?}", file_description, e)),
2044        }
2045    }
2046
2047    /// Get storage options for a table, using credential vending if configured.
2048    ///
2049    /// If credential vendor properties are configured and the table location matches
2050    /// a supported cloud provider, this will create an appropriate vendor and vend
2051    /// temporary credentials scoped to the table location. Otherwise, returns the
2052    /// static storage options.
2053    ///
2054    /// The vendor type is auto-selected based on the table URI:
2055    /// - `s3://` locations use AWS STS AssumeRole
2056    /// - `gs://` locations use GCP OAuth2 tokens
2057    /// - `az://` locations use Azure SAS tokens
2058    ///
2059    /// The permission level (Read, Write, Admin) is configured at namespace
2060    /// initialization time via the `credential_vendor_permission` property.
2061    ///
2062    /// # Arguments
2063    ///
2064    /// * `table_uri` - The full URI of the table
2065    /// * `identity` - Optional identity from the request for identity-based credential vending
2066    async fn get_storage_options_for_table(
2067        &self,
2068        table_uri: &str,
2069        vend_credentials: bool,
2070        identity: Option<&Identity>,
2071    ) -> Result<Option<HashMap<String, String>>> {
2072        if vend_credentials && let Some(ref vendor) = self.credential_vendor {
2073            let vended = vendor.vend_credentials(table_uri, identity).await?;
2074            return Ok(Some(vended.storage_options));
2075        }
2076        // When vend_input_storage_options is enabled and no credential vendor is configured,
2077        // return the input storage options. This is useful for testing.
2078        if self.vend_input_storage_options {
2079            let mut options = self.storage_options.clone().unwrap_or_default();
2080            // Add expires_at_millis if refresh interval is configured
2081            if let Some(refresh_interval_millis) =
2082                self.vend_input_storage_options_refresh_interval_millis
2083            {
2084                let now_millis = std::time::SystemTime::now()
2085                    .duration_since(std::time::UNIX_EPOCH)
2086                    .unwrap()
2087                    .as_millis() as u64;
2088                let expires_at_millis = now_millis + refresh_interval_millis;
2089                options.insert(
2090                    "expires_at_millis".to_string(),
2091                    expires_at_millis.to_string(),
2092                );
2093            }
2094            return Ok(Some(options));
2095        }
2096        // When no credential vendor is configured, return None to avoid
2097        // leaking the namespace's own static credentials to clients.
2098        Ok(None)
2099    }
2100
2101    /// Migrate directory-based tables to the manifest.
2102    ///
2103    /// This is a one-time migration operation that:
2104    /// 1. Scans the directory for existing `.lance` tables
2105    /// 2. Registers any unmigrated tables in the manifest
2106    /// 3. Returns the count of tables that were migrated
2107    ///
2108    /// This method is safe to run multiple times - it will skip tables that are already
2109    /// registered in the manifest.
2110    ///
2111    /// # Usage
2112    ///
2113    /// After creating tables in directory-only mode or dual mode, you can migrate them
2114    /// to the manifest to enable manifest-only mode:
2115    ///
2116    /// ```no_run
2117    /// #![recursion_limit = "256"]
2118    /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
2119    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2120    /// // Create namespace with dual mode (manifest + directory listing)
2121    /// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
2122    ///     .manifest_enabled(true)
2123    ///     .dir_listing_enabled(true)
2124    ///     .build()
2125    ///     .await?;
2126    ///
2127    /// // ... tables are created and used ...
2128    ///
2129    /// // Migrate existing directory tables to manifest
2130    /// let migrated_count = namespace.migrate().await?;
2131    /// println!("Migrated {} tables", migrated_count);
2132    ///
2133    /// // Now you can disable directory listing for better performance:
2134    /// // (requires rebuilding the namespace)
2135    /// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
2136    ///     .manifest_enabled(true)
2137    ///     .dir_listing_enabled(false)  // All tables now in manifest
2138    ///     .build()
2139    ///     .await?;
2140    /// # Ok(())
2141    /// # }
2142    /// ```
2143    ///
2144    /// # Returns
2145    ///
2146    /// Returns the number of tables that were migrated to the manifest.
2147    ///
2148    /// # Errors
2149    ///
2150    /// Returns an error if:
2151    /// - Manifest is not enabled
2152    /// - Directory listing fails
2153    /// - Manifest registration fails
2154    pub async fn migrate(&self) -> Result<usize> {
2155        // We only care about tables in the root namespace
2156        let Some(ref manifest_ns) = self.manifest_ns else {
2157            return Ok(0); // No manifest, nothing to migrate
2158        };
2159
2160        // Get all table locations already in the manifest
2161        let manifest_locations = manifest_ns.list_manifest_table_locations().await?;
2162
2163        // Get all tables from directory and skip declared-only tables that have not
2164        // written any actual version manifests yet.
2165        let dir_tables = self
2166            .filter_declared_tables(self.list_directory_tables().await?, false)
2167            .await?;
2168
2169        // Register each directory table that doesn't have an overlapping location
2170        // If a directory name already exists in the manifest,
2171        // that means the table must have already been migrated or created
2172        // in the manifest, so we can skip it.
2173        let mut migrated_count = 0;
2174        for table_name in dir_tables {
2175            // For root namespace tables, the directory name is "table_name.lance"
2176            let dir_name = format!("{}.lance", table_name);
2177            if !manifest_locations.contains(&dir_name) {
2178                manifest_ns.register_table(&table_name, dir_name).await?;
2179                migrated_count += 1;
2180            }
2181        }
2182
2183        Ok(migrated_count)
2184    }
2185
2186    /// Delete physical manifest files for the given table version ranges.
2187    ///
2188    /// This helper backs `batch_delete_table_versions`. It resolves each table's storage
2189    /// location, computes the version file paths, and deletes them, returning an error on
2190    /// the first failure.
2191    ///
2192    /// Returns the number of files successfully deleted.
2193    async fn delete_physical_version_files(
2194        &self,
2195        table_entries: &[TableDeleteEntry],
2196        branch: Option<&str>,
2197    ) -> Result<i64> {
2198        let mut deleted_count = 0i64;
2199        for te in table_entries {
2200            let table_uri = self.resolve_table_location(&te.table_id).await?;
2201            let table_uri = match branch {
2202                Some(b) => self.resolve_branch_location(&table_uri, b).await?,
2203                None => table_uri,
2204            };
2205            let table_path = self.object_store_path_from_uri(&table_uri)?;
2206            let versions_dir_path = table_path.clone().join(VERSIONS_DIR);
2207
2208            // Match listed files, not constructed names (`{version}.manifest` misses V2).
2209            let manifest_metas: Vec<_> = self
2210                .object_store
2211                .read_dir_all(&versions_dir_path, None)
2212                .try_collect()
2213                .await
2214                .map_err(|e| {
2215                    lance_core::Error::from(NamespaceError::Internal {
2216                        message: format!(
2217                            "Failed to list manifest files for table at '{}': {}",
2218                            table_uri, e
2219                        ),
2220                    })
2221                })?;
2222            let location_by_version: HashMap<u64, Path> = manifest_metas
2223                .into_iter()
2224                .filter_map(|meta| {
2225                    let version = Self::manifest_version_from_filename(meta.location.filename()?)?;
2226                    Some((version, meta.location))
2227                })
2228                .collect();
2229
2230            for (&v, version_path) in &location_by_version {
2231                let vi = v as i64;
2232                if !te.ranges.iter().any(|&(s, e)| vi >= s && (e < 0 || vi < e)) {
2233                    continue;
2234                }
2235                match self.object_store.inner.delete(version_path).await {
2236                    Ok(_) => {
2237                        deleted_count += 1;
2238                    }
2239                    Err(object_store::Error::NotFound { .. }) => {}
2240                    Err(e) => {
2241                        return Err(NamespaceError::Internal {
2242                            message: format!(
2243                                "Failed to delete version {} for table at '{}': {}",
2244                                v, table_uri, e
2245                            ),
2246                        }
2247                        .into());
2248                    }
2249                }
2250            }
2251        }
2252        Ok(deleted_count)
2253    }
2254
2255    /// Apply all query parameters from a `QueryTableRequest`-like source onto a `Scanner`.
2256    ///
2257    /// This covers vector search, filters, column projection, limits, and ANN tuning knobs so
2258    /// that `explain_table_query_plan` / `analyze_table_query_plan` produce an accurate plan.
2259    #[allow(clippy::too_many_arguments)]
2260    fn apply_query_params_to_scanner(
2261        scanner: &mut Scanner,
2262        filter: Option<&str>,
2263        columns: Option<&QueryTableRequestColumns>,
2264        vector_column: Option<&str>,
2265        vector: &QueryTableRequestVector,
2266        k: i32,
2267        offset: Option<i32>,
2268        prefilter: Option<bool>,
2269        bypass_vector_index: Option<bool>,
2270        nprobes: Option<i32>,
2271        ef: Option<i32>,
2272        refine_factor: Option<i32>,
2273        distance_type: Option<&str>,
2274        fast_search_flag: Option<bool>,
2275        with_row_id: Option<bool>,
2276        lower_bound: Option<f32>,
2277        upper_bound: Option<f32>,
2278        operation: &str,
2279    ) -> Result<()> {
2280        // prefilter must be set before nearest() so the fragment-scan guard sees it.
2281        if let Some(pf) = prefilter {
2282            scanner.prefilter(pf);
2283        }
2284
2285        if let Some(filter) = filter {
2286            scanner.filter(filter).map_err(|e| {
2287                Error::invalid_input_source(
2288                    format!("Invalid filter expression for {}: {}", operation, e).into(),
2289                )
2290            })?;
2291        }
2292
2293        if let Some(cols) = columns {
2294            if let Some(ref names) = cols.column_names {
2295                scanner.project(names.as_slice()).map_err(|e| {
2296                    Error::invalid_input_source(
2297                        format!("Invalid column projection for {}: {}", operation, e).into(),
2298                    )
2299                })?;
2300            } else if let Some(ref aliases) = cols.column_aliases {
2301                // aliases maps output_alias -> source_column
2302                let pairs: Vec<(&str, &str)> = aliases
2303                    .iter()
2304                    .map(|(alias, src)| (alias.as_str(), src.as_str()))
2305                    .collect();
2306                scanner.project_with_transform(&pairs).map_err(|e| {
2307                    Error::invalid_input_source(
2308                        format!("Invalid column aliases for {}: {}", operation, e).into(),
2309                    )
2310                })?;
2311            }
2312        }
2313
2314        // Resolve query vector: prefer single_vector, fall back to first row of multi_vector.
2315        let query_vec: Option<Vec<f32>> = vector
2316            .single_vector
2317            .as_ref()
2318            .filter(|v| !v.is_empty())
2319            .cloned()
2320            .or_else(|| {
2321                vector
2322                    .multi_vector
2323                    .as_ref()
2324                    .and_then(|mv| mv.first())
2325                    .filter(|v| !v.is_empty())
2326                    .cloned()
2327            });
2328
2329        if let Some(q_vec) = query_vec {
2330            let col = vector_column.unwrap_or("vector");
2331            let q = Arc::new(Float32Array::from(q_vec));
2332            scanner
2333                .nearest(col, q.as_ref(), k.max(1) as usize)
2334                .map_err(|e| {
2335                    Error::invalid_input_source(
2336                        format!("Invalid vector query for {}: {}", operation, e).into(),
2337                    )
2338                })?;
2339
2340            // ANN parameters — must be applied after nearest().
2341            if let Some(n) = nprobes {
2342                scanner.nprobes(n.max(1) as usize);
2343            }
2344            if let Some(e) = ef {
2345                scanner.ef(e.max(1) as usize);
2346            }
2347            if let Some(rf) = refine_factor {
2348                scanner.refine(rf.max(0) as u32);
2349            }
2350            // bypass_vector_index and fast_search are mutually exclusive; apply in order.
2351            if let Some(true) = bypass_vector_index {
2352                scanner.use_index(false);
2353            }
2354            if let Some(true) = fast_search_flag {
2355                scanner.fast_search();
2356            }
2357            if lower_bound.is_some() || upper_bound.is_some() {
2358                scanner.distance_range(lower_bound, upper_bound);
2359            }
2360            if let Some(dt) = distance_type {
2361                let metric = Self::parse_metric_type(Some(dt))?;
2362                scanner.distance_metric(metric);
2363            }
2364            // Apply offset on top of the k nearest results.
2365            if let Some(off) = offset.filter(|&o| o > 0) {
2366                scanner.limit(None, Some(off as i64)).map_err(|e| {
2367                    Error::invalid_input_source(
2368                        format!("Invalid offset for {}: {}", operation, e).into(),
2369                    )
2370                })?;
2371            }
2372        } else {
2373            // Scalar (non-vector) query: treat k as a row LIMIT.
2374            let limit = if k > 0 { Some(k as i64) } else { None };
2375            scanner
2376                .limit(limit, offset.map(|o| o as i64))
2377                .map_err(|e| {
2378                    Error::invalid_input_source(
2379                        format!("Invalid limit/offset for {}: {}", operation, e).into(),
2380                    )
2381                })?;
2382        }
2383
2384        if let Some(true) = with_row_id {
2385            scanner.with_row_id();
2386        }
2387
2388        Ok(())
2389    }
2390
2391    /// Retrieve a snapshot of operation metrics.
2392    ///
2393    /// Returns a HashMap where keys are operation names (e.g., "list_tables", "describe_table")
2394    /// and values are the number of times each operation was called.
2395    ///
2396    /// Returns an empty HashMap if `ops_metrics_enabled` was false when building the namespace.
2397    pub fn retrieve_ops_metrics(&self) -> HashMap<String, u64> {
2398        self.ops_metrics
2399            .as_ref()
2400            .map(|m| m.retrieve())
2401            .unwrap_or_default()
2402    }
2403
2404    /// Reset all operation metrics counters to zero.
2405    ///
2406    /// Does nothing if `ops_metrics_enabled` was false when building the namespace.
2407    pub fn reset_ops_metrics(&self) {
2408        if let Some(ref metrics) = self.ops_metrics {
2409            metrics.reset();
2410        }
2411    }
2412
2413    /// Increment the counter for an operation.
2414    fn record_op(&self, operation: &str) {
2415        if let Some(ref metrics) = self.ops_metrics {
2416            metrics.increment(operation);
2417        }
2418    }
2419}
2420
2421#[async_trait]
2422impl LanceNamespace for DirectoryNamespace {
2423    async fn list_namespaces(
2424        &self,
2425        request: ListNamespacesRequest,
2426    ) -> Result<ListNamespacesResponse> {
2427        self.record_op("list_namespaces");
2428        if let Some(ref manifest_ns) = self.manifest_ns {
2429            return manifest_ns.list_namespaces(request).await;
2430        }
2431
2432        Self::validate_root_namespace_id(&request.id)?;
2433        Ok(ListNamespacesResponse::new(vec![]))
2434    }
2435
2436    async fn describe_namespace(
2437        &self,
2438        request: DescribeNamespaceRequest,
2439    ) -> Result<DescribeNamespaceResponse> {
2440        self.record_op("describe_namespace");
2441        if let Some(ref manifest_ns) = self.manifest_ns {
2442            return manifest_ns.describe_namespace(request).await;
2443        }
2444
2445        Self::validate_root_namespace_id(&request.id)?;
2446        #[allow(clippy::needless_update)]
2447        Ok(DescribeNamespaceResponse {
2448            properties: Some(HashMap::new()),
2449            ..Default::default()
2450        })
2451    }
2452
2453    async fn create_namespace(
2454        &self,
2455        request: CreateNamespaceRequest,
2456    ) -> Result<CreateNamespaceResponse> {
2457        self.record_op("create_namespace");
2458        if let Some(ref manifest_ns) = self.manifest_ns {
2459            return manifest_ns.create_namespace(request).await;
2460        }
2461
2462        if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
2463            return Err(NamespaceError::NamespaceAlreadyExists {
2464                message: "root namespace".to_string(),
2465            }
2466            .into());
2467        }
2468
2469        Err(NamespaceError::Unsupported {
2470            message: "Child namespaces are only supported when manifest mode is enabled"
2471                .to_string(),
2472        }
2473        .into())
2474    }
2475
2476    async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse> {
2477        self.record_op("drop_namespace");
2478        if let Some(ref manifest_ns) = self.manifest_ns {
2479            return manifest_ns.drop_namespace(request).await;
2480        }
2481
2482        if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
2483            return Err(NamespaceError::InvalidInput {
2484                message: "Root namespace cannot be dropped".to_string(),
2485            }
2486            .into());
2487        }
2488
2489        Err(NamespaceError::Unsupported {
2490            message: "Child namespaces are only supported when manifest mode is enabled"
2491                .to_string(),
2492        }
2493        .into())
2494    }
2495
2496    async fn namespace_exists(&self, request: NamespaceExistsRequest) -> Result<()> {
2497        self.record_op("namespace_exists");
2498        if let Some(ref manifest_ns) = self.manifest_ns {
2499            return manifest_ns.namespace_exists(request).await;
2500        }
2501
2502        if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
2503            return Ok(());
2504        }
2505
2506        Err(NamespaceError::NamespaceNotFound {
2507            message: "Child namespaces are only supported when manifest mode is enabled"
2508                .to_string(),
2509        }
2510        .into())
2511    }
2512
2513    async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
2514        self.record_op("list_tables");
2515        // Validate that namespace ID is provided
2516        let namespace_id = request.id.as_ref().ok_or_else(|| {
2517            lance_core::Error::from(NamespaceError::InvalidInput {
2518                message: "Namespace ID is required".to_string(),
2519            })
2520        })?;
2521
2522        // For child namespaces, always delegate to manifest (if enabled)
2523        if !namespace_id.is_empty() {
2524            if let Some(ref manifest_ns) = self.manifest_ns {
2525                return manifest_ns.list_tables(request).await;
2526            }
2527            return Err(NamespaceError::Unsupported {
2528                message: "Child namespaces are only supported when manifest mode is enabled"
2529                    .to_string(),
2530            }
2531            .into());
2532        }
2533
2534        // When only manifest is enabled (no directory listing), delegate directly to manifest
2535        if let Some(ref manifest_ns) = self.manifest_ns
2536            && !self.dir_listing_enabled
2537        {
2538            return manifest_ns.list_tables(request).await;
2539        }
2540
2541        // When both manifest and directory listing are enabled with migration mode,
2542        // we need to merge and deduplicate
2543        let mut tables = if self.manifest_ns.is_some()
2544            && self.dir_listing_enabled
2545            && self.dir_listing_to_manifest_migration_enabled
2546        {
2547            // Get all manifest table locations (for deduplication)
2548            let manifest_locations = if let Some(ref manifest_ns) = self.manifest_ns {
2549                manifest_ns.list_manifest_table_locations().await?
2550            } else {
2551                std::collections::HashSet::new()
2552            };
2553
2554            // Get all manifest tables (without pagination for merging)
2555            let mut manifest_request = request.clone();
2556            manifest_request.limit = None;
2557            manifest_request.page_token = None;
2558            let manifest_tables = if let Some(ref manifest_ns) = self.manifest_ns {
2559                let manifest_response = manifest_ns.list_tables(manifest_request).await?;
2560                manifest_response.tables
2561            } else {
2562                vec![]
2563            };
2564
2565            // Start with all manifest table names
2566            // Add directory tables that aren't already in the manifest (by location)
2567            let mut all_tables: Vec<String> = manifest_tables;
2568            let dir_tables = self.list_directory_tables().await?;
2569            for table_name in dir_tables {
2570                // Check if this table's location is already in the manifest
2571                // Manifest stores full URIs, so we need to check both formats
2572                let full_location = format!("{}/{}.lance", self.root, table_name);
2573                let relative_location = format!("{}.lance", table_name);
2574                if !manifest_locations.contains(&full_location)
2575                    && !manifest_locations.contains(&relative_location)
2576                {
2577                    all_tables.push(table_name);
2578                }
2579            }
2580
2581            all_tables
2582        } else {
2583            self.list_directory_tables().await?
2584        };
2585
2586        tables = self
2587            .filter_declared_tables(tables, request.include_declared.unwrap_or(true))
2588            .await?;
2589
2590        // Apply sorting and pagination
2591        let next_page_token =
2592            Self::apply_pagination(&mut tables, request.page_token, request.limit);
2593        let mut response = ListTablesResponse::new(tables);
2594        response.page_token = next_page_token;
2595        Ok(response)
2596    }
2597
2598    async fn describe_table(&self, request: DescribeTableRequest) -> Result<DescribeTableResponse> {
2599        self.record_op("describe_table");
2600        self.describe_table_impl(request).await
2601    }
2602
2603    async fn table_exists(&self, request: TableExistsRequest) -> Result<()> {
2604        self.record_op("table_exists");
2605        let is_root_level = request.id.as_ref().is_some_and(|id| id.len() == 1);
2606        let skip_manifest_for_root = self.dir_listing_enabled
2607            && is_root_level
2608            && !self.dir_listing_to_manifest_migration_enabled;
2609        if let Some(ref manifest_ns) = self.manifest_ns
2610            && !skip_manifest_for_root
2611        {
2612            match manifest_ns.table_exists(request.clone()).await {
2613                Ok(()) => return Ok(()),
2614                Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
2615                    // An incompatible manifest must surface "please upgrade"
2616                    // rather than degrading to a directory-listing view.
2617                    return Err(e);
2618                }
2619                Err(_) if self.dir_listing_enabled && is_root_level => {
2620                    // Fall through to directory check only for single-level IDs
2621                }
2622                Err(e) => return Err(e),
2623            }
2624        }
2625
2626        let table_name = Self::table_name_from_id(&request.id)?;
2627        let table_id = Self::format_table_id_from_request(&request.id);
2628
2629        // Atomically check table existence and deregistration status
2630        let status = self.check_table_status(&table_name).await;
2631
2632        if !status.exists {
2633            return Err(NamespaceError::TableNotFound {
2634                message: table_id.clone(),
2635            }
2636            .into());
2637        }
2638
2639        if status.is_deregistered {
2640            return Err(NamespaceError::TableNotFound {
2641                message: format!("Table is deregistered: {}", table_id),
2642            }
2643            .into());
2644        }
2645
2646        Ok(())
2647    }
2648
2649    async fn drop_table(&self, request: DropTableRequest) -> Result<DropTableResponse> {
2650        self.record_op("drop_table");
2651        if let Some(ref manifest_ns) = self.manifest_ns {
2652            return manifest_ns.drop_table(request).await;
2653        }
2654
2655        let table_name = Self::table_name_from_id(&request.id)?;
2656        let table_uri = self.table_full_uri(&table_name);
2657        let table_path = self.table_path(&table_name);
2658
2659        self.object_store
2660            .remove_dir_all(table_path)
2661            .await
2662            .map_err(|e| {
2663                lance_core::Error::from(NamespaceError::Internal {
2664                    message: format!("Failed to drop table {}: {:?}", table_name, e),
2665                })
2666            })?;
2667
2668        Ok(DropTableResponse {
2669            id: request.id,
2670            location: Some(table_uri),
2671            ..Default::default()
2672        })
2673    }
2674
2675    async fn create_table(
2676        &self,
2677        request: CreateTableRequest,
2678        request_data: Bytes,
2679    ) -> Result<CreateTableResponse> {
2680        self.record_op("create_table");
2681        if let Some(ref manifest_ns) = self.manifest_ns {
2682            return manifest_ns.create_table(request, request_data).await;
2683        }
2684
2685        Self::validate_dir_only_properties(request.properties.as_ref(), "create_table")?;
2686
2687        let table_name = Self::table_name_from_id(&request.id)?;
2688        let table_uri = self.table_full_uri(&table_name);
2689        let status = self.check_table_status(&table_name).await;
2690        let (reader, _num_rows) =
2691            Self::ipc_reader_from_request_data(&request_data, "create_table")?;
2692
2693        if status.exists && self.table_has_actual_manifests(&table_name).await? {
2694            return Err(NamespaceError::TableAlreadyExists {
2695                message: table_name,
2696            }
2697            .into());
2698        }
2699
2700        let write_result = self
2701            .write_reader_to_table(
2702                &table_uri,
2703                reader,
2704                WriteMode::Create,
2705                request.storage_options.clone(),
2706            )
2707            .await;
2708        if let Err(err) = write_result {
2709            if self.table_uri_has_actual_manifests(&table_uri).await? {
2710                return Err(NamespaceError::TableAlreadyExists {
2711                    message: table_name,
2712                }
2713                .into());
2714            }
2715            return Err(err);
2716        }
2717        Ok(CreateTableResponse {
2718            version: Some(1),
2719            location: Some(table_uri),
2720            storage_options: self.storage_options.clone(),
2721            properties: request.properties,
2722            ..Default::default()
2723        })
2724    }
2725
2726    async fn declare_table(&self, request: DeclareTableRequest) -> Result<DeclareTableResponse> {
2727        self.record_op("declare_table");
2728        if let Some(ref manifest_ns) = self.manifest_ns {
2729            let mut response = manifest_ns.declare_table(request.clone()).await?;
2730            if let Some(ref location) = response.location {
2731                // For backwards compatibility, only skip vending credentials when explicitly set to false
2732                let vend = request.vend_credentials.unwrap_or(true);
2733                let identity = request.identity.as_deref();
2734                response.storage_options = self
2735                    .get_storage_options_for_table(location, vend, identity)
2736                    .await?;
2737            }
2738            // Set managed_versioning when table_version_tracking_enabled
2739            if self.table_version_tracking_enabled {
2740                response.managed_versioning = Some(true);
2741            }
2742            return Ok(response);
2743        }
2744
2745        Self::validate_dir_only_properties(request.properties.as_ref(), "declare_table")?;
2746
2747        let table_name = Self::table_name_from_id(&request.id)?;
2748        let table_uri = self.table_full_uri(&table_name);
2749
2750        // Validate location if provided
2751        if let Some(location) = &request.location {
2752            let location = location.trim_end_matches('/');
2753            if location != table_uri {
2754                return Err(NamespaceError::InvalidInput {
2755                    message: format!(
2756                        "Cannot declare table {} at location {}, must be at location {}",
2757                        table_name, location, table_uri
2758                    ),
2759                }
2760                .into());
2761            }
2762        }
2763
2764        // Check if table already has data (created via create_table).
2765        // The atomic put only prevents races between concurrent declare_table calls,
2766        // not between declare_table and existing data.
2767        let status = self.check_table_status(&table_name).await;
2768        if status.exists && !status.has_reserved_file {
2769            // Table has data but no reserved file - it was created with data
2770            return Err(NamespaceError::TableAlreadyExists {
2771                message: table_name.to_string(),
2772            }
2773            .into());
2774        }
2775
2776        // Atomically create the .lance-reserved file to mark the table as declared.
2777        // This uses put_if_not_exists semantics to avoid race conditions between
2778        // concurrent declare_table calls.
2779        let reserved_file_path = self.table_reserved_file_path(&table_name);
2780
2781        self.put_marker_file_atomic(&reserved_file_path, &format!("table {}", table_name))
2782            .await
2783            .map_err(|e| {
2784                if e.contains("already exists") {
2785                    lance_core::Error::from(NamespaceError::TableAlreadyExists {
2786                        message: table_name.to_string(),
2787                    })
2788                } else {
2789                    lance_core::Error::from(NamespaceError::Internal { message: e })
2790                }
2791            })?;
2792
2793        // For backwards compatibility, only skip vending credentials when explicitly set to false
2794        let vend_credentials = request.vend_credentials.unwrap_or(true);
2795        let identity = request.identity.as_deref();
2796        let storage_options = self
2797            .get_storage_options_for_table(&table_uri, vend_credentials, identity)
2798            .await?;
2799
2800        Ok(DeclareTableResponse {
2801            location: Some(table_uri),
2802            storage_options,
2803            properties: request.properties,
2804            managed_versioning: if self.table_version_tracking_enabled {
2805                Some(true)
2806            } else {
2807                None
2808            },
2809            ..Default::default()
2810        })
2811    }
2812
2813    async fn register_table(
2814        &self,
2815        request: lance_namespace::models::RegisterTableRequest,
2816    ) -> Result<lance_namespace::models::RegisterTableResponse> {
2817        self.record_op("register_table");
2818        // If manifest is enabled, delegate to manifest namespace
2819        if let Some(ref manifest_ns) = self.manifest_ns {
2820            return LanceNamespace::register_table(manifest_ns.as_ref(), request).await;
2821        }
2822
2823        // Without manifest, register_table is not supported
2824        Err(NamespaceError::Unsupported {
2825            message: "register_table is only supported when manifest mode is enabled".to_string(),
2826        }
2827        .into())
2828    }
2829
2830    async fn deregister_table(
2831        &self,
2832        request: lance_namespace::models::DeregisterTableRequest,
2833    ) -> Result<lance_namespace::models::DeregisterTableResponse> {
2834        self.record_op("deregister_table");
2835        // If manifest is enabled, delegate to manifest namespace
2836        if let Some(ref manifest_ns) = self.manifest_ns {
2837            return LanceNamespace::deregister_table(manifest_ns.as_ref(), request).await;
2838        }
2839
2840        // V1 mode: create a .lance-deregistered marker file in the table directory
2841        let table_name = Self::table_name_from_id(&request.id)?;
2842        let table_uri = self.table_full_uri(&table_name);
2843
2844        // Check table existence and deregistration status.
2845        // This provides better error messages for common cases.
2846        let status = self.check_table_status(&table_name).await;
2847
2848        if !status.exists {
2849            return Err(NamespaceError::TableNotFound {
2850                message: table_name.to_string(),
2851            }
2852            .into());
2853        }
2854
2855        if status.is_deregistered {
2856            return Err(NamespaceError::TableNotFound {
2857                message: format!("Table is already deregistered: {}", table_name),
2858            }
2859            .into());
2860        }
2861
2862        // Atomically create the .lance-deregistered marker file.
2863        // This uses put_if_not_exists semantics to prevent race conditions
2864        // when multiple processes try to deregister the same table concurrently.
2865        // If a race occurs and another process already created the file,
2866        // we'll get an AlreadyExists error which we convert to a proper message.
2867        let deregistered_path = self.table_deregistered_file_path(&table_name);
2868        self.put_marker_file_atomic(
2869            &deregistered_path,
2870            &format!("deregistration marker for table {}", table_name),
2871        )
2872        .await
2873        .map_err(|e| {
2874            if e.contains("already exists") {
2875                lance_core::Error::from(NamespaceError::InvalidTableState {
2876                    message: format!("Table is already deregistered: {}", table_name),
2877                })
2878            } else {
2879                lance_core::Error::from(NamespaceError::Internal { message: e })
2880            }
2881        })?;
2882
2883        Ok(lance_namespace::models::DeregisterTableResponse {
2884            id: request.id,
2885            location: Some(table_uri),
2886            ..Default::default()
2887        })
2888    }
2889
2890    async fn list_table_versions(
2891        &self,
2892        request: ListTableVersionsRequest,
2893    ) -> Result<ListTableVersionsResponse> {
2894        self.record_op("list_table_versions");
2895        let branch = Self::normalized_branch(request.branch.as_deref())?;
2896        let table_uri = self.resolve_table_location(&request.id).await?;
2897        let table_uri = match branch {
2898            Some(b) => self.resolve_branch_location(&table_uri, b).await?,
2899            None => table_uri,
2900        };
2901        let want_descending = request.descending == Some(true);
2902        let table_versions = self
2903            .list_table_versions_from_storage(&table_uri, want_descending, request.limit)
2904            .await?;
2905
2906        Ok(ListTableVersionsResponse {
2907            versions: table_versions,
2908            page_token: None,
2909        })
2910    }
2911
2912    async fn create_table_version(
2913        &self,
2914        request: CreateTableVersionRequest,
2915    ) -> Result<CreateTableVersionResponse> {
2916        self.record_op("create_table_version");
2917        let branch = Self::normalized_branch(request.branch.as_deref())?;
2918        let table_uri = self.resolve_table_location(&request.id).await?;
2919        let table_uri = match branch {
2920            Some(b) => self.resolve_branch_location(&table_uri, b).await?,
2921            None => table_uri,
2922        };
2923
2924        let staging_manifest_path = &request.manifest_path;
2925        let version = request.version as u64;
2926
2927        let table_path = self.object_store_path_from_uri(&table_uri)?;
2928
2929        // Determine naming scheme from request, default to V2
2930        let naming_scheme = match request.naming_scheme.as_deref() {
2931            Some("V1") => ManifestNamingScheme::V1,
2932            _ => ManifestNamingScheme::V2,
2933        };
2934
2935        // Compute final path using the naming scheme
2936        let final_path = naming_scheme.manifest_path(&table_path, version);
2937
2938        let staging_path = Path::parse(staging_manifest_path).map_err(|e| {
2939            lance_core::Error::from(NamespaceError::InvalidInput {
2940                message: format!(
2941                    "Invalid staging manifest path '{}': {}",
2942                    staging_manifest_path, e
2943                ),
2944            })
2945        })?;
2946
2947        let copy_result = match self
2948            .object_store
2949            .inner
2950            .copy_if_not_exists(&staging_path, &final_path)
2951            .await
2952        {
2953            Ok(()) => Ok(()),
2954            Err(ObjectStoreError::NotImplemented { .. })
2955            | Err(ObjectStoreError::NotSupported { .. }) => {
2956                let manifest_data = self
2957                    .object_store
2958                    .inner
2959                    .get(&staging_path)
2960                    .await
2961                    .map_err(|e| {
2962                        lance_core::Error::from(NamespaceError::Internal {
2963                            message: format!(
2964                                "Failed to read staging manifest at '{}': {}",
2965                                staging_manifest_path, e
2966                            ),
2967                        })
2968                    })?
2969                    .bytes()
2970                    .await
2971                    .map_err(|e| {
2972                        lance_core::Error::from(NamespaceError::Internal {
2973                            message: format!(
2974                                "Failed to read staging manifest bytes at '{}': {}",
2975                                staging_manifest_path, e
2976                            ),
2977                        })
2978                    })?;
2979                self.object_store
2980                    .inner
2981                    .put_opts(
2982                        &final_path,
2983                        manifest_data.into(),
2984                        PutOptions {
2985                            mode: PutMode::Create,
2986                            ..Default::default()
2987                        },
2988                    )
2989                    .await
2990                    .map(|_| ())
2991            }
2992            Err(e) => Err(e),
2993        };
2994
2995        match copy_result {
2996            Ok(()) => {}
2997            Err(ObjectStoreError::AlreadyExists { .. })
2998            | Err(ObjectStoreError::Precondition { .. }) => {
2999                return Err(lance_core::Error::from(
3000                    NamespaceError::ConcurrentModification {
3001                        message: format!(
3002                            "Version {} already exists for table at '{}'",
3003                            version, table_uri
3004                        ),
3005                    },
3006                ));
3007            }
3008            Err(e) => {
3009                return Err(lance_core::Error::from(NamespaceError::Internal {
3010                    message: format!(
3011                        "Failed to create version {} for table at '{}': {}",
3012                        version, table_uri, e
3013                    ),
3014                }));
3015            }
3016        }
3017
3018        let final_meta = self
3019            .object_store
3020            .inner
3021            .head(&final_path)
3022            .await
3023            .map_err(|e| {
3024                lance_core::Error::from(NamespaceError::Internal {
3025                    message: format!(
3026                        "Failed to stat created version {} for table at '{}': {}",
3027                        version, table_uri, e
3028                    ),
3029                })
3030            })?;
3031        let manifest_size = final_meta.size as i64;
3032
3033        // Delete the staging manifest after successful copy
3034        if let Err(e) = self.object_store.inner.delete(&staging_path).await {
3035            log::warn!(
3036                "Failed to delete staging manifest at '{}': {:?}",
3037                staging_path,
3038                e
3039            );
3040        }
3041
3042        Ok(CreateTableVersionResponse {
3043            transaction_id: None,
3044            version: Some(Box::new(TableVersion {
3045                version: version as i64,
3046                manifest_path: final_path.to_string(),
3047                manifest_size: Some(manifest_size),
3048                e_tag: final_meta.e_tag,
3049                timestamp_millis: None,
3050                metadata: None,
3051            })),
3052        })
3053    }
3054
3055    async fn describe_table_version(
3056        &self,
3057        request: DescribeTableVersionRequest,
3058    ) -> Result<DescribeTableVersionResponse> {
3059        self.record_op("describe_table_version");
3060        let branch = Self::normalized_branch(request.branch.as_deref())?;
3061        let table_uri = self.resolve_table_location(&request.id).await?;
3062        let table_uri = match branch {
3063            Some(b) => self.resolve_branch_location(&table_uri, b).await?,
3064            None => table_uri,
3065        };
3066        let versions = self
3067            .list_table_versions_from_storage(&table_uri, true, None)
3068            .await?;
3069        let table_version = if let Some(requested_version) = request.version {
3070            versions
3071                .into_iter()
3072                .find(|version| version.version == requested_version)
3073                .ok_or_else(|| {
3074                    lance_core::Error::from(NamespaceError::TableVersionNotFound {
3075                        message: format!(
3076                            "version {} for table {}",
3077                            requested_version,
3078                            Self::format_table_id_from_request(&request.id)
3079                        ),
3080                    })
3081                })?
3082        } else {
3083            versions.into_iter().next().ok_or_else(|| {
3084                lance_core::Error::from(NamespaceError::TableVersionNotFound {
3085                    message: format!(
3086                        "latest version for table {}",
3087                        Self::format_table_id_from_request(&request.id)
3088                    ),
3089                })
3090            })?
3091        };
3092
3093        Ok(DescribeTableVersionResponse {
3094            version: Box::new(table_version),
3095        })
3096    }
3097
3098    async fn batch_delete_table_versions(
3099        &self,
3100        request: BatchDeleteTableVersionsRequest,
3101    ) -> Result<BatchDeleteTableVersionsResponse> {
3102        self.record_op("batch_delete_table_versions");
3103        let branch = Self::normalized_branch(request.branch.as_deref())?;
3104        // Single-table mode: use `id` (from path parameter) + `ranges` to delete
3105        // versions from one table.
3106        let ranges: Vec<(i64, i64)> = request
3107            .ranges
3108            .iter()
3109            .map(|r| (r.start_version, r.end_version))
3110            .collect();
3111
3112        // Reject pathological bounded ranges up front: an explicit huge bounded
3113        // range like (0, i64::MAX) is almost certainly a mistake. A through-latest
3114        // range (end < 0) is bounded by the manifests that actually exist on storage.
3115        const MAX_VERSIONS_PER_REQUEST: i128 = 1_000_000;
3116        let requested: i128 = ranges
3117            .iter()
3118            .map(|(s, e)| {
3119                if *e < 0 {
3120                    0
3121                } else {
3122                    (*e as i128 - *s as i128).max(0)
3123                }
3124            })
3125            .sum();
3126        if requested > MAX_VERSIONS_PER_REQUEST {
3127            return Err(NamespaceError::InvalidInput {
3128                message: format!(
3129                    "batch_delete requested {} versions; limit is {}",
3130                    requested, MAX_VERSIONS_PER_REQUEST
3131                ),
3132            }
3133            .into());
3134        }
3135
3136        let table_entries = vec![TableDeleteEntry {
3137            table_id: request.id.clone(),
3138            ranges,
3139        }];
3140
3141        let total_deleted_count = self
3142            .delete_physical_version_files(&table_entries, branch)
3143            .await?;
3144
3145        Ok(BatchDeleteTableVersionsResponse {
3146            deleted_count: Some(total_deleted_count),
3147            transaction_id: None,
3148        })
3149    }
3150
3151    async fn create_table_index(
3152        &self,
3153        request: CreateTableIndexRequest,
3154    ) -> Result<CreateTableIndexResponse> {
3155        self.record_op("create_table_index");
3156        let table_uri = self.resolve_table_location(&request.id).await?;
3157        let mut dataset = self
3158            .load_dataset(&table_uri, None, "create_table_index")
3159            .await?;
3160        let index_request = Self::build_index_params(&request)?;
3161
3162        dataset
3163            .create_index(
3164                &[request.column.as_str()],
3165                index_request.index_type(),
3166                request.name.clone(),
3167                index_request.params(),
3168                false,
3169            )
3170            .await
3171            .map_err(|e| {
3172                let err_msg = format!("{}", e);
3173                let ns_err = if err_msg.contains("already exists") {
3174                    NamespaceError::TableIndexAlreadyExists {
3175                        message: format!(
3176                            "Index '{}' already exists on table '{}': {:?}",
3177                            request.name.as_deref().unwrap_or("<auto-generated>"),
3178                            table_uri,
3179                            e
3180                        ),
3181                    }
3182                } else if err_msg.contains("not found") || err_msg.contains("does not exist") {
3183                    NamespaceError::TableColumnNotFound {
3184                        message: format!(
3185                            "Column '{}' not found for table '{}': {:?}",
3186                            request.column, table_uri, e
3187                        ),
3188                    }
3189                } else {
3190                    NamespaceError::Internal {
3191                        message: format!(
3192                            "Failed to create {} index '{}' on column '{}' for table '{}': {:?}",
3193                            request.index_type,
3194                            request.name.as_deref().unwrap_or("<auto-generated>"),
3195                            request.column,
3196                            table_uri,
3197                            e
3198                        ),
3199                    }
3200                };
3201                lance_core::Error::from(ns_err)
3202            })?;
3203
3204        let transaction_id = dataset
3205            .read_transaction()
3206            .await
3207            .map_err(|e| {
3208                lance_core::Error::from(NamespaceError::Internal {
3209                    message: format!(
3210                        "Failed to read committed transaction after creating index on '{}': {}",
3211                        table_uri, e
3212                    ),
3213                })
3214            })?
3215            .map(|transaction| transaction.uuid);
3216
3217        Ok(CreateTableIndexResponse { transaction_id })
3218    }
3219
3220    async fn list_table_indices(
3221        &self,
3222        request: ListTableIndicesRequest,
3223    ) -> Result<ListTableIndicesResponse> {
3224        self.record_op("list_table_indices");
3225        let table_uri = self.resolve_table_location(&request.id).await?;
3226        let dataset = self
3227            .load_dataset(&table_uri, request.version, "list_table_indices")
3228            .await?;
3229        let total_rows = dataset.count_rows(None).await.map_err(|e| {
3230            lance_core::Error::from(NamespaceError::Internal {
3231                message: format!("Failed to count rows for table '{}': {:?}", table_uri, e),
3232            })
3233        })? as u64;
3234        let mut indices = dataset
3235            .describe_indices(None)
3236            .await
3237            .map_err(|e| {
3238                lance_core::Error::from(NamespaceError::Internal {
3239                    message: format!("Failed to describe table indices for '{}': {:?}", table_uri, e),
3240                })
3241            })?
3242            .into_iter()
3243            .filter(|description| {
3244                description
3245                    .metadata()
3246                    .first()
3247                    .map(|metadata| !is_system_index(metadata))
3248                    .unwrap_or(false)
3249            })
3250            .map(|description| {
3251                let columns = description
3252                    .field_ids()
3253                    .iter()
3254                        .map(|field_id| {
3255                        dataset
3256                            .schema()
3257                            .field_path(i32::try_from(*field_id).map_err(|e| {
3258                                lance_core::Error::from(NamespaceError::Internal {
3259                                    message: format!(
3260                                        "Field id {} does not fit in i32 for table '{}': {}",
3261                                        field_id, table_uri, e
3262                                    ),
3263                                })
3264                            })?)
3265                            .map_err(|e| {
3266                            lance_core::Error::from(NamespaceError::Internal {
3267                                message: format!(
3268                                    "Failed to resolve field path for field_id {} in table '{}': {}",
3269                                    field_id, table_uri, e
3270                                ),
3271                            })
3272                        })
3273                    })
3274                    .collect::<Result<Vec<_>>>()?;
3275
3276                let segments = description.segments();
3277                let created_at = segments
3278                    .iter()
3279                    .filter_map(|segment| segment.created_at)
3280                    .min()
3281                    .map(|ts| ts.to_rfc3339());
3282
3283                // `..Default::default()` keeps this tolerant of additive reqwest
3284                // client model changes (see #7212).
3285                #[allow(clippy::needless_update)]
3286                let content = IndexContent {
3287                    index_name: description.name().to_string(),
3288                    index_uuid: description.metadata()[0].uuid.to_string(),
3289                    columns,
3290                    status: "SUCCEEDED".to_string(),
3291                    index_type: Some(description.index_type().to_string()),
3292                    type_url: Some(description.type_url().to_string()),
3293                    num_indexed_rows: Some(description.rows_indexed() as i64),
3294                    num_unindexed_rows: Some(
3295                        total_rows.saturating_sub(description.rows_indexed()) as i64,
3296                    ),
3297                    size_bytes: description.total_size_bytes().map(|size| size as i64),
3298                    num_segments: Some(segments.len() as i32),
3299                    created_at,
3300                    index_version: segments.first().map(|segment| segment.index_version),
3301                    index_details: description.details().ok(),
3302                    ..Default::default()
3303                };
3304                Ok(content)
3305            })
3306            .collect::<Result<Vec<_>>>()?;
3307
3308        let page_token = Self::paginate_indices(&mut indices, request.page_token, request.limit);
3309        Ok(ListTableIndicesResponse {
3310            indexes: indices,
3311            page_token,
3312        })
3313    }
3314
3315    async fn describe_table_index_stats(
3316        &self,
3317        request: DescribeTableIndexStatsRequest,
3318    ) -> Result<DescribeTableIndexStatsResponse> {
3319        self.record_op("describe_table_index_stats");
3320        let table_uri = self.resolve_table_location(&request.id).await?;
3321        let dataset = self
3322            .load_dataset(&table_uri, request.version, "describe_table_index_stats")
3323            .await?;
3324        let index_name = request.index_name.as_deref().ok_or_else(|| {
3325            lance_core::Error::from(NamespaceError::InvalidInput {
3326                message: "Index name is required for describe_table_index_stats".to_string(),
3327            })
3328        })?;
3329        let metadatas = dataset
3330            .load_indices_by_name(index_name)
3331            .await
3332            .map_err(|e| {
3333                lance_core::Error::from(NamespaceError::TableIndexNotFound {
3334                    message: format!(
3335                        "Failed to load index '{}' metadata for table '{}': {}",
3336                        index_name, table_uri, e
3337                    ),
3338                })
3339            })?;
3340        if metadatas.first().is_some_and(is_system_index) {
3341            return Err(NamespaceError::Unsupported {
3342                message: format!("System index '{}' is not exposed by this API", index_name),
3343            }
3344            .into());
3345        }
3346
3347        let stats = <Dataset as DatasetIndexExt>::index_statistics(&dataset, index_name)
3348            .await
3349            .map_err(|e| {
3350                lance_core::Error::from(NamespaceError::TableIndexNotFound {
3351                    message: format!(
3352                        "Failed to describe index statistics for '{}' on table '{}': {}",
3353                        index_name, table_uri, e
3354                    ),
3355                })
3356            })?;
3357        let stats: serde_json::Value = serde_json::from_str(&stats).map_err(|e| {
3358            lance_core::Error::from(NamespaceError::Internal {
3359                message: format!(
3360                    "Failed to parse index statistics for '{}' on table '{}': {}",
3361                    index_name, table_uri, e
3362                ),
3363            })
3364        })?;
3365
3366        Ok(Self::describe_table_index_stats_response(&stats))
3367    }
3368
3369    async fn describe_transaction(
3370        &self,
3371        request: DescribeTransactionRequest,
3372    ) -> Result<DescribeTransactionResponse> {
3373        self.record_op("describe_transaction");
3374        let mut request_id = request.id.ok_or_else(|| {
3375            lance_core::Error::from(NamespaceError::InvalidInput {
3376                message: "Transaction id must include table id and transaction identifier"
3377                    .to_string(),
3378            })
3379        })?;
3380        if request_id.len() < 2 {
3381            return Err(NamespaceError::InvalidInput {
3382                message: format!(
3383                    "Transaction request id must include table id and transaction identifier, got {:?}",
3384                    request_id
3385                ),
3386            }
3387            .into());
3388        }
3389
3390        let id = request_id.pop().expect("request_id len checked above");
3391        let table_id = Some(request_id);
3392        let table_uri = self.resolve_table_location(&table_id).await?;
3393        let dataset = self
3394            .load_dataset(&table_uri, None, "describe_transaction")
3395            .await?;
3396        let (version, transaction) = self.find_transaction(&dataset, &id).await?;
3397
3398        Ok(Self::transaction_response(version, &transaction))
3399    }
3400
3401    async fn create_table_scalar_index(
3402        &self,
3403        request: CreateTableIndexRequest,
3404    ) -> Result<CreateTableScalarIndexResponse> {
3405        self.record_op("create_table_scalar_index");
3406        let index_type = Self::parse_index_type(&request.index_type)?;
3407        if !index_type.is_scalar() {
3408            return Err(NamespaceError::InvalidInput {
3409                message: format!(
3410                    "create_table_scalar_index only supports scalar index types, got {}",
3411                    request.index_type
3412                ),
3413            }
3414            .into());
3415        }
3416
3417        let response = self.create_table_index(request).await?;
3418        Ok(CreateTableScalarIndexResponse {
3419            transaction_id: response.transaction_id,
3420        })
3421    }
3422
3423    async fn drop_table_index(
3424        &self,
3425        request: DropTableIndexRequest,
3426    ) -> Result<DropTableIndexResponse> {
3427        self.record_op("drop_table_index");
3428        let table_uri = self.resolve_table_location(&request.id).await?;
3429        let index_name = request.index_name.as_deref().ok_or_else(|| {
3430            lance_core::Error::from(NamespaceError::InvalidInput {
3431                message: "Index name is required for drop_table_index".to_string(),
3432            })
3433        })?;
3434        let mut dataset = self
3435            .load_dataset(&table_uri, None, "drop_table_index")
3436            .await?;
3437        let metadatas = dataset
3438            .load_indices_by_name(index_name)
3439            .await
3440            .map_err(|e| {
3441                lance_core::Error::from(NamespaceError::TableIndexNotFound {
3442                    message: format!(
3443                        "Failed to load index '{}' before dropping it from table '{}': {}",
3444                        index_name, table_uri, e
3445                    ),
3446                })
3447            })?;
3448        if metadatas.first().is_some_and(is_system_index) {
3449            return Err(NamespaceError::Unsupported {
3450                message: format!(
3451                    "System index '{}' cannot be dropped via this API",
3452                    index_name
3453                ),
3454            }
3455            .into());
3456        }
3457
3458        dataset.drop_index(index_name).await.map_err(|e| {
3459            lance_core::Error::from(NamespaceError::TableIndexNotFound {
3460                message: format!(
3461                    "Failed to drop index '{}' from table '{}': {}",
3462                    index_name, table_uri, e
3463                ),
3464            })
3465        })?;
3466
3467        let transaction_id = dataset
3468            .read_transaction()
3469            .await
3470            .map_err(|e| {
3471                lance_core::Error::from(NamespaceError::Internal {
3472                    message: format!(
3473                        "Failed to read committed transaction after dropping index '{}' from '{}': {}",
3474                        index_name, table_uri, e
3475                    ),
3476                })
3477            })?
3478            .map(|transaction| transaction.uuid);
3479
3480        Ok(DropTableIndexResponse { transaction_id })
3481    }
3482
3483    async fn list_all_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
3484        // In dir-only mode there are no child namespaces, so all tables live in the
3485        // root directory. This is equivalent to listing the root namespace.
3486        let mut tables = self.list_directory_tables().await?;
3487        tables = self
3488            .filter_declared_tables(tables, request.include_declared.unwrap_or(true))
3489            .await?;
3490        Self::apply_pagination(&mut tables, request.page_token, request.limit);
3491        Ok(ListTablesResponse::new(tables))
3492    }
3493
3494    async fn restore_table(&self, request: RestoreTableRequest) -> Result<RestoreTableResponse> {
3495        let version = request.version;
3496        if version < 0 {
3497            return Err(Error::invalid_input_source(
3498                format!(
3499                    "Table version for restore_table must be non-negative, got {}",
3500                    version
3501                )
3502                .into(),
3503            ));
3504        }
3505
3506        let branch = Self::normalized_branch(request.branch.as_deref())?;
3507        let table_uri = self.resolve_table_location(&request.id).await?;
3508        let mut dataset = match branch {
3509            Some(branch) => self.open_validated_branch(&table_uri, branch).await?,
3510            None => self.load_dataset(&table_uri, None, "restore_table").await?,
3511        };
3512
3513        dataset = dataset
3514            .checkout_version(version as u64)
3515            .await
3516            .map_err(|e| {
3517                Error::namespace_source(
3518                    format!(
3519                        "Failed to checkout version {} for restore at '{}': {}",
3520                        version, table_uri, e
3521                    )
3522                    .into(),
3523                )
3524            })?;
3525
3526        dataset.restore().await.map_err(|e| {
3527            Error::namespace_source(
3528                format!(
3529                    "Failed to restore table at '{}' to version {}: {}",
3530                    table_uri, version, e
3531                )
3532                .into(),
3533            )
3534        })?;
3535
3536        let transaction_id = dataset
3537            .read_transaction()
3538            .await
3539            .map_err(|e| {
3540                Error::namespace_source(
3541                    format!(
3542                        "Failed to read transaction after restoring '{}': {}",
3543                        table_uri, e
3544                    )
3545                    .into(),
3546                )
3547            })?
3548            .map(|t| t.uuid);
3549
3550        Ok(RestoreTableResponse { transaction_id })
3551    }
3552
3553    async fn update_table_schema_metadata(
3554        &self,
3555        request: UpdateTableSchemaMetadataRequest,
3556    ) -> Result<UpdateTableSchemaMetadataResponse> {
3557        let table_uri = self.resolve_table_location(&request.id).await?;
3558        let mut dataset = self
3559            .load_dataset(&table_uri, None, "update_table_schema_metadata")
3560            .await?;
3561
3562        let new_metadata = request.metadata.unwrap_or_default();
3563        let updated_metadata = dataset
3564            .update_schema_metadata(new_metadata.iter().map(|(k, v)| (k.as_str(), v.as_str())))
3565            .await
3566            .map_err(|e| {
3567                Error::namespace_source(
3568                    format!(
3569                        "Failed to update schema metadata for table at '{}': {}",
3570                        table_uri, e
3571                    )
3572                    .into(),
3573                )
3574            })?;
3575
3576        let transaction_id = dataset
3577            .read_transaction()
3578            .await
3579            .map_err(|e| {
3580                Error::namespace_source(
3581                    format!(
3582                        "Failed to read transaction after updating metadata for '{}': {}",
3583                        table_uri, e
3584                    )
3585                    .into(),
3586                )
3587            })?
3588            .map(|t| t.uuid);
3589
3590        Ok(UpdateTableSchemaMetadataResponse {
3591            metadata: Some(updated_metadata),
3592            transaction_id,
3593        })
3594    }
3595
3596    async fn get_table_stats(
3597        &self,
3598        request: GetTableStatsRequest,
3599    ) -> Result<GetTableStatsResponse> {
3600        let table_uri = self.resolve_table_location(&request.id).await?;
3601        let dataset = Arc::new(
3602            self.load_dataset(&table_uri, None, "get_table_stats")
3603                .await?,
3604        );
3605
3606        // Compute total bytes on disk using field-level statistics
3607        let data_stats = dataset.calculate_data_stats().await.map_err(|e| {
3608            Error::namespace_source(
3609                format!(
3610                    "Failed to calculate data statistics for table at '{}': {}",
3611                    table_uri, e
3612                )
3613                .into(),
3614            )
3615        })?;
3616        let total_bytes: i64 = data_stats
3617            .fields
3618            .iter()
3619            .map(|f| f.bytes_on_disk as i64)
3620            .sum();
3621
3622        // Collect per-fragment row counts
3623        let fragment_row_futures: Vec<_> = dataset
3624            .get_fragments()
3625            .into_iter()
3626            .map(|f| async move { f.physical_rows().await })
3627            .collect();
3628        let fragment_row_results = futures::future::join_all(fragment_row_futures).await;
3629        let mut fragment_row_counts: Vec<i64> = fragment_row_results
3630            .into_iter()
3631            .filter_map(|r| r.ok())
3632            .map(|r| r as i64)
3633            .collect();
3634
3635        let num_fragments = fragment_row_counts.len() as i64;
3636        let num_rows: i64 = fragment_row_counts.iter().sum();
3637
3638        // Fragments with fewer rows than the compaction target are considered "small",
3639        // consistent with CompactionOptions::target_rows_per_fragment default.
3640        const SMALL_FRAGMENT_THRESHOLD: i64 = 1024 * 1024;
3641        let num_small_fragments = fragment_row_counts
3642            .iter()
3643            .filter(|&&r| r < SMALL_FRAGMENT_THRESHOLD)
3644            .count() as i64;
3645
3646        // Compute length summary statistics
3647        fragment_row_counts.sort_unstable();
3648        let lengths = if fragment_row_counts.is_empty() {
3649            FragmentSummary::new(0, 0, 0, 0, 0, 0, 0)
3650        } else {
3651            let len = fragment_row_counts.len();
3652            let min = fragment_row_counts[0];
3653            let max = fragment_row_counts[len - 1];
3654            let mean = num_rows / num_fragments;
3655            let pct = |p: f64| fragment_row_counts[((len - 1) as f64 * p) as usize];
3656            FragmentSummary::new(min, max, mean, pct(0.25), pct(0.50), pct(0.75), pct(0.99))
3657        };
3658
3659        // Count non-system indices
3660        let indices = dataset.load_indices().await.map_err(|e| {
3661            Error::namespace_source(
3662                format!("Failed to load indices for table at '{}': {}", table_uri, e).into(),
3663            )
3664        })?;
3665        let num_indices = indices.iter().filter(|m| !is_system_index(m)).count() as i64;
3666
3667        let fragment_stats = FragmentStats::new(num_fragments, num_small_fragments, lengths);
3668        Ok(GetTableStatsResponse::new(
3669            total_bytes,
3670            num_rows,
3671            num_indices,
3672            fragment_stats,
3673        ))
3674    }
3675
3676    async fn explain_table_query_plan(
3677        &self,
3678        request: ExplainTableQueryPlanRequest,
3679    ) -> Result<String> {
3680        let table_uri = self.resolve_table_location(&request.id).await?;
3681        let dataset = self
3682            .load_dataset(
3683                &table_uri,
3684                request.query.version,
3685                "explain_table_query_plan",
3686            )
3687            .await?;
3688        let verbose = request.verbose.unwrap_or(false);
3689
3690        let mut scanner = dataset.scan();
3691        Self::apply_query_params_to_scanner(
3692            &mut scanner,
3693            request.query.filter.as_deref(),
3694            request.query.columns.as_deref(),
3695            request.query.vector_column.as_deref(),
3696            &request.query.vector,
3697            request.query.k,
3698            request.query.offset,
3699            request.query.prefilter,
3700            request.query.bypass_vector_index,
3701            request.query.nprobes,
3702            request.query.ef,
3703            request.query.refine_factor,
3704            request.query.distance_type.as_deref(),
3705            request.query.fast_search,
3706            request.query.with_row_id,
3707            request.query.lower_bound,
3708            request.query.upper_bound,
3709            "explain_table_query_plan",
3710        )?;
3711
3712        scanner.explain_plan(verbose).await.map_err(|e| {
3713            Error::namespace_source(
3714                format!(
3715                    "Failed to explain query plan for table at '{}': {}",
3716                    table_uri, e
3717                )
3718                .into(),
3719            )
3720        })
3721    }
3722
3723    async fn analyze_table_query_plan(
3724        &self,
3725        request: AnalyzeTableQueryPlanRequest,
3726    ) -> Result<String> {
3727        let table_uri = self.resolve_table_location(&request.id).await?;
3728        let dataset = self
3729            .load_dataset(&table_uri, request.version, "analyze_table_query_plan")
3730            .await?;
3731
3732        let mut scanner = dataset.scan();
3733        Self::apply_query_params_to_scanner(
3734            &mut scanner,
3735            request.filter.as_deref(),
3736            request.columns.as_deref(),
3737            request.vector_column.as_deref(),
3738            &request.vector,
3739            request.k,
3740            request.offset,
3741            request.prefilter,
3742            request.bypass_vector_index,
3743            request.nprobes,
3744            request.ef,
3745            request.refine_factor,
3746            request.distance_type.as_deref(),
3747            request.fast_search,
3748            request.with_row_id,
3749            request.lower_bound,
3750            request.upper_bound,
3751            "analyze_table_query_plan",
3752        )?;
3753
3754        scanner.analyze_plan().await.map_err(|e| {
3755            Error::namespace_source(
3756                format!(
3757                    "Failed to analyze query plan for table at '{}': {}",
3758                    table_uri, e
3759                )
3760                .into(),
3761            )
3762        })
3763    }
3764
3765    async fn count_table_rows(&self, request: CountTableRowsRequest) -> Result<i64> {
3766        self.record_op("count_table_rows");
3767        let table_uri = self.resolve_table_location(&request.id).await?;
3768        let dataset = self
3769            .load_dataset(&table_uri, request.version, "count_table_rows")
3770            .await?;
3771
3772        let count =
3773            dataset
3774                .count_rows(request.predicate)
3775                .await
3776                .map_err(|e| NamespaceError::Internal {
3777                    message: format!("Failed to count rows for table at '{}': {:?}", table_uri, e),
3778                })?;
3779
3780        Ok(count as i64)
3781    }
3782
3783    async fn insert_into_table(
3784        &self,
3785        request: InsertIntoTableRequest,
3786        request_data: Bytes,
3787    ) -> Result<InsertIntoTableResponse> {
3788        self.record_op("insert_into_table");
3789        let table_uri = self.resolve_table_location(&request.id).await?;
3790        let (reader, _num_rows) =
3791            Self::ipc_reader_from_request_data(&request_data, "insert_into_table")?;
3792
3793        let mode = match request.mode.as_deref() {
3794            Some(m) if m.eq_ignore_ascii_case("overwrite") => WriteMode::Overwrite,
3795            Some(m) if m.eq_ignore_ascii_case("append") => WriteMode::Append,
3796            None => WriteMode::Append,
3797            Some(m) => {
3798                return Err(lance_namespace::error::NamespaceError::InvalidInput {
3799                    message: format!(
3800                        "Unsupported write mode '{}'. Supported modes are: 'append', 'overwrite'",
3801                        m
3802                    ),
3803                }
3804                .into());
3805            }
3806        };
3807
3808        if !self.table_uri_has_actual_manifests(&table_uri).await? {
3809            self.write_reader_to_table(&table_uri, reader, WriteMode::Create, None)
3810                .await?;
3811        } else {
3812            self.write_reader_to_table(&table_uri, reader, mode, None)
3813                .await?;
3814        }
3815
3816        Ok(InsertIntoTableResponse {
3817            transaction_id: None,
3818        })
3819    }
3820
3821    async fn merge_insert_into_table(
3822        &self,
3823        request: MergeInsertIntoTableRequest,
3824        request_data: Bytes,
3825    ) -> Result<MergeInsertIntoTableResponse> {
3826        self.record_op("merge_insert_into_table");
3827        let table_uri = self.resolve_table_location(&request.id).await?;
3828        let on = request.on.as_ref().ok_or_else(|| {
3829            lance_core::Error::from(NamespaceError::InvalidInput {
3830                message: "'on' field is required for merge_insert_into_table".to_string(),
3831            })
3832        })?;
3833
3834        let table_has_manifests = self.table_uri_has_actual_manifests(&table_uri).await?;
3835        let (reader, num_rows) =
3836            Self::ipc_reader_from_request_data(&request_data, "merge_insert_into_table")?;
3837
3838        if !table_has_manifests {
3839            let dataset = self
3840                .write_reader_to_table(&table_uri, reader, WriteMode::Create, None)
3841                .await?;
3842            let version = dataset.version().version as i64;
3843            return Ok(MergeInsertIntoTableResponse {
3844                transaction_id: None,
3845                num_updated_rows: Some(0),
3846                num_inserted_rows: Some(num_rows as i64),
3847                num_deleted_rows: Some(0),
3848                version: Some(version),
3849            });
3850        }
3851
3852        let dataset = Arc::new(
3853            self.load_dataset(&table_uri, None, "merge_insert_into_table")
3854                .await?,
3855        );
3856
3857        let mut merge_builder = MergeInsertBuilder::try_new(dataset.clone(), vec![on.clone()])
3858            .map_err(|e| {
3859                lance_core::Error::from(NamespaceError::InvalidInput {
3860                    message: format!("Failed to create merge_insert_into_table builder: {}", e),
3861                })
3862            })?;
3863
3864        if let Some(filter) = request.when_matched_update_all_filt.as_deref() {
3865            let behavior = WhenMatched::update_if(dataset.as_ref(), filter).map_err(|e| {
3866                lance_core::Error::from(NamespaceError::InvalidInput {
3867                    message: format!(
3868                        "Invalid when_matched_update_all_filt for merge_insert_into_table: {}",
3869                        e
3870                    ),
3871                })
3872            })?;
3873            merge_builder.when_matched(behavior);
3874        } else if request.when_matched_update_all.unwrap_or(false) {
3875            merge_builder.when_matched(WhenMatched::UpdateAll);
3876        }
3877
3878        if matches!(request.when_not_matched_insert_all, Some(false)) {
3879            merge_builder.when_not_matched(WhenNotMatched::DoNothing);
3880        } else {
3881            merge_builder.when_not_matched(WhenNotMatched::InsertAll);
3882        }
3883
3884        if let Some(filter) = request.when_not_matched_by_source_delete_filt.as_deref() {
3885            let behavior = WhenNotMatchedBySource::delete_if(dataset.as_ref(), filter).map_err(|e| {
3886                lance_core::Error::from(NamespaceError::InvalidInput {
3887                    message: format!(
3888                        "Invalid when_not_matched_by_source_delete_filt for merge_insert_into_table: {}",
3889                        e
3890                    ),
3891                })
3892            })?;
3893            merge_builder.when_not_matched_by_source(behavior);
3894        } else if request.when_not_matched_by_source_delete.unwrap_or(false) {
3895            merge_builder.when_not_matched_by_source(WhenNotMatchedBySource::Delete);
3896        }
3897
3898        if let Some(use_index) = request.use_index {
3899            merge_builder.use_index(use_index);
3900        }
3901
3902        let (dataset, stats) = merge_builder
3903            .try_build()
3904            .map_err(|e| {
3905                lance_core::Error::from(NamespaceError::InvalidInput {
3906                    message: format!("Failed to build merge_insert_into_table job: {}", e),
3907                })
3908            })?
3909            .execute_reader(reader)
3910            .await
3911            .map_err(|e| NamespaceError::Internal {
3912                message: format!(
3913                    "Failed to merge_insert_into_table at '{}': {}",
3914                    table_uri, e
3915                ),
3916            })?;
3917
3918        Ok(MergeInsertIntoTableResponse {
3919            transaction_id: None,
3920            num_updated_rows: Some(stats.num_updated_rows as i64),
3921            num_inserted_rows: Some(stats.num_inserted_rows as i64),
3922            num_deleted_rows: Some(stats.num_deleted_rows as i64),
3923            version: Some(dataset.version().version as i64),
3924        })
3925    }
3926
3927    async fn query_table(&self, request: QueryTableRequest) -> Result<Bytes> {
3928        use arrow::ipc::writer::FileWriter;
3929
3930        self.record_op("query_table");
3931        let table_uri = self.resolve_table_location(&request.id).await?;
3932        let dataset = self
3933            .load_dataset(&table_uri, request.version, "query_table")
3934            .await?;
3935
3936        // Build scanner
3937        let mut scanner = dataset.scan();
3938
3939        // Check if this is a vector search query
3940        // vector is Box<QueryTableRequestVector>, not Option
3941        let has_vector_query = request
3942            .vector
3943            .single_vector
3944            .as_ref()
3945            .map(|sv| !sv.is_empty())
3946            .unwrap_or(false)
3947            || request
3948                .vector
3949                .multi_vector
3950                .as_ref()
3951                .map(|mv| !mv.is_empty())
3952                .unwrap_or(false);
3953
3954        // Apply prefilter setting (must be set before nearest)
3955        if let Some(prefilter) = request.prefilter {
3956            scanner.prefilter(prefilter);
3957        }
3958
3959        // Apply vector search if query vector is provided
3960        if has_vector_query {
3961            let vector_column = request.vector_column.as_deref().unwrap_or("vector");
3962
3963            // Get the query vector(s)
3964            let query_vector: Vec<f32> = request
3965                .vector
3966                .single_vector
3967                .clone()
3968                .or_else(|| {
3969                    request
3970                        .vector
3971                        .multi_vector
3972                        .as_ref()
3973                        .and_then(|mv| mv.first().cloned())
3974                })
3975                .unwrap_or_default();
3976
3977            if !query_vector.is_empty() {
3978                let k = if request.k > 0 {
3979                    request.k as usize
3980                } else {
3981                    10
3982                };
3983                let query_array = Float32Array::from(query_vector);
3984                scanner
3985                    .nearest(vector_column, &query_array, k)
3986                    .map_err(|e| NamespaceError::InvalidInput {
3987                        message: format!("Invalid vector search: {:?}", e),
3988                    })?;
3989
3990                // Apply distance type if specified
3991                if let Some(ref distance_type) = request.distance_type {
3992                    let metric = match distance_type.to_lowercase().as_str() {
3993                        "l2" | "euclidean" => MetricType::L2,
3994                        "cosine" => MetricType::Cosine,
3995                        "dot" | "inner_product" => MetricType::Dot,
3996                        "hamming" => MetricType::Hamming,
3997                        _ => {
3998                            return Err(NamespaceError::InvalidInput {
3999                                message: format!("Unknown distance type: {}", distance_type),
4000                            }
4001                            .into());
4002                        }
4003                    };
4004                    scanner.distance_metric(metric);
4005                }
4006
4007                // Apply nprobes if specified (maps to minimum_nprobes, matching lancedb behavior)
4008                if let Some(nprobes) = request.nprobes {
4009                    scanner.minimum_nprobes(nprobes as usize);
4010                }
4011
4012                // Apply ef (HNSW search effort) if specified
4013                if let Some(ef) = request.ef {
4014                    scanner.ef(ef as usize);
4015                }
4016
4017                // Apply refine_factor if specified
4018                if let Some(refine_factor) = request.refine_factor {
4019                    scanner.refine(refine_factor as u32);
4020                }
4021
4022                // Apply distance bounds if specified
4023                if request.lower_bound.is_some() || request.upper_bound.is_some() {
4024                    scanner.distance_range(request.lower_bound, request.upper_bound);
4025                }
4026
4027                // Apply use_index (inverse of bypass_vector_index)
4028                if let Some(bypass) = request.bypass_vector_index {
4029                    scanner.use_index(!bypass);
4030                }
4031
4032                // Apply fast_search if specified
4033                if request.fast_search == Some(true) {
4034                    scanner.fast_search();
4035                }
4036            }
4037        }
4038
4039        // Apply full text search if specified
4040        if let Some(ref fts_query) = request.full_text_query {
4041            // Handle string_query (simple string FTS)
4042            if let Some(ref string_query) = fts_query.string_query {
4043                let mut fts = FullTextSearchQuery::new(string_query.query.clone());
4044
4045                // Apply column filter if specified
4046                if let Some(ref columns) = string_query.columns
4047                    && !columns.is_empty()
4048                {
4049                    fts = fts
4050                        .with_columns(columns)
4051                        .map_err(|e| NamespaceError::InvalidInput {
4052                            message: format!("Invalid FTS columns: {:?}", e),
4053                        })?;
4054                }
4055
4056                scanner
4057                    .full_text_search(fts)
4058                    .map_err(|e| NamespaceError::InvalidInput {
4059                        message: format!("Invalid full text search: {:?}", e),
4060                    })?;
4061            }
4062            // Note: structured_query would require more complex parsing
4063            // For now, we only support string_query
4064        }
4065
4066        // Apply column projection if specified
4067        if let Some(ref columns) = request.columns {
4068            if let Some(ref column_names) = columns.column_names
4069                && !column_names.is_empty()
4070            {
4071                scanner
4072                    .project(column_names)
4073                    .map_err(|e| NamespaceError::InvalidInput {
4074                        message: format!("Invalid column projection: {:?}", e),
4075                    })?;
4076            } else if let Some(ref column_aliases) = columns.column_aliases
4077                && !column_aliases.is_empty()
4078            {
4079                // column_aliases is HashMap<String, String> where key is alias, value is SQL expression
4080                let transform_pairs: Vec<(String, String)> = column_aliases
4081                    .iter()
4082                    .map(|(alias, sql)| (alias.clone(), sql.clone()))
4083                    .collect();
4084                scanner
4085                    .project_with_transform(
4086                        &transform_pairs
4087                            .iter()
4088                            .map(|(a, s)| (a.as_str(), s.as_str()))
4089                            .collect::<Vec<_>>(),
4090                    )
4091                    .map_err(|e| NamespaceError::InvalidInput {
4092                        message: format!("Invalid column alias expression: {:?}", e),
4093                    })?;
4094            }
4095        }
4096
4097        // Apply filter if specified
4098        if let Some(ref filter) = request.filter
4099            && !filter.is_empty()
4100        {
4101            scanner
4102                .filter(filter)
4103                .map_err(|e| NamespaceError::InvalidInput {
4104                    message: format!("Invalid filter expression: {:?}", e),
4105                })?;
4106        }
4107
4108        // Apply with_row_id if requested
4109        if request.with_row_id == Some(true) {
4110            scanner.with_row_id();
4111        }
4112
4113        // Apply limit if specified (k is the number of results to return)
4114        // k == 0 means no limit
4115        // Note: For vector search, limit is already applied via nearest()
4116        if !has_vector_query && request.k > 0 {
4117            let offset = request.offset.map(|o| o as i64);
4118            scanner.limit(Some(request.k as i64), offset).map_err(|e| {
4119                NamespaceError::InvalidInput {
4120                    message: format!("Invalid limit/offset: {:?}", e),
4121                }
4122            })?;
4123        } else if has_vector_query && request.offset.is_some() {
4124            // For vector search, offset is handled separately
4125            let offset = request.offset.map(|o| o as i64);
4126            scanner
4127                .limit(None, offset)
4128                .map_err(|e| NamespaceError::InvalidInput {
4129                    message: format!("Invalid offset: {:?}", e),
4130                })?;
4131        }
4132
4133        // Execute the scan and collect results
4134        let batch = scanner
4135            .try_into_batch()
4136            .await
4137            .map_err(|e| NamespaceError::Internal {
4138                message: format!("Failed to execute query: {:?}", e),
4139            })?;
4140
4141        // Serialize to Arrow IPC file format
4142        let schema = batch.schema();
4143        let mut buffer = Vec::new();
4144        {
4145            let mut writer = FileWriter::try_new(&mut buffer, &schema).map_err(|e| {
4146                NamespaceError::Internal {
4147                    message: format!("Failed to create IPC writer: {:?}", e),
4148                }
4149            })?;
4150            writer.write(&batch).map_err(|e| NamespaceError::Internal {
4151                message: format!("Failed to write batch to IPC: {:?}", e),
4152            })?;
4153            writer.finish().map_err(|e| NamespaceError::Internal {
4154                message: format!("Failed to finish IPC writer: {:?}", e),
4155            })?;
4156        }
4157
4158        Ok(Bytes::from(buffer))
4159    }
4160
4161    async fn list_table_tags(
4162        &self,
4163        request: ListTableTagsRequest,
4164    ) -> Result<ListTableTagsResponse> {
4165        self.record_op("list_table_tags");
4166        let table_uri = self.resolve_table_location(&request.id).await?;
4167        let dataset = self
4168            .load_dataset(&table_uri, None, "list_table_tags")
4169            .await?;
4170
4171        let raw_tags = dataset.tags().list().await.map_err(|e| {
4172            lance_core::Error::from(NamespaceError::Internal {
4173                message: format!("Failed to list tags for table at '{}': {}", table_uri, e),
4174            })
4175        })?;
4176
4177        let tags = raw_tags
4178            .into_iter()
4179            .map(|(name, contents)| {
4180                let mut tag_model =
4181                    ModelTagContents::new(contents.version as i64, contents.manifest_size as i64);
4182                tag_model.branch = contents.branch;
4183                (name, tag_model)
4184            })
4185            .collect();
4186
4187        Ok(ListTableTagsResponse {
4188            tags,
4189            page_token: None,
4190        })
4191    }
4192
4193    async fn get_table_tag_version(
4194        &self,
4195        request: GetTableTagVersionRequest,
4196    ) -> Result<GetTableTagVersionResponse> {
4197        self.record_op("get_table_tag_version");
4198        if request.tag.is_empty() {
4199            return Err(NamespaceError::InvalidInput {
4200                message: "tag name must not be empty for get_table_tag_version".to_string(),
4201            }
4202            .into());
4203        }
4204
4205        let table_uri = self.resolve_table_location(&request.id).await?;
4206        let dataset = self
4207            .load_dataset(&table_uri, None, "get_table_tag_version")
4208            .await?;
4209
4210        let contents = dataset
4211            .tags()
4212            .get(&request.tag)
4213            .await
4214            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4215
4216        Ok(GetTableTagVersionResponse {
4217            version: contents.version as i64,
4218            branch: contents.branch,
4219        })
4220    }
4221
4222    async fn create_table_tag(
4223        &self,
4224        request: CreateTableTagRequest,
4225    ) -> Result<CreateTableTagResponse> {
4226        self.record_op("create_table_tag");
4227        if request.tag.is_empty() {
4228            return Err(NamespaceError::InvalidInput {
4229                message: "tag name must not be empty for create_table_tag".to_string(),
4230            }
4231            .into());
4232        }
4233        if request.version <= 0 {
4234            return Err(NamespaceError::InvalidInput {
4235                message: format!(
4236                    "tag version must be a positive integer, got {} for create_table_tag",
4237                    request.version
4238                ),
4239            }
4240            .into());
4241        }
4242
4243        let table_uri = self.resolve_table_location(&request.id).await?;
4244        let dataset = self
4245            .load_dataset(&table_uri, None, "create_table_tag")
4246            .await?;
4247
4248        dataset
4249            .tags()
4250            .create(&request.tag, request.version as u64)
4251            .await
4252            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4253
4254        Ok(CreateTableTagResponse {
4255            transaction_id: None,
4256        })
4257    }
4258
4259    async fn delete_table_tag(
4260        &self,
4261        request: DeleteTableTagRequest,
4262    ) -> Result<DeleteTableTagResponse> {
4263        self.record_op("delete_table_tag");
4264        if request.tag.is_empty() {
4265            return Err(NamespaceError::InvalidInput {
4266                message: "tag name must not be empty for delete_table_tag".to_string(),
4267            }
4268            .into());
4269        }
4270
4271        let table_uri = self.resolve_table_location(&request.id).await?;
4272        let dataset = self
4273            .load_dataset(&table_uri, None, "delete_table_tag")
4274            .await?;
4275
4276        dataset
4277            .tags()
4278            .delete(&request.tag)
4279            .await
4280            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4281
4282        Ok(DeleteTableTagResponse {
4283            transaction_id: None,
4284        })
4285    }
4286
4287    async fn update_table_tag(
4288        &self,
4289        request: UpdateTableTagRequest,
4290    ) -> Result<UpdateTableTagResponse> {
4291        self.record_op("update_table_tag");
4292        if request.tag.is_empty() {
4293            return Err(NamespaceError::InvalidInput {
4294                message: "tag name must not be empty for update_table_tag".to_string(),
4295            }
4296            .into());
4297        }
4298        if request.version <= 0 {
4299            return Err(NamespaceError::InvalidInput {
4300                message: format!(
4301                    "tag version must be a positive integer, got {} for update_table_tag",
4302                    request.version
4303                ),
4304            }
4305            .into());
4306        }
4307
4308        let table_uri = self.resolve_table_location(&request.id).await?;
4309        let dataset = self
4310            .load_dataset(&table_uri, None, "update_table_tag")
4311            .await?;
4312
4313        dataset
4314            .tags()
4315            .update(&request.tag, request.version as u64)
4316            .await
4317            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4318
4319        Ok(UpdateTableTagResponse {
4320            transaction_id: None,
4321        })
4322    }
4323
4324    async fn create_table_branch(
4325        &self,
4326        request: CreateTableBranchRequest,
4327    ) -> Result<CreateTableBranchResponse> {
4328        self.record_op("create_table_branch");
4329        if request.name.is_empty() {
4330            return Err(NamespaceError::InvalidInput {
4331                message: "branch name must not be empty for create_table_branch".to_string(),
4332            }
4333            .into());
4334        }
4335        let from_version = match request.from_version {
4336            Some(v) if v <= 0 => {
4337                return Err(NamespaceError::InvalidInput {
4338                    message: format!(
4339                        "from_version must be a positive integer, got {} for create_table_branch",
4340                        v
4341                    ),
4342                }
4343                .into());
4344            }
4345            Some(v) => Some(v as u64),
4346            None => None,
4347        };
4348
4349        let table_uri = self.resolve_table_location(&request.id).await?;
4350        let mut dataset = self
4351            .load_dataset(&table_uri, None, "create_table_branch")
4352            .await?;
4353
4354        // Best-effort pre-check: a duplicate returns a clean TableBranchAlreadyExists conflict
4355        // instead of the opaque Internal error create_branch raises on a pre-existing branch. A
4356        // concurrent create can still race past this window. Remove once lance-core create_branch
4357        // returns RefConflict up front.
4358        if dataset.branches().get(&request.name).await.is_ok() {
4359            return Err(NamespaceError::TableBranchAlreadyExists {
4360                message: format!("branch '{}' for table at '{}'", request.name, table_uri),
4361            }
4362            .into());
4363        }
4364
4365        dataset
4366            .create_branch(
4367                &request.name,
4368                (request.from_branch.as_deref(), from_version),
4369                None,
4370            )
4371            .await
4372            .map_err(|e| {
4373                // After load_dataset + the dup pre-check, a DatasetNotFound from create_branch
4374                // means the requested fork source (from_branch/from_version) doesn't exist.
4375                if matches!(e, lance_core::Error::DatasetNotFound { .. }) {
4376                    NamespaceError::InvalidInput {
4377                        message: format!(
4378                            "from_branch/from_version for branch '{}' refers to a source that does not exist: {}",
4379                            request.name, e
4380                        ),
4381                    }
4382                    .into()
4383                } else {
4384                    Self::map_branch_error(e, &request.name, &table_uri)
4385                }
4386            })?;
4387
4388        Ok(CreateTableBranchResponse {
4389            transaction_id: None,
4390        })
4391    }
4392
4393    async fn list_table_branches(
4394        &self,
4395        request: ListTableBranchesRequest,
4396    ) -> Result<ListTableBranchesResponse> {
4397        self.record_op("list_table_branches");
4398        let table_uri = self.resolve_table_location(&request.id).await?;
4399        let dataset = self
4400            .load_dataset(&table_uri, None, "list_table_branches")
4401            .await?;
4402
4403        let raw_branches = dataset.list_branches().await.map_err(|e| {
4404            lance_core::Error::from(NamespaceError::Internal {
4405                message: format!(
4406                    "Failed to list branches for table at '{}': {}",
4407                    table_uri, e
4408                ),
4409            })
4410        })?;
4411
4412        let branches = raw_branches
4413            .into_iter()
4414            .map(|(name, contents)| {
4415                // The namespace `BranchContents` model has no `identifier` field, so the
4416                // lance-core branch identifier is intentionally dropped here.
4417                let mut branch_model = ModelBranchContents::new(
4418                    contents.parent_version as i64,
4419                    contents.create_at as i64,
4420                    contents.manifest_size as i64,
4421                );
4422                branch_model.parent_branch = contents.parent_branch;
4423                branch_model.metadata = if contents.metadata.is_empty() {
4424                    None
4425                } else {
4426                    Some(contents.metadata)
4427                };
4428                (name, branch_model)
4429            })
4430            .collect();
4431
4432        Ok(ListTableBranchesResponse {
4433            branches,
4434            page_token: None,
4435        })
4436    }
4437
4438    async fn delete_table_branch(
4439        &self,
4440        request: DeleteTableBranchRequest,
4441    ) -> Result<DeleteTableBranchResponse> {
4442        self.record_op("delete_table_branch");
4443        if request.name.is_empty() {
4444            return Err(NamespaceError::InvalidInput {
4445                message: "branch name must not be empty for delete_table_branch".to_string(),
4446            }
4447            .into());
4448        }
4449
4450        let table_uri = self.resolve_table_location(&request.id).await?;
4451        let mut dataset = self
4452            .load_dataset(&table_uri, None, "delete_table_branch")
4453            .await?;
4454
4455        dataset
4456            .delete_branch(&request.name)
4457            .await
4458            .map_err(|e| match e {
4459                lance_core::Error::RefConflict { message } => NamespaceError::InvalidInput {
4460                    message: format!(
4461                        "branch '{}' for table at '{}': {}",
4462                        request.name, table_uri, message
4463                    ),
4464                }
4465                .into(),
4466                other => Self::map_branch_error(other, &request.name, &table_uri),
4467            })?;
4468
4469        Ok(DeleteTableBranchResponse {
4470            transaction_id: None,
4471        })
4472    }
4473
4474    fn namespace_id(&self) -> String {
4475        format!("DirectoryNamespace {{ root: {:?} }}", self.root)
4476    }
4477}
4478
4479#[cfg(test)]
4480mod tests {
4481    use super::*;
4482    use arrow_ipc::reader::{FileReader, StreamReader};
4483    use lance::dataset::Dataset;
4484    use lance::index::DatasetIndexExt;
4485    use lance_core::utils::tempfile::{TempStdDir, TempStrDir};
4486    use lance_core::utils::testing::CountingObjectStore;
4487    use lance_io::object_store::{providers::local::FileStoreProvider, uri_to_url};
4488    use lance_namespace::error::ErrorCode;
4489    use lance_namespace::models::{
4490        CreateTableRequest, JsonArrowDataType, JsonArrowField, JsonArrowSchema, ListTablesRequest,
4491        QueryTableRequestColumns,
4492    };
4493    use lance_namespace::schema::convert_json_arrow_schema;
4494    use std::io::Cursor;
4495    use std::sync::{
4496        Arc,
4497        atomic::{AtomicUsize, Ordering},
4498    };
4499    use url::Url;
4500
4501    fn assert_plan_contains_all(plan: &str, expected_fragments: &[&str], context: &str) {
4502        for expected_fragment in expected_fragments {
4503            assert!(
4504                plan.contains(expected_fragment),
4505                "{}. Missing fragment: '{}'. Plan:\n{}",
4506                context,
4507                expected_fragment,
4508                plan
4509            );
4510        }
4511    }
4512
4513    /// Helper to create a test DirectoryNamespace with a temporary directory
4514    async fn create_test_namespace() -> (DirectoryNamespace, TempStdDir) {
4515        let temp_dir = TempStdDir::default();
4516
4517        let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
4518            .build()
4519            .await
4520            .unwrap();
4521        (namespace, temp_dir)
4522    }
4523
4524    #[derive(Debug)]
4525    struct CountingFileStoreProvider {
4526        listing_count: Arc<AtomicUsize>,
4527    }
4528
4529    #[async_trait]
4530    impl lance_io::object_store::ObjectStoreProvider for CountingFileStoreProvider {
4531        async fn new_store(
4532            &self,
4533            base_path: Url,
4534            params: &ObjectStoreParams,
4535        ) -> Result<ObjectStore> {
4536            let provider = FileStoreProvider;
4537            let mut store = provider.new_store(base_path, params).await?;
4538            store.inner = Arc::new(CountingObjectStore::new(
4539                store.inner.clone(),
4540                self.listing_count.clone(),
4541            ));
4542            Ok(store)
4543        }
4544
4545        fn extract_path(&self, url: &Url) -> Result<Path> {
4546            let provider = FileStoreProvider;
4547            provider.extract_path(url)
4548        }
4549
4550        fn calculate_object_store_prefix(
4551            &self,
4552            url: &Url,
4553            storage_options: Option<&HashMap<String, String>>,
4554        ) -> Result<String> {
4555            let provider = FileStoreProvider;
4556            provider.calculate_object_store_prefix(url, storage_options)
4557        }
4558    }
4559
4560    fn file_object_store_uri(path: &str) -> String {
4561        let file_url = uri_to_url(path).unwrap();
4562        let mut url = Url::parse("file-object-store:///").unwrap();
4563        url.set_path(file_url.path());
4564        url.to_string()
4565    }
4566
4567    fn build_listing_counting_session(listing_count: Arc<AtomicUsize>) -> Arc<Session> {
4568        let registry = Arc::new(ObjectStoreRegistry::default());
4569        registry.insert(
4570            "file-object-store",
4571            Arc::new(CountingFileStoreProvider { listing_count }),
4572        );
4573        Arc::new(Session::new(0, 0, registry))
4574    }
4575
4576    /// Helper to create test IPC data from a schema
4577    fn create_test_ipc_data(schema: &JsonArrowSchema) -> Vec<u8> {
4578        use arrow::ipc::writer::StreamWriter;
4579
4580        let arrow_schema = convert_json_arrow_schema(schema).unwrap();
4581        let arrow_schema = Arc::new(arrow_schema);
4582        let batch = arrow::record_batch::RecordBatch::new_empty(arrow_schema.clone());
4583        let mut buffer = Vec::new();
4584        {
4585            let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
4586            writer.write(&batch).unwrap();
4587            writer.finish().unwrap();
4588        }
4589        buffer
4590    }
4591
4592    fn create_ipc_data_from_batches(
4593        schema: Arc<arrow_schema::Schema>,
4594        batches: Vec<arrow::record_batch::RecordBatch>,
4595    ) -> Vec<u8> {
4596        use arrow::ipc::writer::StreamWriter;
4597
4598        let mut buffer = Vec::new();
4599        {
4600            let mut writer = StreamWriter::try_new(&mut buffer, &schema).unwrap();
4601            for batch in &batches {
4602                writer.write(batch).unwrap();
4603            }
4604            writer.finish().unwrap();
4605        }
4606        buffer
4607    }
4608
4609    fn create_non_empty_test_ipc_data() -> Vec<u8> {
4610        use arrow::array::{Int32Array, StringArray};
4611        use arrow::record_batch::RecordBatch;
4612
4613        let schema = Arc::new(convert_json_arrow_schema(&create_test_schema()).unwrap());
4614        let batch = RecordBatch::try_new(
4615            schema.clone(),
4616            vec![
4617                Arc::new(Int32Array::from(vec![1, 2])),
4618                Arc::new(StringArray::from(vec![Some("alice"), Some("bob")])),
4619            ],
4620        )
4621        .unwrap();
4622        create_ipc_data_from_batches(schema, vec![batch])
4623    }
4624
4625    fn create_single_row_test_ipc_data() -> Vec<u8> {
4626        use arrow::array::{Int32Array, StringArray};
4627        use arrow::record_batch::RecordBatch;
4628
4629        let schema = Arc::new(convert_json_arrow_schema(&create_test_schema()).unwrap());
4630        let batch = RecordBatch::try_new(
4631            schema.clone(),
4632            vec![
4633                Arc::new(Int32Array::from(vec![10])),
4634                Arc::new(StringArray::from(vec![Some("carol")])),
4635            ],
4636        )
4637        .unwrap();
4638        create_ipc_data_from_batches(schema, vec![batch])
4639    }
4640
4641    /// Helper to create a simple test schema
4642    fn create_test_schema() -> JsonArrowSchema {
4643        let int_type = JsonArrowDataType::new("int32".to_string());
4644        let string_type = JsonArrowDataType::new("utf8".to_string());
4645
4646        let id_field = JsonArrowField {
4647            name: "id".to_string(),
4648            r#type: Box::new(int_type),
4649            nullable: false,
4650            metadata: None,
4651        };
4652
4653        let name_field = JsonArrowField {
4654            name: "name".to_string(),
4655            r#type: Box::new(string_type),
4656            nullable: true,
4657            metadata: None,
4658        };
4659
4660        JsonArrowSchema {
4661            fields: vec![id_field, name_field],
4662            metadata: None,
4663        }
4664    }
4665
4666    fn create_scalar_table_ipc_data() -> Vec<u8> {
4667        use arrow::array::{Int32Array, StringArray};
4668        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
4669
4670        let schema = Arc::new(ArrowSchema::new(vec![
4671            Field::new("id", DataType::Int32, false),
4672            Field::new("name", DataType::Utf8, true),
4673        ]));
4674        let batch = arrow::record_batch::RecordBatch::try_new(
4675            schema.clone(),
4676            vec![
4677                Arc::new(Int32Array::from(vec![1, 2, 3])),
4678                Arc::new(StringArray::from(vec!["alice", "bob", "cory"])),
4679            ],
4680        )
4681        .unwrap();
4682        create_ipc_data_from_batches(schema, vec![batch])
4683    }
4684
4685    fn create_vector_table_ipc_data() -> Vec<u8> {
4686        use arrow::array::{FixedSizeListArray, Float32Array, Int32Array};
4687        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
4688
4689        let schema = Arc::new(ArrowSchema::new(vec![
4690            Field::new("id", DataType::Int32, false),
4691            Field::new(
4692                "vector",
4693                DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 2),
4694                true,
4695            ),
4696        ]));
4697        let vector_field = Arc::new(Field::new("item", DataType::Float32, true));
4698        let vectors = FixedSizeListArray::try_new(
4699            vector_field,
4700            2,
4701            Arc::new(Float32Array::from(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6])),
4702            None,
4703        )
4704        .unwrap();
4705        let batch = arrow::record_batch::RecordBatch::try_new(
4706            schema.clone(),
4707            vec![Arc::new(Int32Array::from(vec![1, 2, 3])), Arc::new(vectors)],
4708        )
4709        .unwrap();
4710        create_ipc_data_from_batches(schema, vec![batch])
4711    }
4712
4713    async fn create_scalar_table(namespace: &DirectoryNamespace, table_name: &str) {
4714        let mut create_table_request = CreateTableRequest::new();
4715        create_table_request.id = Some(vec![table_name.to_string()]);
4716        namespace
4717            .create_table(
4718                create_table_request,
4719                Bytes::from(create_scalar_table_ipc_data()),
4720            )
4721            .await
4722            .unwrap();
4723    }
4724
4725    async fn create_vector_table(namespace: &DirectoryNamespace, table_name: &str) {
4726        let mut create_table_request = CreateTableRequest::new();
4727        create_table_request.id = Some(vec![table_name.to_string()]);
4728        namespace
4729            .create_table(
4730                create_table_request,
4731                Bytes::from(create_vector_table_ipc_data()),
4732            )
4733            .await
4734            .unwrap();
4735    }
4736
4737    async fn open_dataset(namespace: &DirectoryNamespace, table_name: &str) -> Dataset {
4738        let mut describe_request = DescribeTableRequest::new();
4739        describe_request.id = Some(vec![table_name.to_string()]);
4740        let table_uri = namespace
4741            .describe_table(describe_request)
4742            .await
4743            .unwrap()
4744            .location
4745            .expect("table location should exist");
4746        Dataset::open(&table_uri).await.unwrap()
4747    }
4748
4749    async fn create_scalar_index(
4750        namespace: &DirectoryNamespace,
4751        table_name: &str,
4752        index_name: &str,
4753    ) -> Option<String> {
4754        use lance_namespace::models::CreateTableIndexRequest;
4755
4756        let mut create_index_request =
4757            CreateTableIndexRequest::new("id".to_string(), "BTREE".to_string());
4758        create_index_request.id = Some(vec![table_name.to_string()]);
4759        create_index_request.name = Some(index_name.to_string());
4760        namespace
4761            .create_table_scalar_index(create_index_request)
4762            .await
4763            .unwrap()
4764            .transaction_id
4765    }
4766
4767    /// Fork `branch_name` from the table's current version and append
4768    /// `extra_versions` commits to it (each a new version on the branch, written
4769    /// with the default V2 naming). The main branch is left untouched. Returns
4770    /// the branch's storage URI (`<root>/tree/<branch>`).
4771    async fn create_branch_with_commits(
4772        namespace: &DirectoryNamespace,
4773        table_name: &str,
4774        branch_name: &str,
4775        extra_versions: usize,
4776    ) -> String {
4777        let mut main = open_dataset(namespace, table_name).await;
4778        let fork_version = main.version().version;
4779        let branch = main
4780            .create_branch(branch_name, fork_version, None)
4781            .await
4782            .unwrap();
4783        let branch_uri = branch.uri().to_string();
4784        for i in 0..extra_versions {
4785            append_scalar_version(&branch_uri, (i as i32 + 1) * 100).await;
4786        }
4787        branch_uri
4788    }
4789
4790    /// Append one scalar-schema batch to the dataset at `uri`, creating a new
4791    /// version (default V2 naming). Shared by branch and main chain setup.
4792    async fn append_scalar_version(uri: &str, seed: i32) {
4793        use arrow::array::{Int32Array, StringArray};
4794        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
4795        let schema = Arc::new(ArrowSchema::new(vec![
4796            Field::new("id", DataType::Int32, false),
4797            Field::new("name", DataType::Utf8, true),
4798        ]));
4799        let batch = arrow::record_batch::RecordBatch::try_new(
4800            schema.clone(),
4801            vec![
4802                Arc::new(Int32Array::from(vec![seed, seed + 1])),
4803                Arc::new(StringArray::from(vec![Some("x"), Some("y")])),
4804            ],
4805        )
4806        .unwrap();
4807        let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
4808        Dataset::write(
4809            reader,
4810            uri,
4811            Some(WriteParams {
4812                mode: WriteMode::Append,
4813                ..Default::default()
4814            }),
4815        )
4816        .await
4817        .unwrap();
4818    }
4819
4820    /// List a table's versions on `branch` (None == main) via the namespace.
4821    async fn list_versions(
4822        namespace: &DirectoryNamespace,
4823        table_name: &str,
4824        branch: Option<&str>,
4825    ) -> Result<Vec<TableVersion>> {
4826        let req = ListTableVersionsRequest {
4827            id: Some(vec![table_name.to_string()]),
4828            branch: branch.map(|b| b.to_string()),
4829            ..Default::default()
4830        };
4831        namespace.list_table_versions(req).await.map(|r| r.versions)
4832    }
4833
4834    #[tokio::test]
4835    async fn test_list_table_versions_on_branch() {
4836        let (namespace, _temp_dir) = create_test_namespace().await;
4837        create_scalar_table(&namespace, "users").await;
4838        create_branch_with_commits(&namespace, "users", "exp", 2).await;
4839
4840        // The branch lists its own chain, and every version resolves to a
4841        // manifest under the branch's tree path.
4842        let branch_versions = list_versions(&namespace, "users", Some("exp"))
4843            .await
4844            .unwrap();
4845        assert!(branch_versions.len() >= 2);
4846        assert!(
4847            branch_versions
4848                .iter()
4849                .all(|v| v.manifest_path.contains("tree/exp")),
4850            "branch versions must resolve to branch manifests: {:?}",
4851            branch_versions
4852        );
4853
4854        // Unset and "main" behave identically and never see the tree path.
4855        let main_versions = list_versions(&namespace, "users", None).await.unwrap();
4856        let main_explicit = list_versions(&namespace, "users", Some("main"))
4857            .await
4858            .unwrap();
4859        assert_eq!(main_versions.len(), main_explicit.len());
4860        assert!(
4861            main_versions
4862                .iter()
4863                .all(|v| !v.manifest_path.contains("tree/"))
4864        );
4865
4866        // A non-existent branch is a clean not-found, not an empty list.
4867        let missing = list_versions(&namespace, "users", Some("does-not-exist")).await;
4868        assert!(missing.is_err());
4869        assert!(missing.unwrap_err().to_string().contains("not found"));
4870    }
4871
4872    #[tokio::test]
4873    async fn test_describe_table_version_on_branch() {
4874        let (namespace, _temp_dir) = create_test_namespace().await;
4875        create_scalar_table(&namespace, "users").await;
4876        create_branch_with_commits(&namespace, "users", "exp", 2).await;
4877
4878        let branch_versions = list_versions(&namespace, "users", Some("exp"))
4879            .await
4880            .unwrap();
4881        let latest = branch_versions.iter().map(|v| v.version).max().unwrap();
4882
4883        // Describe latest on the branch returns the branch's manifest_path.
4884        let req = DescribeTableVersionRequest {
4885            id: Some(vec!["users".to_string()]),
4886            branch: Some("exp".to_string()),
4887            ..Default::default()
4888        };
4889        let resp = namespace.describe_table_version(req).await.unwrap();
4890        assert_eq!(resp.version.version, latest);
4891        assert!(resp.version.manifest_path.contains("tree/exp"));
4892
4893        // A specific existing branch version resolves.
4894        let req = DescribeTableVersionRequest {
4895            id: Some(vec!["users".to_string()]),
4896            version: Some(latest),
4897            branch: Some("exp".to_string()),
4898            ..Default::default()
4899        };
4900        assert!(namespace.describe_table_version(req).await.is_ok());
4901
4902        // A version absent on the branch is not found.
4903        let req = DescribeTableVersionRequest {
4904            id: Some(vec!["users".to_string()]),
4905            version: Some(999_999),
4906            branch: Some("exp".to_string()),
4907            ..Default::default()
4908        };
4909        assert!(namespace.describe_table_version(req).await.is_err());
4910
4911        // A non-existent branch is not found.
4912        let req = DescribeTableVersionRequest {
4913            id: Some(vec!["users".to_string()]),
4914            branch: Some("nope".to_string()),
4915            ..Default::default()
4916        };
4917        let err = namespace.describe_table_version(req).await;
4918        assert!(err.is_err() && err.unwrap_err().to_string().contains("not found"));
4919    }
4920
4921    #[tokio::test]
4922    async fn test_restore_table_on_branch() {
4923        use lance_namespace::models::RestoreTableRequest;
4924
4925        let (namespace, _temp_dir) = create_test_namespace().await;
4926        create_scalar_table(&namespace, "users").await;
4927        create_branch_with_commits(&namespace, "users", "exp", 2).await;
4928
4929        let before = list_versions(&namespace, "users", Some("exp"))
4930            .await
4931            .unwrap();
4932        let branch_latest = before.iter().map(|v| v.version).max().unwrap();
4933        let earliest = before.iter().map(|v| v.version).min().unwrap();
4934        let main_before = list_versions(&namespace, "users", None)
4935            .await
4936            .unwrap()
4937            .len();
4938
4939        // Restoring the branch to an earlier version commits a NEW version on
4940        // the branch (restore is itself a commit), and must not touch main.
4941        let req = RestoreTableRequest {
4942            id: Some(vec!["users".to_string()]),
4943            version: earliest,
4944            branch: Some("exp".to_string()),
4945            ..Default::default()
4946        };
4947        let resp = namespace.restore_table(req).await.unwrap();
4948        assert!(resp.transaction_id.is_some());
4949
4950        let after = list_versions(&namespace, "users", Some("exp"))
4951            .await
4952            .unwrap();
4953        let new_latest = after.iter().map(|v| v.version).max().unwrap();
4954        assert!(
4955            new_latest > branch_latest,
4956            "restore should add a branch version"
4957        );
4958
4959        let main_after = list_versions(&namespace, "users", None)
4960            .await
4961            .unwrap()
4962            .len();
4963        assert_eq!(main_after, main_before, "main must be unaffected");
4964    }
4965
4966    #[tokio::test]
4967    async fn test_batch_delete_table_versions_on_branch() {
4968        use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
4969
4970        let (namespace, _temp_dir) = create_test_namespace().await;
4971        create_scalar_table(&namespace, "users").await;
4972        create_branch_with_commits(&namespace, "users", "exp", 2).await;
4973
4974        let before = list_versions(&namespace, "users", Some("exp"))
4975            .await
4976            .unwrap();
4977        let main_before = list_versions(&namespace, "users", None).await.unwrap();
4978
4979        // Delete the branch's whole history with a through-latest range (end = -1).
4980        // The branch manifests use V2 naming (inverted, zero-padded), so a nonzero
4981        // deleted_count proves the V2 fix: the old code constructed
4982        // "{version}.manifest" and silently matched nothing.
4983        let req = BatchDeleteTableVersionsRequest {
4984            id: Some(vec!["users".to_string()]),
4985            branch: Some("exp".to_string()),
4986            ranges: vec![VersionRange::new(0, -1)],
4987            ..Default::default()
4988        };
4989        let resp = namespace.batch_delete_table_versions(req).await.unwrap();
4990        assert_eq!(
4991            resp.deleted_count,
4992            Some(before.len() as i64),
4993            "every branch manifest should be physically deleted"
4994        );
4995
4996        // The emptied branch now reads as not-found, and main is untouched.
4997        assert!(
4998            list_versions(&namespace, "users", Some("exp"))
4999                .await
5000                .is_err()
5001        );
5002        let main_after = list_versions(&namespace, "users", None).await.unwrap();
5003        assert_eq!(
5004            main_after.len(),
5005            main_before.len(),
5006            "main must be untouched"
5007        );
5008    }
5009
5010    #[tokio::test]
5011    async fn test_create_table_version_on_branch() {
5012        use futures::TryStreamExt;
5013        use lance_namespace::models::CreateTableVersionRequest;
5014
5015        let (namespace, _temp_dir) = create_test_namespace().await;
5016        create_scalar_table(&namespace, "users").await;
5017        let branch_uri = create_branch_with_commits(&namespace, "users", "exp", 1).await;
5018
5019        // Stage a manifest by copying one of the branch's existing manifests.
5020        let branch_ds = Dataset::open(&branch_uri).await.unwrap();
5021        let versions_dir = branch_ds.versions_dir();
5022        let store = branch_ds.object_store(None).await.unwrap();
5023        let existing = store
5024            .inner
5025            .list(Some(&versions_dir))
5026            .try_collect::<Vec<_>>()
5027            .await
5028            .unwrap()
5029            .into_iter()
5030            .find(|m| {
5031                m.location
5032                    .filename()
5033                    .map(|f| f.ends_with(".manifest"))
5034                    .unwrap_or(false)
5035            })
5036            .expect("a branch manifest");
5037        let bytes = store
5038            .inner
5039            .get(&existing.location)
5040            .await
5041            .unwrap()
5042            .bytes()
5043            .await
5044            .unwrap();
5045        let staging = versions_dir.join("staging_manifest");
5046        store.inner.put(&staging, bytes.into()).await.unwrap();
5047
5048        let main_before = list_versions(&namespace, "users", None)
5049            .await
5050            .unwrap()
5051            .len();
5052        let new_version = list_versions(&namespace, "users", Some("exp"))
5053            .await
5054            .unwrap()
5055            .iter()
5056            .map(|v| v.version)
5057            .max()
5058            .unwrap()
5059            + 1;
5060
5061        let req = CreateTableVersionRequest {
5062            id: Some(vec!["users".to_string()]),
5063            version: new_version,
5064            manifest_path: staging.to_string(),
5065            naming_scheme: Some("V2".to_string()),
5066            branch: Some("exp".to_string()),
5067            ..Default::default()
5068        };
5069        let resp = namespace.create_table_version(req).await.unwrap();
5070        let info = resp.version.expect("version info");
5071        // The new manifest must land under the branch's tree path.
5072        assert!(
5073            info.manifest_path.contains("tree/exp"),
5074            "got {}",
5075            info.manifest_path
5076        );
5077
5078        // It is visible on the branch, and main did not gain a version.
5079        let after = list_versions(&namespace, "users", Some("exp"))
5080            .await
5081            .unwrap();
5082        assert!(after.iter().any(|v| v.version == new_version));
5083        let main_after = list_versions(&namespace, "users", None)
5084            .await
5085            .unwrap()
5086            .len();
5087        assert_eq!(main_after, main_before, "main must be unaffected");
5088    }
5089
5090    /// The namespace-managed commit store derives the branch a request targets
5091    /// from the base path it is handed, so a single store serves every branch of
5092    /// the table: a branch-qualified base resolves and commits against the
5093    /// branch chain while the table root targets main.
5094    #[tokio::test]
5095    async fn test_external_manifest_store_resolves_branch_from_base_path() {
5096        use futures::TryStreamExt;
5097        use lance::io::commit::namespace_manifest::LanceNamespaceExternalManifestStore;
5098        use lance_table::io::commit::external_manifest::ExternalManifestStore;
5099
5100        let (namespace, _temp_dir) = create_test_namespace().await;
5101        create_scalar_table(&namespace, "users").await; // main: version 1
5102        let branch_uri = create_branch_with_commits(&namespace, "users", "exp", 2).await;
5103
5104        let namespace = Arc::new(namespace);
5105        let table_id = vec!["users".to_string()];
5106        let branch_ds = Dataset::open(&branch_uri).await.unwrap();
5107        let branch_base = branch_ds.branch_location().path;
5108        let root_base = branch_ds.branch_location().find_main().unwrap().path;
5109        let store = LanceNamespaceExternalManifestStore::new(
5110            namespace.clone(),
5111            table_id.clone(),
5112            root_base.clone(),
5113        );
5114
5115        // The branch-qualified base resolves the branch chain, the root base
5116        // resolves main: proof the base path reaches list_table_versions.
5117        let (branch_latest, branch_path) = store
5118            .get_latest_version(branch_base.as_ref())
5119            .await
5120            .unwrap()
5121            .expect("branch has versions");
5122        let (_main_latest, main_path) = store
5123            .get_latest_version(root_base.as_ref())
5124            .await
5125            .unwrap()
5126            .expect("main has versions");
5127        assert!(
5128            branch_path.contains("tree/exp"),
5129            "branch latest must resolve to the branch tree: {}",
5130            branch_path
5131        );
5132        assert!(
5133            !main_path.contains("tree/exp"),
5134            "main latest must not resolve to a branch tree: {}",
5135            main_path
5136        );
5137
5138        // describe (get) with the branch base also resolves to the branch tree.
5139        let described = store
5140            .get(branch_base.as_ref(), branch_latest)
5141            .await
5142            .unwrap();
5143        assert!(
5144            described.contains("tree/exp"),
5145            "describe on the branch must resolve to the branch tree: {}",
5146            described
5147        );
5148
5149        // A base that is neither the root nor a branch chain is rejected.
5150        assert!(store.get_latest_version("somewhere/else").await.is_err());
5151
5152        // Commit (put) with the branch base: the new version must land on the
5153        // branch chain. Stage a manifest by copying an existing branch manifest.
5154        let versions_dir = branch_ds.versions_dir();
5155        let obj = branch_ds.object_store(None).await.unwrap();
5156        let existing = obj
5157            .inner
5158            .list(Some(&versions_dir))
5159            .try_collect::<Vec<_>>()
5160            .await
5161            .unwrap()
5162            .into_iter()
5163            .find(|m| {
5164                m.location
5165                    .filename()
5166                    .map(|f| f.ends_with(".manifest"))
5167                    .unwrap_or(false)
5168            })
5169            .expect("a branch manifest");
5170        let bytes = obj
5171            .inner
5172            .get(&existing.location)
5173            .await
5174            .unwrap()
5175            .bytes()
5176            .await
5177            .unwrap();
5178        let size = bytes.len() as u64;
5179        let staging = versions_dir.clone().join("staging_manifest");
5180        obj.inner.put(&staging, bytes.into()).await.unwrap();
5181
5182        let committed = store
5183            .put(
5184                &branch_base,
5185                branch_latest + 1,
5186                &staging,
5187                size,
5188                None,
5189                obj.inner.as_ref(),
5190                ManifestNamingScheme::V2,
5191            )
5192            .await
5193            .unwrap();
5194        assert!(
5195            committed.path.to_string().contains("tree/exp"),
5196            "a commit through a branch-qualified base must land on the branch tree: {}",
5197            committed.path
5198        );
5199    }
5200
5201    /// write_into_namespace_on_branch must append against the branch chain
5202    /// THROUGH the managed commit handler: the version is registered with the
5203    /// namespace (create_table_version), lands on the branch tree, and main's
5204    /// catalog is untouched. The ops-metrics assertions exist because a
5205    /// physical-only commit is invisible to DirectoryNamespace branch listing
5206    /// (it lists storage), while a catalog-authoritative namespace would
5207    /// silently lose the version.
5208    #[tokio::test]
5209    async fn test_write_into_namespace_on_branch_appends_to_branch() {
5210        use lance::dataset::builder::DatasetBuilder;
5211        use lance_namespace::models::CreateTableBranchRequest;
5212
5213        let temp = TempStdDir::default();
5214        let namespace = Arc::new(
5215            DirectoryNamespaceBuilder::new(temp.to_str().unwrap())
5216                .manifest_enabled(true)
5217                .table_version_tracking_enabled(true)
5218                .ops_metrics_enabled(true)
5219                .build()
5220                .await
5221                .unwrap(),
5222        );
5223        let ns: Arc<dyn LanceNamespace> = namespace.clone();
5224        let table_id = vec!["t".to_string()];
5225        create_managed_table(&ns, &table_id).await; // main: v1 (id=1), v2 (id=2)
5226        ns.create_table_branch(CreateTableBranchRequest {
5227            id: Some(table_id.clone()),
5228            name: "exp".to_string(),
5229            ..Default::default()
5230        })
5231        .await
5232        .unwrap();
5233
5234        let main_chain_len = |ns: Arc<dyn LanceNamespace>, table_id: Vec<String>| async move {
5235            ns.list_table_versions(ListTableVersionsRequest {
5236                id: Some(table_id),
5237                ..Default::default()
5238            })
5239            .await
5240            .unwrap()
5241            .versions
5242            .len()
5243        };
5244        let main_before = main_chain_len(ns.clone(), table_id.clone()).await;
5245        let commits_before = namespace
5246            .retrieve_ops_metrics()
5247            .get("create_table_version")
5248            .copied()
5249            .unwrap_or(0);
5250
5251        let branch_ds = Dataset::write_into_namespace_on_branch(
5252            RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5253            ns.clone(),
5254            table_id.clone(),
5255            "exp",
5256            Some(WriteParams {
5257                mode: WriteMode::Append,
5258                ..Default::default()
5259            }),
5260        )
5261        .await
5262        .unwrap();
5263        assert_eq!(branch_ds.manifest.branch.as_deref(), Some("exp"));
5264        assert_eq!(scan_id_column(&branch_ds).await, vec![1, 2, 3]);
5265
5266        // The append must commit through the namespace, not just write a
5267        // physical manifest under the branch tree.
5268        let commits_after = namespace
5269            .retrieve_ops_metrics()
5270            .get("create_table_version")
5271            .copied()
5272            .unwrap_or(0);
5273        assert_eq!(
5274            commits_after,
5275            commits_before + 1,
5276            "the branch append must register its version via create_table_version"
5277        );
5278        let exp_versions = ns
5279            .list_table_versions(ListTableVersionsRequest {
5280                id: Some(table_id.clone()),
5281                branch: Some("exp".to_string()),
5282                ..Default::default()
5283            })
5284            .await
5285            .unwrap()
5286            .versions;
5287        assert!(
5288            exp_versions
5289                .iter()
5290                .all(|v| v.manifest_path.contains("tree/exp")),
5291            "branch versions must resolve to the branch tree: {:?}",
5292            exp_versions
5293        );
5294        assert_eq!(
5295            main_chain_len(ns.clone(), table_id.clone()).await,
5296            main_before,
5297            "main's catalog must be untouched by the branch append"
5298        );
5299
5300        // A managed main append through the same entry point must register in
5301        // the catalog too, so a fresh managed open resolves the new latest.
5302        Dataset::write_into_namespace(
5303            RecordBatchIterator::new(vec![Ok(single_int_batch(100))], single_int_schema()),
5304            ns.clone(),
5305            table_id.clone(),
5306            Some(WriteParams {
5307                mode: WriteMode::Append,
5308                ..Default::default()
5309            }),
5310        )
5311        .await
5312        .unwrap();
5313        assert_eq!(
5314            main_chain_len(ns.clone(), table_id.clone()).await,
5315            main_before + 1,
5316            "a managed main append must register its version in the catalog"
5317        );
5318        let fresh = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5319            .await
5320            .unwrap()
5321            .load()
5322            .await
5323            .unwrap();
5324        assert_eq!(
5325            scan_id_column(&fresh).await,
5326            vec![1, 2, 100],
5327            "a fresh managed open must resolve the appended version, not a stale latest"
5328        );
5329    }
5330
5331    /// CREATE on a branch is rejected: a branch forks from an existing version.
5332    #[tokio::test]
5333    async fn test_write_into_namespace_on_branch_rejects_create() {
5334        use arrow::array::{Int32Array, StringArray};
5335        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
5336
5337        let (namespace, _temp_dir) = create_test_namespace().await;
5338        let namespace = Arc::new(namespace);
5339
5340        let schema = Arc::new(ArrowSchema::new(vec![
5341            Field::new("id", DataType::Int32, false),
5342            Field::new("name", DataType::Utf8, true),
5343        ]));
5344        let batch = arrow::record_batch::RecordBatch::try_new(
5345            schema.clone(),
5346            vec![
5347                Arc::new(Int32Array::from(vec![1])),
5348                Arc::new(StringArray::from(vec![Some("a")])),
5349            ],
5350        )
5351        .unwrap();
5352        let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
5353
5354        let result = Dataset::write_into_namespace_on_branch(
5355            reader,
5356            namespace.clone(),
5357            vec!["new_table".to_string()],
5358            "exp",
5359            Some(WriteParams {
5360                mode: WriteMode::Create,
5361                ..Default::default()
5362            }),
5363        )
5364        .await;
5365        assert!(result.is_err(), "create on a branch must be rejected");
5366        assert!(
5367            result.unwrap_err().to_string().contains("branch"),
5368            "error should mention the branch restriction"
5369        );
5370    }
5371
5372    #[tokio::test]
5373    async fn test_branch_name_validation_rejects_traversal() {
5374        let (namespace, _temp_dir) = create_test_namespace().await;
5375        create_scalar_table(&namespace, "users").await;
5376
5377        // A traversal-style branch name is rejected as invalid input before any
5378        // storage path is built from it.
5379        let err = list_versions(&namespace, "users", Some("../evil")).await;
5380        assert!(err.is_err());
5381        assert!(err.unwrap_err().to_string().contains("invalid branch name"));
5382    }
5383
5384    #[tokio::test]
5385    async fn test_branch_ops_reject_zombie_branch() {
5386        use futures::TryStreamExt;
5387        use lance_namespace::models::{
5388            BatchDeleteTableVersionsRequest, CreateTableVersionRequest, RestoreTableRequest,
5389            VersionRange,
5390        };
5391
5392        let (namespace, _temp_dir) = create_test_namespace().await;
5393        create_scalar_table(&namespace, "users").await;
5394
5395        // Stage a real (loadable) manifest under tree/ghost/_versions/ without
5396        // create_branch, so the path exists but has no BranchContents ref.
5397        let dataset = open_dataset(&namespace, "users").await;
5398        let store = dataset.object_store(None).await.unwrap();
5399        let manifest = store
5400            .inner
5401            .list(Some(&dataset.versions_dir()))
5402            .try_collect::<Vec<_>>()
5403            .await
5404            .unwrap()
5405            .into_iter()
5406            .find(|m| {
5407                m.location
5408                    .filename()
5409                    .map(|f| f.ends_with(".manifest"))
5410                    .unwrap_or(false)
5411            })
5412            .expect("a manifest");
5413        let bytes = store
5414            .inner
5415            .get(&manifest.location)
5416            .await
5417            .unwrap()
5418            .bytes()
5419            .await
5420            .unwrap();
5421        let zombie = Path::from(format!(
5422            "{}/tree/ghost/_versions/{}",
5423            dataset.branch_location().path,
5424            manifest.location.filename().unwrap()
5425        ));
5426        store.inner.put(&zombie, bytes.into()).await.unwrap();
5427
5428        // The directory is physically present, but the source of truth has no
5429        // such branch -- this is what makes every op below reject it.
5430        assert!(dataset.branches().get("ghost").await.is_err());
5431
5432        fn rejected<T: std::fmt::Debug>(label: &str, r: Result<T>) {
5433            match r {
5434                Ok(v) => panic!("{label} must reject the zombie branch, got Ok({v:?})"),
5435                Err(e) => assert!(e.to_string().contains("not found"), "{label}: {e}"),
5436            }
5437        }
5438
5439        rejected(
5440            "list",
5441            list_versions(&namespace, "users", Some("ghost")).await,
5442        );
5443        rejected(
5444            "describe",
5445            namespace
5446                .describe_table_version(DescribeTableVersionRequest {
5447                    id: Some(vec!["users".to_string()]),
5448                    branch: Some("ghost".to_string()),
5449                    ..Default::default()
5450                })
5451                .await,
5452        );
5453        rejected(
5454            "create",
5455            namespace
5456                .create_table_version(CreateTableVersionRequest {
5457                    id: Some(vec!["users".to_string()]),
5458                    version: 2,
5459                    manifest_path: zombie.to_string(),
5460                    branch: Some("ghost".to_string()),
5461                    ..Default::default()
5462                })
5463                .await,
5464        );
5465        rejected(
5466            "restore",
5467            namespace
5468                .restore_table(RestoreTableRequest {
5469                    id: Some(vec!["users".to_string()]),
5470                    version: 1,
5471                    branch: Some("ghost".to_string()),
5472                    ..Default::default()
5473                })
5474                .await,
5475        );
5476        rejected(
5477            "batch_delete",
5478            namespace
5479                .batch_delete_table_versions(BatchDeleteTableVersionsRequest {
5480                    id: Some(vec!["users".to_string()]),
5481                    branch: Some("ghost".to_string()),
5482                    ranges: vec![VersionRange::new(1, 1)],
5483                    ..Default::default()
5484                })
5485                .await,
5486        );
5487    }
5488
5489    /// V2 is the default naming scheme, and the pre-rewrite delete path
5490    /// constructed `{version}.manifest` (a V1 name) and silently matched nothing
5491    /// on a V2 table, returning deleted_count 0. This pins the fix on the main
5492    /// chain (branch=None), which previously had no batch_delete coverage at all.
5493    #[tokio::test]
5494    async fn test_batch_delete_table_versions_main_v2() {
5495        use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
5496
5497        let (namespace, _temp_dir) = create_test_namespace().await;
5498        create_scalar_table(&namespace, "users").await; // version 1
5499        let main_uri = open_dataset(&namespace, "users").await.uri().to_string();
5500        append_scalar_version(&main_uri, 100).await; // version 2
5501        append_scalar_version(&main_uri, 200).await; // version 3
5502
5503        let before = list_versions(&namespace, "users", None).await.unwrap();
5504        assert!(before.len() >= 3);
5505        // Confirm these really are V2-named manifests (20-digit inverted version
5506        // + ".manifest" == 29 chars), i.e. the case the old code skipped.
5507        assert!(
5508            before
5509                .iter()
5510                .all(|v| v.manifest_path.rsplit('/').next().unwrap().len() == 29),
5511            "expected V2-named manifests: {:?}",
5512            before
5513        );
5514        let min_v = before.iter().map(|v| v.version).min().unwrap();
5515        let max_v = before.iter().map(|v| v.version).max().unwrap();
5516
5517        // Delete everything except the latest version. end is exclusive, so
5518        // [min_v, max_v) keeps max_v.
5519        let req = BatchDeleteTableVersionsRequest {
5520            id: Some(vec!["users".to_string()]),
5521            ranges: vec![VersionRange::new(min_v, max_v)],
5522            ..Default::default()
5523        };
5524        let resp = namespace.batch_delete_table_versions(req).await.unwrap();
5525        assert_eq!(
5526            resp.deleted_count,
5527            Some((before.len() - 1) as i64),
5528            "V2 manifests must actually be deleted (was 0 before the fix)"
5529        );
5530
5531        let after = list_versions(&namespace, "users", None).await.unwrap();
5532        assert_eq!(after.len(), 1);
5533        assert_eq!(after[0].version, max_v);
5534    }
5535
5536    /// Pins the exclusive end of VersionRange: [v, v+1) must match only v.
5537    #[tokio::test]
5538    async fn test_batch_delete_end_is_exclusive() {
5539        use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
5540
5541        let (namespace, _temp_dir) = create_test_namespace().await;
5542        create_scalar_table(&namespace, "users").await; // version 1
5543        let main_uri = open_dataset(&namespace, "users").await.uri().to_string();
5544        append_scalar_version(&main_uri, 100).await; // version 2
5545        append_scalar_version(&main_uri, 200).await; // version 3
5546
5547        let before = list_versions(&namespace, "users", None).await.unwrap();
5548        let min_v = before.iter().map(|v| v.version).min().unwrap();
5549
5550        let req = BatchDeleteTableVersionsRequest {
5551            id: Some(vec!["users".to_string()]),
5552            ranges: vec![VersionRange::new(min_v, min_v + 1)],
5553            ..Default::default()
5554        };
5555        let resp = namespace.batch_delete_table_versions(req).await.unwrap();
5556        assert_eq!(
5557            resp.deleted_count,
5558            Some(1),
5559            "only min_v is in [min_v, min_v+1)"
5560        );
5561
5562        let after = list_versions(&namespace, "users", None).await.unwrap();
5563        assert!(
5564            !after.iter().any(|v| v.version == min_v),
5565            "min_v must be deleted"
5566        );
5567        assert_eq!(after.len(), before.len() - 1, "exactly one version removed");
5568    }
5569
5570    #[tokio::test]
5571    async fn test_batch_delete_rejects_unbounded_range() {
5572        use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
5573
5574        let (namespace, _temp_dir) = create_test_namespace().await;
5575        create_scalar_table(&namespace, "users").await;
5576
5577        // An unbounded range must be rejected up front, not turned into ~10^19
5578        // iterations / an unbounded id list.
5579        let req = BatchDeleteTableVersionsRequest {
5580            id: Some(vec!["users".to_string()]),
5581            ranges: vec![VersionRange::new(0, i64::MAX)],
5582            ..Default::default()
5583        };
5584        let err = namespace.batch_delete_table_versions(req).await;
5585        assert!(err.is_err());
5586        assert!(
5587            err.unwrap_err().to_string().contains("limit"),
5588            "expected a range-too-large error"
5589        );
5590    }
5591
5592    /// Build a managed (manifest-tracked) namespace over `path`.
5593    async fn create_managed_namespace(path: &str) -> Arc<dyn LanceNamespace> {
5594        Arc::new(
5595            DirectoryNamespaceBuilder::new(path)
5596                .manifest_enabled(true)
5597                .table_version_tracking_enabled(true)
5598                .build()
5599                .await
5600                .unwrap(),
5601        )
5602    }
5603
5604    fn single_int_schema() -> Arc<arrow::datatypes::Schema> {
5605        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
5606        Arc::new(ArrowSchema::new(vec![Field::new(
5607            "id",
5608            DataType::Int32,
5609            false,
5610        )]))
5611    }
5612
5613    fn single_int_batch(seed: i32) -> arrow::record_batch::RecordBatch {
5614        use arrow::array::Int32Array;
5615        arrow::record_batch::RecordBatch::try_new(
5616            single_int_schema(),
5617            vec![Arc::new(Int32Array::from(vec![seed]))],
5618        )
5619        .unwrap()
5620    }
5621
5622    /// Create a managed table with versions v1 (id=1) and v2 (id=2) on main and
5623    /// return the main dataset handle.
5624    async fn create_managed_table(ns: &Arc<dyn LanceNamespace>, table_id: &[String]) -> Dataset {
5625        let mut ds = Dataset::write_into_namespace(
5626            RecordBatchIterator::new(vec![Ok(single_int_batch(1))], single_int_schema()),
5627            ns.clone(),
5628            table_id.to_vec(),
5629            Some(WriteParams {
5630                mode: WriteMode::Create,
5631                ..Default::default()
5632            }),
5633        )
5634        .await
5635        .unwrap();
5636        ds.append(
5637            RecordBatchIterator::new(vec![Ok(single_int_batch(2))], single_int_schema()),
5638            None,
5639        )
5640        .await
5641        .unwrap();
5642        ds
5643    }
5644
5645    /// Sorted values of the `id` column across a full scan.
5646    async fn scan_id_column(ds: &Dataset) -> Vec<i32> {
5647        use arrow::array::Int32Array;
5648        use futures::TryStreamExt;
5649        let batches: Vec<arrow::record_batch::RecordBatch> = ds
5650            .scan()
5651            .try_into_stream()
5652            .await
5653            .unwrap()
5654            .try_collect()
5655            .await
5656            .unwrap();
5657        let mut ids: Vec<i32> = batches
5658            .iter()
5659            .flat_map(|b| {
5660                b.column(0)
5661                    .as_any()
5662                    .downcast_ref::<Int32Array>()
5663                    .unwrap()
5664                    .values()
5665                    .to_vec()
5666            })
5667            .collect();
5668        ids.sort();
5669        ids
5670    }
5671
5672    /// E2e for the managed branch path through the builder: create a branch via the
5673    /// namespace op, open it with `from_namespace(managed).with_branch`, commit on
5674    /// it, and confirm the dataset is rooted at the branch chain (manifest, base
5675    /// path and data placement) while main's catalog is untouched.
5676    #[tokio::test]
5677    async fn test_managed_branch_open_and_commit() {
5678        use futures::TryStreamExt;
5679        use lance::dataset::builder::DatasetBuilder;
5680        use lance_namespace::models::CreateTableBranchRequest;
5681
5682        let temp = TempStdDir::default();
5683        let ns = create_managed_namespace(temp.to_str().unwrap()).await;
5684        let table_id = vec!["t".to_string()];
5685        create_managed_table(&ns, &table_id).await;
5686        let main_before = ns
5687            .list_table_versions(ListTableVersionsRequest {
5688                id: Some(table_id.clone()),
5689                ..Default::default()
5690            })
5691            .await
5692            .unwrap()
5693            .versions
5694            .len();
5695
5696        // Create a branch via the namespace op (the FS-handler path, which succeeds
5697        // on a managed table).
5698        ns.create_table_branch(CreateTableBranchRequest {
5699            id: Some(table_id.clone()),
5700            name: "exp".to_string(),
5701            ..Default::default()
5702        })
5703        .await
5704        .unwrap();
5705
5706        // Open the managed table on the branch: the base path is qualified up
5707        // front and the manifest store derives the branch from it.
5708        let mut branch_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5709            .await
5710            .unwrap()
5711            .with_branch("exp", None)
5712            .load()
5713            .await
5714            .unwrap();
5715        assert_eq!(
5716            branch_ds.manifest.branch.as_deref(),
5717            Some("exp"),
5718            "with_branch on a managed table must open the branch chain"
5719        );
5720        let branch_base = branch_ds.branch_location().path;
5721        assert!(
5722            branch_base.as_ref().ends_with("tree/exp"),
5723            "the branch dataset must be rooted at the branch chain: {}",
5724            branch_base
5725        );
5726        let branch_v_before = branch_ds.version().version;
5727
5728        // Commit on the branch.
5729        branch_ds
5730            .append(
5731                RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5732                None,
5733            )
5734            .await
5735            .unwrap();
5736        assert_eq!(
5737            branch_ds.manifest.branch.as_deref(),
5738            Some("exp"),
5739            "the commit must stay on the branch"
5740        );
5741        assert!(
5742            branch_ds.version().version > branch_v_before,
5743            "the branch version must advance after the commit"
5744        );
5745        assert_eq!(scan_id_column(&branch_ds).await, vec![1, 2, 3]);
5746
5747        // The committed data files live under the branch chain, not main's data
5748        // dir, so unmanaged readers of the branch and main's cleanup see a
5749        // consistent layout.
5750        let store = branch_ds.object_store(None).await.unwrap();
5751        let branch_data = branch_base.clone().join("data");
5752        let branch_files = store
5753            .inner
5754            .list(Some(&branch_data))
5755            .try_collect::<Vec<_>>()
5756            .await
5757            .unwrap();
5758        assert!(
5759            !branch_files.is_empty(),
5760            "the branch commit must place data files under the branch chain"
5761        );
5762
5763        // The same branch is readable through the unmanaged (path-based) open.
5764        let table_uri = ns
5765            .describe_table(DescribeTableRequest {
5766                id: Some(table_id.clone()),
5767                ..Default::default()
5768            })
5769            .await
5770            .unwrap()
5771            .location
5772            .unwrap();
5773        let fs_branch_ds = DatasetBuilder::from_uri(&table_uri)
5774            .with_branch("exp", None)
5775            .load()
5776            .await
5777            .unwrap();
5778        assert_eq!(fs_branch_ds.manifest.branch.as_deref(), Some("exp"));
5779        assert_eq!(scan_id_column(&fs_branch_ds).await, vec![1, 2, 3]);
5780
5781        // Main's catalog is untouched (branches are not tracked in __manifest),
5782        // and main still reads its own data.
5783        let main_after = ns
5784            .list_table_versions(ListTableVersionsRequest {
5785                id: Some(table_id.clone()),
5786                ..Default::default()
5787            })
5788            .await
5789            .unwrap()
5790            .versions
5791            .len();
5792        assert_eq!(
5793            main_after, main_before,
5794            "committing on the branch must not change main's chain"
5795        );
5796        let main_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5797            .await
5798            .unwrap()
5799            .load()
5800            .await
5801            .unwrap();
5802        assert_eq!(main_ds.manifest.branch, None);
5803        assert_eq!(scan_id_column(&main_ds).await, vec![1, 2]);
5804    }
5805
5806    /// Branch-pointing tags on a managed table: create them through the normal
5807    /// API (from both the main and the branch handle), open the table at the
5808    /// tag, and check the tag out from an already-open dataset. All of these
5809    /// must resolve the branch chain, never main's chain.
5810    #[tokio::test]
5811    async fn test_managed_branch_tags() {
5812        use lance::dataset::builder::DatasetBuilder;
5813        use lance::dataset::refs::Ref;
5814        use lance_namespace::models::CreateTableBranchRequest;
5815
5816        let temp = TempStdDir::default();
5817        let ns = create_managed_namespace(temp.to_str().unwrap()).await;
5818        let table_id = vec!["t".to_string()];
5819        let main_ds = create_managed_table(&ns, &table_id).await;
5820        ns.create_table_branch(CreateTableBranchRequest {
5821            id: Some(table_id.clone()),
5822            name: "exp".to_string(),
5823            ..Default::default()
5824        })
5825        .await
5826        .unwrap();
5827        let mut branch_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5828            .await
5829            .unwrap()
5830            .with_branch("exp", None)
5831            .load()
5832            .await
5833            .unwrap();
5834        branch_ds
5835            .append(
5836                RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5837                None,
5838            )
5839            .await
5840            .unwrap();
5841        let branch_version = branch_ds.version().version;
5842
5843        // A branch-pointing tag created from the main handle must validate
5844        // against the branch chain (the version does not exist on main).
5845        main_ds
5846            .tags()
5847            .create("exp-tag", ("exp", Some(branch_version)))
5848            .await
5849            .unwrap();
5850        let tag = main_ds.tags().get("exp-tag").await.unwrap();
5851        assert_eq!(tag.branch.as_deref(), Some("exp"));
5852        assert_eq!(tag.version, branch_version);
5853
5854        // A tag created from the branch handle resolves the branch implicitly.
5855        branch_ds
5856            .tags()
5857            .create("exp-tag2", branch_version)
5858            .await
5859            .unwrap();
5860        let tag2 = branch_ds.tags().get("exp-tag2").await.unwrap();
5861        assert_eq!(tag2.branch.as_deref(), Some("exp"));
5862
5863        // Opening the managed table at the branch-pointing tag checks out the
5864        // branch chain.
5865        let tag_open = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5866            .await
5867            .unwrap()
5868            .with_tag("exp-tag")
5869            .load()
5870            .await
5871            .unwrap();
5872        assert_eq!(tag_open.manifest.branch.as_deref(), Some("exp"));
5873        assert_eq!(tag_open.version().version, branch_version);
5874        assert_eq!(scan_id_column(&tag_open).await, vec![1, 2, 3]);
5875
5876        // So does checking the tag out from an already-open main dataset.
5877        let tag_checkout = main_ds
5878            .checkout_version(Ref::Tag("exp-tag".to_string()))
5879            .await
5880            .unwrap();
5881        assert_eq!(tag_checkout.manifest.branch.as_deref(), Some("exp"));
5882        assert_eq!(scan_id_column(&tag_checkout).await, vec![1, 2, 3]);
5883
5884        // A missing tag on a managed table errors at open.
5885        let err = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5886            .await
5887            .unwrap()
5888            .with_tag("no-such-tag")
5889            .load()
5890            .await;
5891        assert!(err.is_err(), "a missing tag must error");
5892    }
5893
5894    /// Cross-branch checkout on a managed table, including version numbers that
5895    /// exist on both chains (branch numbering continues from the fork point, so
5896    /// overlap is the common case). Every checkout must land on the requested
5897    /// chain and read that chain's data.
5898    #[tokio::test]
5899    async fn test_managed_cross_branch_checkout() {
5900        use lance::dataset::builder::DatasetBuilder;
5901        use lance::dataset::refs::Ref;
5902        use lance_namespace::models::CreateTableBranchRequest;
5903
5904        let temp = TempStdDir::default();
5905        let ns = create_managed_namespace(temp.to_str().unwrap()).await;
5906        let table_id = vec!["t".to_string()];
5907        let mut main_ds = create_managed_table(&ns, &table_id).await;
5908        ns.create_table_branch(CreateTableBranchRequest {
5909            id: Some(table_id.clone()),
5910            name: "exp".to_string(),
5911            ..Default::default()
5912        })
5913        .await
5914        .unwrap();
5915
5916        // exp gets id=3 at its tip; main gets id=100 at the same version number.
5917        let mut branch_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5918            .await
5919            .unwrap()
5920            .with_branch("exp", None)
5921            .load()
5922            .await
5923            .unwrap();
5924        branch_ds
5925            .append(
5926                RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5927                None,
5928            )
5929            .await
5930            .unwrap();
5931        let overlap_version = branch_ds.version().version;
5932        while main_ds.version().version < overlap_version {
5933            main_ds
5934                .append(
5935                    RecordBatchIterator::new(vec![Ok(single_int_batch(100))], single_int_schema()),
5936                    None,
5937                )
5938                .await
5939                .unwrap();
5940        }
5941
5942        // main -> branch at the overlapping version number: must read the
5943        // branch's data, not main's same-numbered version.
5944        let on_branch = main_ds
5945            .checkout_version(Ref::Version(Some("exp".to_string()), Some(overlap_version)))
5946            .await
5947            .unwrap();
5948        assert_eq!(on_branch.manifest.branch.as_deref(), Some("exp"));
5949        assert_eq!(scan_id_column(&on_branch).await, vec![1, 2, 3]);
5950
5951        // main -> branch latest.
5952        let mut on_branch_latest = main_ds.checkout_branch("exp").await.unwrap();
5953        assert_eq!(on_branch_latest.manifest.branch.as_deref(), Some("exp"));
5954        assert_eq!(on_branch_latest.version().version, overlap_version);
5955
5956        // A commit through the checked-out handle (which shares main's commit
5957        // handler) must land on the branch chain, not main's.
5958        let main_chain_len = |ns: Arc<dyn LanceNamespace>, table_id: Vec<String>| async move {
5959            ns.list_table_versions(ListTableVersionsRequest {
5960                id: Some(table_id),
5961                ..Default::default()
5962            })
5963            .await
5964            .unwrap()
5965            .versions
5966            .len()
5967        };
5968        let main_before = main_chain_len(ns.clone(), table_id.clone()).await;
5969        on_branch_latest
5970            .append(
5971                RecordBatchIterator::new(vec![Ok(single_int_batch(4))], single_int_schema()),
5972                None,
5973            )
5974            .await
5975            .unwrap();
5976        assert_eq!(on_branch_latest.manifest.branch.as_deref(), Some("exp"));
5977        assert_eq!(scan_id_column(&on_branch_latest).await, vec![1, 2, 3, 4]);
5978        assert_eq!(
5979            main_chain_len(ns.clone(), table_id.clone()).await,
5980            main_before,
5981            "a commit on the checked-out branch must not advance main's chain"
5982        );
5983
5984        // branch -> main at a specific version.
5985        let on_main = branch_ds
5986            .checkout_version(Ref::Version(None, Some(1)))
5987            .await
5988            .unwrap();
5989        assert_eq!(on_main.manifest.branch, None);
5990        assert_eq!(scan_id_column(&on_main).await, vec![1]);
5991
5992        // branch -> another branch.
5993        ns.create_table_branch(CreateTableBranchRequest {
5994            id: Some(table_id.clone()),
5995            name: "exp2".to_string(),
5996            ..Default::default()
5997        })
5998        .await
5999        .unwrap();
6000        let on_branch2 = branch_ds.checkout_branch("exp2").await.unwrap();
6001        assert_eq!(on_branch2.manifest.branch.as_deref(), Some("exp2"));
6002
6003        // A version missing from the branch chain errors loudly.
6004        let err = main_ds
6005            .checkout_version(Ref::Version(Some("exp".to_string()), Some(999)))
6006            .await;
6007        assert!(err.is_err(), "a version missing from the branch must error");
6008    }
6009
6010    /// CommitBuilder must honor an explicitly supplied commit handler for a
6011    /// Dataset destination: a managed-versioning commit through a dataset that
6012    /// was opened without the namespace handler (as the Java and Python commit
6013    /// APIs allow) must still register with the catalog instead of silently
6014    /// writing a physical manifest the catalog never sees.
6015    #[tokio::test]
6016    async fn test_commit_builder_honors_explicit_handler_for_dataset_dest() {
6017        use lance::dataset::write::{CommitBuilder, InsertBuilder};
6018        use lance::dataset::{WriteDestination, builder::DatasetBuilder};
6019        use lance::io::commit::namespace_manifest::LanceNamespaceExternalManifestStore;
6020        use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
6021
6022        let temp = TempStdDir::default();
6023        let namespace = Arc::new(
6024            DirectoryNamespaceBuilder::new(temp.to_str().unwrap())
6025                .manifest_enabled(true)
6026                .table_version_tracking_enabled(true)
6027                .ops_metrics_enabled(true)
6028                .build()
6029                .await
6030                .unwrap(),
6031        );
6032        let ns: Arc<dyn LanceNamespace> = namespace.clone();
6033        let table_id = vec!["t".to_string()];
6034        create_managed_table(&ns, &table_id).await; // main: v1 (id=1), v2 (id=2)
6035
6036        // Open WITHOUT the namespace handler, the way a binding caller can.
6037        let table_uri = ns
6038            .describe_table(DescribeTableRequest {
6039                id: Some(table_id.clone()),
6040                ..Default::default()
6041            })
6042            .await
6043            .unwrap()
6044            .location
6045            .unwrap();
6046        let plain_ds = Arc::new(Dataset::open(&table_uri).await.unwrap());
6047
6048        let transaction = InsertBuilder::new(WriteDestination::Dataset(plain_ds.clone()))
6049            .with_params(&WriteParams {
6050                mode: WriteMode::Append,
6051                ..Default::default()
6052            })
6053            .execute_uncommitted(vec![single_int_batch(3)])
6054            .await
6055            .unwrap();
6056
6057        let handler = Arc::new(ExternalManifestCommitHandler {
6058            external_manifest_store: Arc::new(
6059                LanceNamespaceExternalManifestStore::for_table_uri(
6060                    ns.clone(),
6061                    table_id.clone(),
6062                    &table_uri,
6063                )
6064                .unwrap(),
6065            ),
6066        });
6067        let commits_before = namespace
6068            .retrieve_ops_metrics()
6069            .get("create_table_version")
6070            .copied()
6071            .unwrap_or(0);
6072        let committed = CommitBuilder::new(WriteDestination::Dataset(plain_ds))
6073            .with_commit_handler(handler)
6074            .execute(transaction)
6075            .await
6076            .unwrap();
6077        assert_eq!(scan_id_column(&committed).await, vec![1, 2, 3]);
6078
6079        let commits_after = namespace
6080            .retrieve_ops_metrics()
6081            .get("create_table_version")
6082            .copied()
6083            .unwrap_or(0);
6084        assert_eq!(
6085            commits_after,
6086            commits_before + 1,
6087            "the explicit handler must route the commit through create_table_version"
6088        );
6089        let fresh = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
6090            .await
6091            .unwrap()
6092            .load()
6093            .await
6094            .unwrap();
6095        assert_eq!(
6096            scan_id_column(&fresh).await,
6097            vec![1, 2, 3],
6098            "a fresh managed open must resolve the committed version"
6099        );
6100    }
6101
6102    /// A branch forked from a non-latest version opens on its own chain.
6103    #[tokio::test]
6104    async fn test_managed_branch_from_non_latest_fork() {
6105        use lance::dataset::builder::DatasetBuilder;
6106        use lance_namespace::models::CreateTableBranchRequest;
6107
6108        let temp = TempStdDir::default();
6109        let ns = create_managed_namespace(temp.to_str().unwrap()).await;
6110        let table_id = vec!["t".to_string()];
6111        create_managed_table(&ns, &table_id).await; // main: v1 (id=1), v2 (id=2)
6112
6113        ns.create_table_branch(CreateTableBranchRequest {
6114            id: Some(table_id.clone()),
6115            name: "old".to_string(),
6116            from_version: Some(1),
6117            ..Default::default()
6118        })
6119        .await
6120        .unwrap();
6121
6122        let old_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
6123            .await
6124            .unwrap()
6125            .with_branch("old", None)
6126            .load()
6127            .await
6128            .unwrap();
6129        assert_eq!(old_ds.manifest.branch.as_deref(), Some("old"));
6130        assert_eq!(
6131            scan_id_column(&old_ds).await,
6132            vec![1],
6133            "the fork must contain only the fork-point data"
6134        );
6135    }
6136
6137    /// The shared parser must decode both naming schemes; this is the cheap
6138    /// V1 no-regression guard (creating a real V1 table is not exposed here).
6139    #[test]
6140    fn test_manifest_version_from_filename() {
6141        // V1: the plain version number.
6142        assert_eq!(
6143            DirectoryNamespace::manifest_version_from_filename("5.manifest"),
6144            Some(5)
6145        );
6146        assert_eq!(
6147            DirectoryNamespace::manifest_version_from_filename("0.manifest"),
6148            Some(0)
6149        );
6150        // V2: version stored as u64::MAX - version, zero-padded to 20 digits.
6151        let v2_five = format!("{:020}.manifest", u64::MAX - 5);
6152        assert_eq!(
6153            DirectoryNamespace::manifest_version_from_filename(&v2_five),
6154            Some(5)
6155        );
6156        let v2_zero = format!("{:020}.manifest", u64::MAX);
6157        assert_eq!(
6158            DirectoryNamespace::manifest_version_from_filename(&v2_zero),
6159            Some(0)
6160        );
6161        // Non-manifest and detached (`d`-prefixed) entries are ignored.
6162        assert_eq!(
6163            DirectoryNamespace::manifest_version_from_filename("data.lance"),
6164            None
6165        );
6166        assert_eq!(
6167            DirectoryNamespace::manifest_version_from_filename("d5.manifest"),
6168            None
6169        );
6170    }
6171
6172    #[tokio::test]
6173    async fn test_create_table() {
6174        let (namespace, _temp_dir) = create_test_namespace().await;
6175
6176        // Create test IPC data
6177        let schema = create_test_schema();
6178        let ipc_data = create_test_ipc_data(&schema);
6179
6180        let mut request = CreateTableRequest::new();
6181        request.id = Some(vec!["test_table".to_string()]);
6182
6183        let response = namespace
6184            .create_table(request, bytes::Bytes::from(ipc_data))
6185            .await
6186            .unwrap();
6187
6188        assert!(response.location.is_some());
6189        assert!(response.location.unwrap().ends_with("test_table.lance"));
6190        assert_eq!(response.version, Some(1));
6191    }
6192
6193    #[tokio::test]
6194    async fn test_create_table_without_data() {
6195        let (namespace, _temp_dir) = create_test_namespace().await;
6196
6197        let mut request = CreateTableRequest::new();
6198        request.id = Some(vec!["test_table".to_string()]);
6199
6200        let result = namespace.create_table(request, bytes::Bytes::new()).await;
6201        assert!(result.is_err());
6202        assert!(
6203            result
6204                .unwrap_err()
6205                .to_string()
6206                .contains("Arrow IPC stream) is required")
6207        );
6208    }
6209
6210    #[tokio::test]
6211    async fn test_create_table_with_invalid_id() {
6212        let (namespace, _temp_dir) = create_test_namespace().await;
6213
6214        // Create test IPC data
6215        let schema = create_test_schema();
6216        let ipc_data = create_test_ipc_data(&schema);
6217
6218        // Test with empty ID
6219        let mut request = CreateTableRequest::new();
6220        request.id = Some(vec![]);
6221
6222        let result = namespace
6223            .create_table(request, bytes::Bytes::from(ipc_data.clone()))
6224            .await;
6225        assert!(result.is_err());
6226
6227        // Test with multi-level ID - should now work with manifest enabled
6228        // First create the parent namespace
6229        let mut create_ns_req = CreateNamespaceRequest::new();
6230        create_ns_req.id = Some(vec!["test_namespace".to_string()]);
6231        namespace.create_namespace(create_ns_req).await.unwrap();
6232
6233        // Now create table in the namespace
6234        let mut request = CreateTableRequest::new();
6235        request.id = Some(vec!["test_namespace".to_string(), "table".to_string()]);
6236
6237        let result = namespace
6238            .create_table(request, bytes::Bytes::from(ipc_data))
6239            .await;
6240        // Should succeed with manifest enabled
6241        assert!(
6242            result.is_ok(),
6243            "Multi-level table IDs should work with manifest enabled"
6244        );
6245    }
6246
6247    #[tokio::test]
6248    async fn test_list_tables() {
6249        let (namespace, _temp_dir) = create_test_namespace().await;
6250
6251        // Initially, no tables
6252        let mut request = ListTablesRequest::new();
6253        request.id = Some(vec![]);
6254        let response = namespace.list_tables(request).await.unwrap();
6255        assert_eq!(response.tables.len(), 0);
6256
6257        // Create test IPC data
6258        let schema = create_test_schema();
6259        let ipc_data = create_test_ipc_data(&schema);
6260
6261        // Create a table
6262        let mut create_request = CreateTableRequest::new();
6263        create_request.id = Some(vec!["table1".to_string()]);
6264        namespace
6265            .create_table(create_request, bytes::Bytes::from(ipc_data.clone()))
6266            .await
6267            .unwrap();
6268
6269        // Create another table
6270        let mut create_request = CreateTableRequest::new();
6271        create_request.id = Some(vec!["table2".to_string()]);
6272        namespace
6273            .create_table(create_request, bytes::Bytes::from(ipc_data))
6274            .await
6275            .unwrap();
6276
6277        // List tables should return both
6278        let mut request = ListTablesRequest::new();
6279        request.id = Some(vec![]);
6280        let response = namespace.list_tables(request).await.unwrap();
6281        let tables = response.tables;
6282        assert_eq!(tables.len(), 2);
6283        assert!(tables.contains(&"table1".to_string()));
6284        assert!(tables.contains(&"table2".to_string()));
6285    }
6286
6287    #[tokio::test]
6288    async fn test_list_tables_pagination() {
6289        let (namespace, _temp_dir) = create_test_namespace().await;
6290
6291        let schema = create_test_schema();
6292        let ipc_data = create_test_ipc_data(&schema);
6293
6294        for name in ["alpha", "bravo", "charlie"] {
6295            let mut req = CreateTableRequest::new();
6296            req.id = Some(vec![name.to_string()]);
6297            namespace
6298                .create_table(req, bytes::Bytes::from(ipc_data.clone()))
6299                .await
6300                .unwrap();
6301        }
6302
6303        // First page: limit=2, no page_token
6304        let first_page = namespace
6305            .list_tables(ListTablesRequest {
6306                id: Some(vec![]),
6307                limit: Some(2),
6308                ..Default::default()
6309            })
6310            .await
6311            .unwrap();
6312
6313        assert_eq!(first_page.tables, vec!["alpha", "bravo"]);
6314        assert_eq!(first_page.page_token.as_deref(), Some("bravo"));
6315
6316        // Second page: use page_token from first response
6317        let second_page = namespace
6318            .list_tables(ListTablesRequest {
6319                id: Some(vec![]),
6320                limit: Some(2),
6321                page_token: first_page.page_token.clone(),
6322                ..Default::default()
6323            })
6324            .await
6325            .unwrap();
6326
6327        assert_eq!(second_page.tables, vec!["charlie"]);
6328        assert!(second_page.page_token.is_none());
6329    }
6330
6331    #[tokio::test]
6332    async fn test_list_tables_pagination_limit_zero() {
6333        let (namespace, _temp_dir) = create_test_namespace().await;
6334
6335        let schema = create_test_schema();
6336        let ipc_data = create_test_ipc_data(&schema);
6337
6338        let mut req = CreateTableRequest::new();
6339        req.id = Some(vec!["alpha".to_string()]);
6340        namespace
6341            .create_table(req, bytes::Bytes::from(ipc_data))
6342            .await
6343            .unwrap();
6344
6345        let response = namespace
6346            .list_tables(ListTablesRequest {
6347                id: Some(vec![]),
6348                limit: Some(0),
6349                ..Default::default()
6350            })
6351            .await
6352            .unwrap();
6353
6354        assert!(response.tables.is_empty());
6355        assert!(response.page_token.is_none());
6356    }
6357
6358    #[tokio::test]
6359    async fn test_list_tables_with_namespace_id() {
6360        let (namespace, _temp_dir) = create_test_namespace().await;
6361
6362        // First create a child namespace
6363        let mut create_ns_req = CreateNamespaceRequest::new();
6364        create_ns_req.id = Some(vec!["test_namespace".to_string()]);
6365        namespace.create_namespace(create_ns_req).await.unwrap();
6366
6367        // Now list tables in the child namespace
6368        let mut request = ListTablesRequest::new();
6369        request.id = Some(vec!["test_namespace".to_string()]);
6370
6371        let result = namespace.list_tables(request).await;
6372        // Should succeed (with manifest enabled) and return empty list (no tables yet)
6373        assert!(
6374            result.is_ok(),
6375            "list_tables should work with child namespace when manifest is enabled"
6376        );
6377        let response = result.unwrap();
6378        assert_eq!(
6379            response.tables.len(),
6380            0,
6381            "Namespace should have no tables yet"
6382        );
6383    }
6384
6385    #[tokio::test]
6386    async fn test_create_scalar_index() {
6387        let (namespace, _temp_dir) = create_test_namespace().await;
6388        create_scalar_table(&namespace, "users").await;
6389
6390        let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6391        let dataset = open_dataset(&namespace, "users").await;
6392        let expected_transaction_id = dataset
6393            .read_transaction()
6394            .await
6395            .unwrap()
6396            .map(|transaction| transaction.uuid);
6397        assert_eq!(transaction_id, expected_transaction_id);
6398        let indices = dataset.load_indices().await.unwrap();
6399        assert!(indices.iter().any(|index| index.name == "users_id_idx"));
6400    }
6401
6402    #[tokio::test]
6403    async fn test_create_vector_index() {
6404        use lance_namespace::models::CreateTableIndexRequest;
6405
6406        let (namespace, _temp_dir) = create_test_namespace().await;
6407        create_vector_table(&namespace, "vectors").await;
6408
6409        let mut create_index_request =
6410            CreateTableIndexRequest::new("vector".to_string(), "IVF_FLAT".to_string());
6411        create_index_request.id = Some(vec!["vectors".to_string()]);
6412        create_index_request.name = Some("vector_idx".to_string());
6413        create_index_request.distance_type = Some("l2".to_string());
6414        let transaction_id = namespace
6415            .create_table_index(create_index_request)
6416            .await
6417            .unwrap()
6418            .transaction_id;
6419
6420        let dataset = open_dataset(&namespace, "vectors").await;
6421        let expected_transaction_id = dataset
6422            .read_transaction()
6423            .await
6424            .unwrap()
6425            .map(|transaction| transaction.uuid);
6426        assert_eq!(transaction_id, expected_transaction_id);
6427        let indices = dataset.load_indices().await.unwrap();
6428        assert!(indices.iter().any(|index| index.name == "vector_idx"));
6429    }
6430
6431    #[tokio::test]
6432    async fn test_list_table_indices() {
6433        use lance_namespace::models::{CreateTableIndexRequest, ListTableIndicesRequest};
6434
6435        let (namespace, _temp_dir) = create_test_namespace().await;
6436        create_scalar_table(&namespace, "users").await;
6437        create_scalar_index(&namespace, "users", "a_idx").await;
6438        create_scalar_index(&namespace, "users", "b_idx").await;
6439        let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6440
6441        let response = namespace
6442            .list_table_indices(ListTableIndicesRequest {
6443                id: Some(vec!["users".to_string()]),
6444                ..Default::default()
6445            })
6446            .await
6447            .unwrap();
6448
6449        assert_eq!(response.indexes.len(), 3);
6450        assert_eq!(response.indexes[0].index_name, "a_idx");
6451        assert_eq!(response.indexes[1].index_name, "b_idx");
6452        assert_eq!(response.indexes[2].index_name, "users_id_idx");
6453        assert!(response.page_token.is_none());
6454        let users_id_idx = response
6455            .indexes
6456            .iter()
6457            .find(|index| index.index_name == "users_id_idx")
6458            .unwrap();
6459        assert_eq!(users_id_idx.columns, vec!["id"]);
6460        assert_eq!(users_id_idx.status, "SUCCEEDED");
6461
6462        // Enriched fields populated from the index metadata for a scalar index.
6463        assert_eq!(users_id_idx.index_type.as_deref(), Some("BTree"));
6464        assert!(
6465            users_id_idx
6466                .type_url
6467                .as_deref()
6468                .is_some_and(|s| !s.is_empty())
6469        );
6470        assert_eq!(users_id_idx.num_indexed_rows, Some(3));
6471        assert_eq!(users_id_idx.num_unindexed_rows, Some(0));
6472        assert_eq!(users_id_idx.num_segments, Some(1));
6473        assert!(users_id_idx.size_bytes.is_some_and(|size| size > 0));
6474        assert!(users_id_idx.created_at.is_some());
6475        assert!(users_id_idx.index_version.is_some());
6476        assert!(users_id_idx.index_details.is_some());
6477
6478        let dataset = open_dataset(&namespace, "users").await;
6479        let expected_transaction_id = dataset
6480            .read_transaction()
6481            .await
6482            .unwrap()
6483            .map(|transaction| transaction.uuid);
6484        assert_eq!(transaction_id, expected_transaction_id);
6485        let indices = dataset.load_indices().await.unwrap();
6486        assert_eq!(
6487            indices
6488                .iter()
6489                .filter(|index| index.name == "users_id_idx")
6490                .count(),
6491            1
6492        );
6493
6494        let first_page = namespace
6495            .list_table_indices(ListTableIndicesRequest {
6496                id: Some(vec!["users".to_string()]),
6497                limit: Some(2),
6498                ..Default::default()
6499            })
6500            .await
6501            .unwrap();
6502
6503        assert_eq!(first_page.indexes.len(), 2);
6504        assert_eq!(first_page.indexes[0].index_name, "a_idx");
6505        assert_eq!(first_page.indexes[1].index_name, "b_idx");
6506        assert_eq!(first_page.page_token.as_deref(), Some("b_idx"));
6507
6508        let second_page = namespace
6509            .list_table_indices(ListTableIndicesRequest {
6510                id: Some(vec!["users".to_string()]),
6511                page_token: first_page.page_token.clone(),
6512                limit: Some(2),
6513                ..Default::default()
6514            })
6515            .await
6516            .unwrap();
6517
6518        assert_eq!(second_page.indexes.len(), 1);
6519        assert_eq!(second_page.indexes[0].index_name, "users_id_idx");
6520        assert!(second_page.page_token.is_none());
6521
6522        // A vector index exercises a different type_url, index_type, and details payload.
6523        create_vector_table(&namespace, "vectors").await;
6524        let mut create_index_request =
6525            CreateTableIndexRequest::new("vector".to_string(), "IVF_FLAT".to_string());
6526        create_index_request.id = Some(vec!["vectors".to_string()]);
6527        create_index_request.name = Some("vector_idx".to_string());
6528        create_index_request.distance_type = Some("l2".to_string());
6529        namespace
6530            .create_table_index(create_index_request)
6531            .await
6532            .unwrap();
6533
6534        let vector_response = namespace
6535            .list_table_indices(ListTableIndicesRequest {
6536                id: Some(vec!["vectors".to_string()]),
6537                ..Default::default()
6538            })
6539            .await
6540            .unwrap();
6541
6542        assert_eq!(vector_response.indexes.len(), 1);
6543        let vector_idx = &vector_response.indexes[0];
6544        assert_eq!(vector_idx.index_name, "vector_idx");
6545        assert_eq!(vector_idx.columns, vec!["vector"]);
6546        assert_eq!(vector_idx.index_type.as_deref(), Some("IVF_FLAT"));
6547        assert!(
6548            vector_idx
6549                .type_url
6550                .as_deref()
6551                .is_some_and(|s| !s.is_empty())
6552        );
6553        assert!(vector_idx.num_indexed_rows.is_some());
6554        assert!(vector_idx.num_unindexed_rows.is_some());
6555        assert_eq!(vector_idx.num_segments, Some(1));
6556        assert!(vector_idx.created_at.is_some());
6557        assert!(vector_idx.index_version.is_some());
6558        assert!(vector_idx.index_details.is_some());
6559    }
6560
6561    #[tokio::test]
6562    async fn test_describe_table_index_stats() {
6563        use lance_namespace::models::DescribeTableIndexStatsRequest;
6564
6565        let (namespace, _temp_dir) = create_test_namespace().await;
6566        create_scalar_table(&namespace, "users").await;
6567        let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6568
6569        let response = namespace
6570            .describe_table_index_stats(DescribeTableIndexStatsRequest {
6571                id: Some(vec!["users".to_string()]),
6572                index_name: Some("users_id_idx".to_string()),
6573                ..Default::default()
6574            })
6575            .await
6576            .unwrap();
6577        assert_eq!(response.index_type, Some("BTree".to_string()));
6578        assert_eq!(response.num_indices, Some(1));
6579        assert_eq!(response.num_indexed_rows, Some(3));
6580        assert_eq!(response.num_unindexed_rows, Some(0));
6581
6582        let dataset = open_dataset(&namespace, "users").await;
6583        let expected_transaction_id = dataset
6584            .read_transaction()
6585            .await
6586            .unwrap()
6587            .map(|transaction| transaction.uuid);
6588        assert_eq!(transaction_id, expected_transaction_id);
6589        let stats: serde_json::Value =
6590            serde_json::from_str(&dataset.index_statistics("users_id_idx").await.unwrap()).unwrap();
6591        assert_eq!(stats["index_type"], "BTree");
6592        assert_eq!(stats["num_indices"], 1);
6593        assert_eq!(stats["num_indexed_rows"], 3);
6594        assert_eq!(stats["num_unindexed_rows"], 0);
6595    }
6596
6597    #[tokio::test]
6598    async fn test_describe_transaction() {
6599        use lance_namespace::models::DescribeTransactionRequest;
6600
6601        let (namespace, _temp_dir) = create_test_namespace().await;
6602        create_scalar_table(&namespace, "users").await;
6603        let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6604        let dataset = open_dataset(&namespace, "users").await;
6605        let latest_transaction = dataset.read_transaction().await.unwrap();
6606        assert_eq!(
6607            transaction_id,
6608            latest_transaction
6609                .as_ref()
6610                .map(|transaction| transaction.uuid.clone())
6611        );
6612
6613        if let Some(transaction_id) = transaction_id {
6614            let response = namespace
6615                .describe_transaction(DescribeTransactionRequest {
6616                    id: Some(vec!["users".to_string(), transaction_id.clone()]),
6617                    ..Default::default()
6618                })
6619                .await
6620                .unwrap();
6621            assert_eq!(response.status, "SUCCEEDED");
6622            assert_eq!(
6623                response
6624                    .properties
6625                    .as_ref()
6626                    .and_then(|props| props.get("operation")),
6627                Some(&"CreateIndex".to_string())
6628            );
6629            assert_eq!(
6630                response
6631                    .properties
6632                    .as_ref()
6633                    .and_then(|props| props.get("uuid")),
6634                Some(&transaction_id)
6635            );
6636        } else {
6637            assert!(latest_transaction.is_none());
6638        }
6639    }
6640
6641    #[tokio::test]
6642    async fn test_drop_table_index() {
6643        use lance_namespace::models::{DropTableIndexRequest, ListTableIndicesRequest};
6644
6645        let (namespace, _temp_dir) = create_test_namespace().await;
6646        create_scalar_table(&namespace, "users").await;
6647        let create_transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6648
6649        let drop_transaction_id = namespace
6650            .drop_table_index(DropTableIndexRequest {
6651                id: Some(vec!["users".to_string()]),
6652                index_name: Some("users_id_idx".to_string()),
6653                ..Default::default()
6654            })
6655            .await
6656            .unwrap()
6657            .transaction_id;
6658
6659        let dataset = open_dataset(&namespace, "users").await;
6660        let previous_dataset = dataset
6661            .checkout_version(dataset.version().version - 1)
6662            .await
6663            .unwrap();
6664        let previous_transaction_id = previous_dataset
6665            .read_transaction()
6666            .await
6667            .unwrap()
6668            .map(|transaction| transaction.uuid);
6669        assert_eq!(create_transaction_id, previous_transaction_id);
6670        let expected_drop_transaction_id = dataset
6671            .read_transaction()
6672            .await
6673            .unwrap()
6674            .map(|transaction| transaction.uuid);
6675        assert_eq!(drop_transaction_id, expected_drop_transaction_id);
6676        let indices = dataset.load_indices().await.unwrap();
6677        assert!(!indices.iter().any(|index| index.name == "users_id_idx"));
6678
6679        let list_response = namespace
6680            .list_table_indices(ListTableIndicesRequest {
6681                id: Some(vec!["users".to_string()]),
6682                ..Default::default()
6683            })
6684            .await
6685            .unwrap();
6686        assert!(list_response.indexes.is_empty());
6687    }
6688
6689    #[tokio::test]
6690    async fn test_describe_table() {
6691        let (namespace, _temp_dir) = create_test_namespace().await;
6692
6693        // Create a table first
6694        let schema = create_test_schema();
6695        let ipc_data = create_test_ipc_data(&schema);
6696
6697        let mut create_request = CreateTableRequest::new();
6698        create_request.id = Some(vec!["test_table".to_string()]);
6699        namespace
6700            .create_table(create_request, bytes::Bytes::from(ipc_data))
6701            .await
6702            .unwrap();
6703
6704        // Describe the table
6705        let mut request = DescribeTableRequest::new();
6706        request.id = Some(vec!["test_table".to_string()]);
6707        let response = namespace.describe_table(request).await.unwrap();
6708
6709        assert!(response.location.is_some());
6710        assert!(response.location.unwrap().ends_with("test_table.lance"));
6711    }
6712
6713    #[tokio::test]
6714    async fn test_describe_nonexistent_table() {
6715        let (namespace, _temp_dir) = create_test_namespace().await;
6716
6717        let mut request = DescribeTableRequest::new();
6718        request.id = Some(vec!["nonexistent".to_string()]);
6719
6720        let result = namespace.describe_table(request).await;
6721        assert!(result.is_err());
6722        assert!(result.unwrap_err().to_string().contains("Table not found"));
6723    }
6724
6725    #[tokio::test]
6726    async fn test_table_exists() {
6727        let (namespace, _temp_dir) = create_test_namespace().await;
6728
6729        // Create a table
6730        let schema = create_test_schema();
6731        let ipc_data = create_test_ipc_data(&schema);
6732
6733        let mut create_request = CreateTableRequest::new();
6734        create_request.id = Some(vec!["existing_table".to_string()]);
6735        namespace
6736            .create_table(create_request, bytes::Bytes::from(ipc_data))
6737            .await
6738            .unwrap();
6739
6740        // Check existing table
6741        let mut request = TableExistsRequest::new();
6742        request.id = Some(vec!["existing_table".to_string()]);
6743        let result = namespace.table_exists(request).await;
6744        assert!(result.is_ok());
6745
6746        // Check non-existent table
6747        let mut request = TableExistsRequest::new();
6748        request.id = Some(vec!["nonexistent".to_string()]);
6749        let result = namespace.table_exists(request).await;
6750        assert!(result.is_err());
6751        assert!(result.unwrap_err().to_string().contains("Table not found"));
6752    }
6753
6754    #[tokio::test]
6755    async fn test_drop_table() {
6756        let (namespace, _temp_dir) = create_test_namespace().await;
6757
6758        // Create a table
6759        let schema = create_test_schema();
6760        let ipc_data = create_test_ipc_data(&schema);
6761
6762        let mut create_request = CreateTableRequest::new();
6763        create_request.id = Some(vec!["table_to_drop".to_string()]);
6764        namespace
6765            .create_table(create_request, bytes::Bytes::from(ipc_data))
6766            .await
6767            .unwrap();
6768
6769        // Verify it exists
6770        let mut exists_request = TableExistsRequest::new();
6771        exists_request.id = Some(vec!["table_to_drop".to_string()]);
6772        assert!(namespace.table_exists(exists_request.clone()).await.is_ok());
6773
6774        // Drop the table
6775        let mut drop_request = DropTableRequest::new();
6776        drop_request.id = Some(vec!["table_to_drop".to_string()]);
6777        let response = namespace.drop_table(drop_request).await.unwrap();
6778        assert!(response.location.is_some());
6779
6780        // Verify it no longer exists
6781        assert!(namespace.table_exists(exists_request).await.is_err());
6782    }
6783
6784    #[tokio::test]
6785    async fn test_drop_nonexistent_table() {
6786        let (namespace, _temp_dir) = create_test_namespace().await;
6787
6788        let mut request = DropTableRequest::new();
6789        request.id = Some(vec!["nonexistent".to_string()]);
6790
6791        // Should not fail when dropping non-existent table (idempotent)
6792        let result = namespace.drop_table(request).await;
6793        // The operation might succeed or fail depending on implementation
6794        // But it should not panic
6795        let _ = result;
6796    }
6797
6798    #[tokio::test]
6799    async fn test_root_namespace_operations() {
6800        let (namespace, _temp_dir) = create_test_namespace().await;
6801
6802        // Test list_namespaces - should return empty list for root
6803        let mut request = ListNamespacesRequest::new();
6804        request.id = Some(vec![]);
6805        let result = namespace.list_namespaces(request).await;
6806        assert!(result.is_ok());
6807        assert_eq!(result.unwrap().namespaces.len(), 0);
6808
6809        // Test describe_namespace - should succeed for root
6810        let mut request = DescribeNamespaceRequest::new();
6811        request.id = Some(vec![]);
6812        let result = namespace.describe_namespace(request).await;
6813        assert!(result.is_ok());
6814
6815        // Test namespace_exists - root always exists
6816        let mut request = NamespaceExistsRequest::new();
6817        request.id = Some(vec![]);
6818        let result = namespace.namespace_exists(request).await;
6819        assert!(result.is_ok());
6820
6821        // Test create_namespace - root cannot be created
6822        let mut request = CreateNamespaceRequest::new();
6823        request.id = Some(vec![]);
6824        let result = namespace.create_namespace(request).await;
6825        assert!(result.is_err());
6826        assert!(result.unwrap_err().to_string().contains("already exists"));
6827
6828        // Test drop_namespace - root cannot be dropped
6829        let mut request = DropNamespaceRequest::new();
6830        request.id = Some(vec![]);
6831        let result = namespace.drop_namespace(request).await;
6832        assert!(result.is_err());
6833        assert!(
6834            result
6835                .unwrap_err()
6836                .to_string()
6837                .contains("cannot be dropped")
6838        );
6839    }
6840
6841    #[tokio::test]
6842    async fn test_non_root_namespace_operations() {
6843        let (namespace, _temp_dir) = create_test_namespace().await;
6844
6845        // With manifest enabled (default), child namespaces are now supported
6846        // Test create_namespace for non-root - should succeed with manifest
6847        let mut request = CreateNamespaceRequest::new();
6848        request.id = Some(vec!["child".to_string()]);
6849        let result = namespace.create_namespace(request).await;
6850        assert!(
6851            result.is_ok(),
6852            "Child namespace creation should succeed with manifest enabled"
6853        );
6854
6855        // Test namespace_exists for non-root - should exist after creation
6856        let mut request = NamespaceExistsRequest::new();
6857        request.id = Some(vec!["child".to_string()]);
6858        let result = namespace.namespace_exists(request).await;
6859        assert!(
6860            result.is_ok(),
6861            "Child namespace should exist after creation"
6862        );
6863
6864        // Test drop_namespace for non-root - should succeed
6865        let mut request = DropNamespaceRequest::new();
6866        request.id = Some(vec!["child".to_string()]);
6867        let result = namespace.drop_namespace(request).await;
6868        assert!(
6869            result.is_ok(),
6870            "Child namespace drop should succeed with manifest enabled"
6871        );
6872
6873        // Verify namespace no longer exists
6874        let mut request = NamespaceExistsRequest::new();
6875        request.id = Some(vec!["child".to_string()]);
6876        let result = namespace.namespace_exists(request).await;
6877        assert!(
6878            result.is_err(),
6879            "Child namespace should not exist after drop"
6880        );
6881    }
6882
6883    #[tokio::test]
6884    async fn test_config_custom_root() {
6885        let temp_dir = TempStdDir::default();
6886        let custom_path = temp_dir.join("custom");
6887        std::fs::create_dir(&custom_path).unwrap();
6888
6889        let namespace = DirectoryNamespaceBuilder::new(custom_path.to_string_lossy().to_string())
6890            .build()
6891            .await
6892            .unwrap();
6893
6894        // Create test IPC data
6895        let schema = create_test_schema();
6896        let ipc_data = create_test_ipc_data(&schema);
6897
6898        // Create a table and verify location
6899        let mut request = CreateTableRequest::new();
6900        request.id = Some(vec!["test_table".to_string()]);
6901
6902        let response = namespace
6903            .create_table(request, bytes::Bytes::from(ipc_data))
6904            .await
6905            .unwrap();
6906
6907        assert!(response.location.unwrap().contains("custom"));
6908    }
6909
6910    #[tokio::test]
6911    async fn test_config_storage_options() {
6912        let temp_dir = TempStdDir::default();
6913
6914        let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
6915            .storage_option("option1", "value1")
6916            .storage_option("option2", "value2")
6917            .build()
6918            .await
6919            .unwrap();
6920
6921        // Create test IPC data
6922        let schema = create_test_schema();
6923        let ipc_data = create_test_ipc_data(&schema);
6924
6925        // Create a table and check storage options are included
6926        let mut request = CreateTableRequest::new();
6927        request.id = Some(vec!["test_table".to_string()]);
6928
6929        let response = namespace
6930            .create_table(request, bytes::Bytes::from(ipc_data))
6931            .await
6932            .unwrap();
6933
6934        let storage_options = response.storage_options.unwrap();
6935        assert_eq!(storage_options.get("option1"), Some(&"value1".to_string()));
6936        assert_eq!(storage_options.get("option2"), Some(&"value2".to_string()));
6937    }
6938
6939    /// When no credential vendor is configured, `describe_table` and
6940    /// `declare_table` must strip credential keys from storage options
6941    /// while preserving non-credential config (region, endpoint, etc.).
6942    #[tokio::test]
6943    async fn test_no_storage_options_without_vendor() {
6944        use lance_namespace::models::DeclareTableRequest;
6945
6946        let temp_dir = TempStdDir::default();
6947
6948        // No manifest, no credential vendor, but storage options with credentials
6949        let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
6950            .manifest_enabled(false)
6951            .storage_option("aws_access_key_id", "AKID")
6952            .storage_option("aws_secret_access_key", "SECRET")
6953            .storage_option("region", "us-east-1")
6954            .build()
6955            .await
6956            .unwrap();
6957
6958        let schema = create_test_schema();
6959        let ipc_data = create_test_ipc_data(&schema);
6960
6961        // create_table
6962        let mut create_req = CreateTableRequest::new();
6963        create_req.id = Some(vec!["t1".to_string()]);
6964        namespace
6965            .create_table(create_req, bytes::Bytes::from(ipc_data))
6966            .await
6967            .unwrap();
6968
6969        // describe_table should not return storage options without a vendor
6970        let mut desc_req = DescribeTableRequest::new();
6971        desc_req.id = Some(vec!["t1".to_string()]);
6972        let resp = namespace.describe_table(desc_req).await.unwrap();
6973        assert!(resp.storage_options.is_none());
6974
6975        // declare_table should not return storage options without a vendor
6976        let mut decl_req = DeclareTableRequest::new();
6977        decl_req.id = Some(vec!["t2".to_string()]);
6978        let resp = namespace.declare_table(decl_req).await.unwrap();
6979        assert!(resp.storage_options.is_none());
6980    }
6981
6982    /// Same test with manifest mode enabled.
6983    #[tokio::test]
6984    async fn test_no_storage_options_without_vendor_manifest() {
6985        let temp_dir = TempStdDir::default();
6986
6987        let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
6988            .storage_option("aws_access_key_id", "AKID")
6989            .storage_option("aws_secret_access_key", "SECRET")
6990            .storage_option("region", "us-east-1")
6991            .build()
6992            .await
6993            .unwrap();
6994
6995        let schema = create_test_schema();
6996        let ipc_data = create_test_ipc_data(&schema);
6997
6998        let mut create_req = CreateTableRequest::new();
6999        create_req.id = Some(vec!["t1".to_string()]);
7000        namespace
7001            .create_table(create_req, bytes::Bytes::from(ipc_data))
7002            .await
7003            .unwrap();
7004
7005        // describe_table through manifest should not return storage options without a vendor
7006        let mut desc_req = DescribeTableRequest::new();
7007        desc_req.id = Some(vec!["t1".to_string()]);
7008        let resp = namespace.describe_table(desc_req).await.unwrap();
7009        assert!(resp.storage_options.is_none());
7010    }
7011
7012    #[tokio::test]
7013    async fn test_from_properties_manifest_enabled() {
7014        let temp_dir = TempStdDir::default();
7015
7016        let mut properties = HashMap::new();
7017        properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7018        properties.insert("manifest_enabled".to_string(), "true".to_string());
7019        properties.insert("dir_listing_enabled".to_string(), "false".to_string());
7020
7021        let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7022        assert!(builder.manifest_enabled);
7023        assert!(!builder.dir_listing_enabled);
7024
7025        let namespace = builder.build().await.unwrap();
7026
7027        // Create test IPC data
7028        let schema = create_test_schema();
7029        let ipc_data = create_test_ipc_data(&schema);
7030
7031        // Create a table
7032        let mut request = CreateTableRequest::new();
7033        request.id = Some(vec!["test_table".to_string()]);
7034
7035        let response = namespace
7036            .create_table(request, bytes::Bytes::from(ipc_data))
7037            .await
7038            .unwrap();
7039
7040        assert!(response.location.is_some());
7041    }
7042
7043    #[tokio::test]
7044    async fn test_from_properties_dir_listing_enabled() {
7045        let temp_dir = TempStdDir::default();
7046
7047        let mut properties = HashMap::new();
7048        properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7049        properties.insert("manifest_enabled".to_string(), "false".to_string());
7050        properties.insert("dir_listing_enabled".to_string(), "true".to_string());
7051
7052        let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7053        assert!(!builder.manifest_enabled);
7054        assert!(builder.dir_listing_enabled);
7055
7056        let namespace = builder.build().await.unwrap();
7057
7058        // Create test IPC data
7059        let schema = create_test_schema();
7060        let ipc_data = create_test_ipc_data(&schema);
7061
7062        // Create a table
7063        let mut request = CreateTableRequest::new();
7064        request.id = Some(vec!["test_table".to_string()]);
7065
7066        let response = namespace
7067            .create_table(request, bytes::Bytes::from(ipc_data))
7068            .await
7069            .unwrap();
7070
7071        assert!(response.location.is_some());
7072    }
7073
7074    #[tokio::test]
7075    async fn test_from_properties_defaults() {
7076        let temp_dir = TempStdDir::default();
7077
7078        let mut properties = HashMap::new();
7079        properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7080
7081        let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7082        // Both should default to true
7083        assert!(builder.manifest_enabled);
7084        assert!(builder.dir_listing_enabled);
7085    }
7086
7087    #[tokio::test]
7088    async fn test_from_properties_with_storage_options() {
7089        let temp_dir = TempStdDir::default();
7090
7091        let mut properties = HashMap::new();
7092        properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7093        properties.insert("manifest_enabled".to_string(), "true".to_string());
7094        properties.insert("storage.region".to_string(), "us-west-2".to_string());
7095        properties.insert("storage.bucket".to_string(), "my-bucket".to_string());
7096
7097        let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7098        assert!(builder.manifest_enabled);
7099        assert!(builder.storage_options.is_some());
7100
7101        let storage_options = builder.storage_options.unwrap();
7102        assert_eq!(
7103            storage_options.get("region"),
7104            Some(&"us-west-2".to_string())
7105        );
7106        assert_eq!(
7107            storage_options.get("bucket"),
7108            Some(&"my-bucket".to_string())
7109        );
7110    }
7111
7112    #[tokio::test]
7113    async fn test_various_arrow_types() {
7114        let (namespace, _temp_dir) = create_test_namespace().await;
7115
7116        // Create schema with various types
7117        let fields = vec![
7118            JsonArrowField {
7119                name: "bool_col".to_string(),
7120                r#type: Box::new(JsonArrowDataType::new("bool".to_string())),
7121                nullable: true,
7122                metadata: None,
7123            },
7124            JsonArrowField {
7125                name: "int8_col".to_string(),
7126                r#type: Box::new(JsonArrowDataType::new("int8".to_string())),
7127                nullable: true,
7128                metadata: None,
7129            },
7130            JsonArrowField {
7131                name: "float64_col".to_string(),
7132                r#type: Box::new(JsonArrowDataType::new("float64".to_string())),
7133                nullable: true,
7134                metadata: None,
7135            },
7136            JsonArrowField {
7137                name: "binary_col".to_string(),
7138                r#type: Box::new(JsonArrowDataType::new("binary".to_string())),
7139                nullable: true,
7140                metadata: None,
7141            },
7142        ];
7143
7144        let schema = JsonArrowSchema {
7145            fields,
7146            metadata: None,
7147        };
7148
7149        // Create IPC data
7150        let ipc_data = create_test_ipc_data(&schema);
7151
7152        let mut request = CreateTableRequest::new();
7153        request.id = Some(vec!["complex_table".to_string()]);
7154
7155        let response = namespace
7156            .create_table(request, bytes::Bytes::from(ipc_data))
7157            .await
7158            .unwrap();
7159
7160        assert!(response.location.is_some());
7161    }
7162
7163    #[tokio::test]
7164    async fn test_connect_dir() {
7165        let temp_dir = TempStdDir::default();
7166
7167        let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
7168            .build()
7169            .await
7170            .unwrap();
7171
7172        // Test basic operation through the concrete type
7173        let mut request = ListTablesRequest::new();
7174        request.id = Some(vec![]);
7175        let response = namespace.list_tables(request).await.unwrap();
7176        assert_eq!(response.tables.len(), 0);
7177    }
7178
7179    #[tokio::test]
7180    async fn test_create_table_with_ipc_data() {
7181        use arrow::array::{Int32Array, StringArray};
7182        use arrow::ipc::writer::StreamWriter;
7183
7184        let (namespace, _temp_dir) = create_test_namespace().await;
7185
7186        // Create a schema with some fields
7187        let schema = create_test_schema();
7188
7189        // Create some test data that matches the schema
7190        let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
7191        let arrow_schema = Arc::new(arrow_schema);
7192
7193        // Create a RecordBatch with actual data
7194        let id_array = Int32Array::from(vec![1, 2, 3]);
7195        let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
7196        let batch = arrow::record_batch::RecordBatch::try_new(
7197            arrow_schema.clone(),
7198            vec![Arc::new(id_array), Arc::new(name_array)],
7199        )
7200        .unwrap();
7201
7202        // Write the batch to an IPC stream
7203        let mut buffer = Vec::new();
7204        {
7205            let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
7206            writer.write(&batch).unwrap();
7207            writer.finish().unwrap();
7208        }
7209
7210        // Create table with the IPC data
7211        let mut request = CreateTableRequest::new();
7212        request.id = Some(vec!["test_table_with_data".to_string()]);
7213
7214        let response = namespace
7215            .create_table(request, Bytes::from(buffer))
7216            .await
7217            .unwrap();
7218
7219        assert_eq!(response.version, Some(1));
7220        assert!(
7221            response
7222                .location
7223                .unwrap()
7224                .contains("test_table_with_data.lance")
7225        );
7226
7227        // Verify table exists
7228        let mut exists_request = TableExistsRequest::new();
7229        exists_request.id = Some(vec!["test_table_with_data".to_string()]);
7230        namespace.table_exists(exists_request).await.unwrap();
7231    }
7232
7233    #[tokio::test]
7234    async fn test_child_namespace_create_and_list() {
7235        let (namespace, _temp_dir) = create_test_namespace().await;
7236
7237        // Create multiple child namespaces
7238        for i in 1..=3 {
7239            let mut create_req = CreateNamespaceRequest::new();
7240            create_req.id = Some(vec![format!("ns{}", i)]);
7241            let result = namespace.create_namespace(create_req).await;
7242            assert!(result.is_ok(), "Failed to create child namespace ns{}", i);
7243        }
7244
7245        // List child namespaces
7246        let list_req = ListNamespacesRequest {
7247            id: Some(vec![]),
7248            ..Default::default()
7249        };
7250        let result = namespace.list_namespaces(list_req).await;
7251        assert!(result.is_ok());
7252        let namespaces = result.unwrap().namespaces;
7253        assert_eq!(namespaces.len(), 3);
7254        assert!(namespaces.contains(&"ns1".to_string()));
7255        assert!(namespaces.contains(&"ns2".to_string()));
7256        assert!(namespaces.contains(&"ns3".to_string()));
7257    }
7258
7259    #[tokio::test]
7260    async fn test_nested_namespace_hierarchy() {
7261        let (namespace, _temp_dir) = create_test_namespace().await;
7262
7263        // Create parent namespace
7264        let mut create_req = CreateNamespaceRequest::new();
7265        create_req.id = Some(vec!["parent".to_string()]);
7266        namespace.create_namespace(create_req).await.unwrap();
7267
7268        // Create nested children
7269        let mut create_req = CreateNamespaceRequest::new();
7270        create_req.id = Some(vec!["parent".to_string(), "child1".to_string()]);
7271        namespace.create_namespace(create_req).await.unwrap();
7272
7273        let mut create_req = CreateNamespaceRequest::new();
7274        create_req.id = Some(vec!["parent".to_string(), "child2".to_string()]);
7275        namespace.create_namespace(create_req).await.unwrap();
7276
7277        // List children of parent
7278        let list_req = ListNamespacesRequest {
7279            id: Some(vec!["parent".to_string()]),
7280            ..Default::default()
7281        };
7282        let result = namespace.list_namespaces(list_req).await;
7283        assert!(result.is_ok());
7284        let children = result.unwrap().namespaces;
7285        assert_eq!(children.len(), 2);
7286        assert!(children.contains(&"child1".to_string()));
7287        assert!(children.contains(&"child2".to_string()));
7288
7289        // List root should only show parent
7290        let list_req = ListNamespacesRequest {
7291            id: Some(vec![]),
7292            ..Default::default()
7293        };
7294        let result = namespace.list_namespaces(list_req).await;
7295        assert!(result.is_ok());
7296        let root_namespaces = result.unwrap().namespaces;
7297        assert_eq!(root_namespaces.len(), 1);
7298        assert_eq!(root_namespaces[0], "parent");
7299    }
7300
7301    #[tokio::test]
7302    async fn test_table_in_child_namespace() {
7303        let (namespace, _temp_dir) = create_test_namespace().await;
7304
7305        // Create child namespace
7306        let mut create_ns_req = CreateNamespaceRequest::new();
7307        create_ns_req.id = Some(vec!["test_ns".to_string()]);
7308        namespace.create_namespace(create_ns_req).await.unwrap();
7309
7310        // Create table in child namespace
7311        let schema = create_test_schema();
7312        let ipc_data = create_test_ipc_data(&schema);
7313        let mut create_table_req = CreateTableRequest::new();
7314        create_table_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7315        let result = namespace
7316            .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7317            .await;
7318        assert!(result.is_ok(), "Failed to create table in child namespace");
7319
7320        // List tables in child namespace
7321        let list_req = ListTablesRequest {
7322            id: Some(vec!["test_ns".to_string()]),
7323            ..Default::default()
7324        };
7325        let result = namespace.list_tables(list_req).await;
7326        assert!(result.is_ok());
7327        let tables = result.unwrap().tables;
7328        assert_eq!(tables.len(), 1);
7329        assert_eq!(tables[0], "table1");
7330
7331        // Verify table exists
7332        let mut exists_req = TableExistsRequest::new();
7333        exists_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7334        let result = namespace.table_exists(exists_req).await;
7335        assert!(result.is_ok());
7336
7337        // Describe table in child namespace
7338        let mut describe_req = DescribeTableRequest::new();
7339        describe_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7340        let result = namespace.describe_table(describe_req).await;
7341        assert!(result.is_ok());
7342        let response = result.unwrap();
7343        assert!(response.location.is_some());
7344    }
7345
7346    #[tokio::test]
7347    async fn test_multiple_tables_in_child_namespace() {
7348        let (namespace, _temp_dir) = create_test_namespace().await;
7349
7350        // Create child namespace
7351        let mut create_ns_req = CreateNamespaceRequest::new();
7352        create_ns_req.id = Some(vec!["test_ns".to_string()]);
7353        namespace.create_namespace(create_ns_req).await.unwrap();
7354
7355        // Create multiple tables
7356        let schema = create_test_schema();
7357        let ipc_data = create_test_ipc_data(&schema);
7358        for i in 1..=3 {
7359            let mut create_table_req = CreateTableRequest::new();
7360            create_table_req.id = Some(vec!["test_ns".to_string(), format!("table{}", i)]);
7361            namespace
7362                .create_table(create_table_req, bytes::Bytes::from(ipc_data.clone()))
7363                .await
7364                .unwrap();
7365        }
7366
7367        // List tables
7368        let list_req = ListTablesRequest {
7369            id: Some(vec!["test_ns".to_string()]),
7370            ..Default::default()
7371        };
7372        let result = namespace.list_tables(list_req).await;
7373        assert!(result.is_ok());
7374        let tables = result.unwrap().tables;
7375        assert_eq!(tables.len(), 3);
7376        assert!(tables.contains(&"table1".to_string()));
7377        assert!(tables.contains(&"table2".to_string()));
7378        assert!(tables.contains(&"table3".to_string()));
7379    }
7380
7381    #[tokio::test]
7382    async fn test_drop_table_in_child_namespace() {
7383        let (namespace, _temp_dir) = create_test_namespace().await;
7384
7385        // Create child namespace
7386        let mut create_ns_req = CreateNamespaceRequest::new();
7387        create_ns_req.id = Some(vec!["test_ns".to_string()]);
7388        namespace.create_namespace(create_ns_req).await.unwrap();
7389
7390        // Create table
7391        let schema = create_test_schema();
7392        let ipc_data = create_test_ipc_data(&schema);
7393        let mut create_table_req = CreateTableRequest::new();
7394        create_table_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7395        namespace
7396            .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7397            .await
7398            .unwrap();
7399
7400        // Drop table
7401        let mut drop_req = DropTableRequest::new();
7402        drop_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7403        let result = namespace.drop_table(drop_req).await;
7404        assert!(result.is_ok(), "Failed to drop table in child namespace");
7405
7406        // Verify table no longer exists
7407        let mut exists_req = TableExistsRequest::new();
7408        exists_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7409        let result = namespace.table_exists(exists_req).await;
7410        assert!(result.is_err());
7411    }
7412
7413    #[tokio::test]
7414    async fn test_deeply_nested_namespace() {
7415        let (namespace, _temp_dir) = create_test_namespace().await;
7416
7417        // Create deeply nested namespace hierarchy
7418        let mut create_req = CreateNamespaceRequest::new();
7419        create_req.id = Some(vec!["level1".to_string()]);
7420        namespace.create_namespace(create_req).await.unwrap();
7421
7422        let mut create_req = CreateNamespaceRequest::new();
7423        create_req.id = Some(vec!["level1".to_string(), "level2".to_string()]);
7424        namespace.create_namespace(create_req).await.unwrap();
7425
7426        let mut create_req = CreateNamespaceRequest::new();
7427        create_req.id = Some(vec![
7428            "level1".to_string(),
7429            "level2".to_string(),
7430            "level3".to_string(),
7431        ]);
7432        namespace.create_namespace(create_req).await.unwrap();
7433
7434        // Create table in deeply nested namespace
7435        let schema = create_test_schema();
7436        let ipc_data = create_test_ipc_data(&schema);
7437        let mut create_table_req = CreateTableRequest::new();
7438        create_table_req.id = Some(vec![
7439            "level1".to_string(),
7440            "level2".to_string(),
7441            "level3".to_string(),
7442            "table1".to_string(),
7443        ]);
7444        let result = namespace
7445            .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7446            .await;
7447        assert!(
7448            result.is_ok(),
7449            "Failed to create table in deeply nested namespace"
7450        );
7451
7452        // Verify table exists
7453        let mut exists_req = TableExistsRequest::new();
7454        exists_req.id = Some(vec![
7455            "level1".to_string(),
7456            "level2".to_string(),
7457            "level3".to_string(),
7458            "table1".to_string(),
7459        ]);
7460        let result = namespace.table_exists(exists_req).await;
7461        assert!(result.is_ok());
7462    }
7463
7464    #[tokio::test]
7465    async fn test_namespace_with_properties() {
7466        let (namespace, _temp_dir) = create_test_namespace().await;
7467
7468        // Create namespace with properties
7469        let mut properties = HashMap::new();
7470        properties.insert("owner".to_string(), "test_user".to_string());
7471        properties.insert("description".to_string(), "Test namespace".to_string());
7472
7473        let mut create_req = CreateNamespaceRequest::new();
7474        create_req.id = Some(vec!["test_ns".to_string()]);
7475        create_req.properties = Some(properties.clone());
7476        namespace.create_namespace(create_req).await.unwrap();
7477
7478        // Describe namespace and verify properties
7479        let describe_req = DescribeNamespaceRequest {
7480            id: Some(vec!["test_ns".to_string()]),
7481            ..Default::default()
7482        };
7483        let result = namespace.describe_namespace(describe_req).await;
7484        assert!(result.is_ok());
7485        let response = result.unwrap();
7486        assert!(response.properties.is_some());
7487        let props = response.properties.unwrap();
7488        assert_eq!(props.get("owner"), Some(&"test_user".to_string()));
7489        assert_eq!(
7490            props.get("description"),
7491            Some(&"Test namespace".to_string())
7492        );
7493    }
7494
7495    #[tokio::test]
7496    async fn test_cannot_drop_namespace_with_tables() {
7497        let (namespace, _temp_dir) = create_test_namespace().await;
7498
7499        // Create namespace
7500        let mut create_ns_req = CreateNamespaceRequest::new();
7501        create_ns_req.id = Some(vec!["test_ns".to_string()]);
7502        namespace.create_namespace(create_ns_req).await.unwrap();
7503
7504        // Create table in namespace
7505        let schema = create_test_schema();
7506        let ipc_data = create_test_ipc_data(&schema);
7507        let mut create_table_req = CreateTableRequest::new();
7508        create_table_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7509        namespace
7510            .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7511            .await
7512            .unwrap();
7513
7514        // Try to drop namespace - should fail
7515        let mut drop_req = DropNamespaceRequest::new();
7516        drop_req.id = Some(vec!["test_ns".to_string()]);
7517        let result = namespace.drop_namespace(drop_req).await;
7518        assert!(
7519            result.is_err(),
7520            "Should not be able to drop namespace with tables"
7521        );
7522    }
7523
7524    #[tokio::test]
7525    async fn test_isolation_between_namespaces() {
7526        let (namespace, _temp_dir) = create_test_namespace().await;
7527
7528        // Create two namespaces
7529        let mut create_req = CreateNamespaceRequest::new();
7530        create_req.id = Some(vec!["ns1".to_string()]);
7531        namespace.create_namespace(create_req).await.unwrap();
7532
7533        let mut create_req = CreateNamespaceRequest::new();
7534        create_req.id = Some(vec!["ns2".to_string()]);
7535        namespace.create_namespace(create_req).await.unwrap();
7536
7537        // Create table with same name in both namespaces
7538        let schema = create_test_schema();
7539        let ipc_data = create_test_ipc_data(&schema);
7540
7541        let mut create_table_req = CreateTableRequest::new();
7542        create_table_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
7543        namespace
7544            .create_table(create_table_req, bytes::Bytes::from(ipc_data.clone()))
7545            .await
7546            .unwrap();
7547
7548        let mut create_table_req = CreateTableRequest::new();
7549        create_table_req.id = Some(vec!["ns2".to_string(), "table1".to_string()]);
7550        namespace
7551            .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7552            .await
7553            .unwrap();
7554
7555        // List tables in each namespace
7556        let list_req = ListTablesRequest {
7557            id: Some(vec!["ns1".to_string()]),
7558            page_token: None,
7559            limit: None,
7560            ..Default::default()
7561        };
7562        let result = namespace.list_tables(list_req).await.unwrap();
7563        assert_eq!(result.tables.len(), 1);
7564        assert_eq!(result.tables[0], "table1");
7565
7566        let list_req = ListTablesRequest {
7567            id: Some(vec!["ns2".to_string()]),
7568            page_token: None,
7569            limit: None,
7570            ..Default::default()
7571        };
7572        let result = namespace.list_tables(list_req).await.unwrap();
7573        assert_eq!(result.tables.len(), 1);
7574        assert_eq!(result.tables[0], "table1");
7575
7576        // Drop table in ns1 shouldn't affect ns2
7577        let mut drop_req = DropTableRequest::new();
7578        drop_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
7579        namespace.drop_table(drop_req).await.unwrap();
7580
7581        // Verify ns1 table is gone but ns2 table still exists
7582        let mut exists_req = TableExistsRequest::new();
7583        exists_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
7584        assert!(namespace.table_exists(exists_req).await.is_err());
7585
7586        let mut exists_req = TableExistsRequest::new();
7587        exists_req.id = Some(vec!["ns2".to_string(), "table1".to_string()]);
7588        assert!(namespace.table_exists(exists_req).await.is_ok());
7589    }
7590
7591    #[tokio::test]
7592    async fn test_migrate_directory_tables() {
7593        let temp_dir = TempStdDir::default();
7594        let temp_path = temp_dir.to_str().unwrap();
7595
7596        // Step 1: Create tables in directory-only mode
7597        let dir_only_ns = DirectoryNamespaceBuilder::new(temp_path)
7598            .manifest_enabled(false)
7599            .dir_listing_enabled(true)
7600            .build()
7601            .await
7602            .unwrap();
7603
7604        // Create some tables
7605        let schema = create_test_schema();
7606        let ipc_data = create_test_ipc_data(&schema);
7607
7608        for i in 1..=3 {
7609            let mut create_req = CreateTableRequest::new();
7610            create_req.id = Some(vec![format!("table{}", i)]);
7611            dir_only_ns
7612                .create_table(create_req, bytes::Bytes::from(ipc_data.clone()))
7613                .await
7614                .unwrap();
7615        }
7616
7617        drop(dir_only_ns);
7618
7619        // Step 2: Create namespace with dual mode (manifest + directory listing)
7620        let dual_mode_ns = DirectoryNamespaceBuilder::new(temp_path)
7621            .manifest_enabled(true)
7622            .dir_listing_enabled(true)
7623            .build()
7624            .await
7625            .unwrap();
7626
7627        // Before migration, tables should be visible (via directory listing fallback)
7628        let mut list_req = ListTablesRequest::new();
7629        list_req.id = Some(vec![]);
7630        let tables = dual_mode_ns.list_tables(list_req).await.unwrap().tables;
7631        assert_eq!(tables.len(), 3);
7632
7633        // Run migration
7634        let migrated_count = dual_mode_ns.migrate().await.unwrap();
7635        assert_eq!(migrated_count, 3, "Should migrate all 3 tables");
7636
7637        // Verify tables are now in manifest
7638        let mut list_req = ListTablesRequest::new();
7639        list_req.id = Some(vec![]);
7640        let tables = dual_mode_ns.list_tables(list_req).await.unwrap().tables;
7641        assert_eq!(tables.len(), 3);
7642
7643        // Run migration again - should be idempotent
7644        let migrated_count = dual_mode_ns.migrate().await.unwrap();
7645        assert_eq!(
7646            migrated_count, 0,
7647            "Should not migrate already-migrated tables"
7648        );
7649
7650        drop(dual_mode_ns);
7651
7652        // Step 3: Create namespace with manifest-only mode
7653        let manifest_only_ns = DirectoryNamespaceBuilder::new(temp_path)
7654            .manifest_enabled(true)
7655            .dir_listing_enabled(false)
7656            .build()
7657            .await
7658            .unwrap();
7659
7660        // Tables should still be accessible (now from manifest only)
7661        let mut list_req = ListTablesRequest::new();
7662        list_req.id = Some(vec![]);
7663        let tables = manifest_only_ns.list_tables(list_req).await.unwrap().tables;
7664        assert_eq!(tables.len(), 3);
7665        assert!(tables.contains(&"table1".to_string()));
7666        assert!(tables.contains(&"table2".to_string()));
7667        assert!(tables.contains(&"table3".to_string()));
7668    }
7669
7670    #[tokio::test]
7671    async fn test_migrate_without_manifest() {
7672        let temp_dir = TempStdDir::default();
7673        let temp_path = temp_dir.to_str().unwrap();
7674
7675        // Create namespace without manifest
7676        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7677            .manifest_enabled(false)
7678            .dir_listing_enabled(true)
7679            .build()
7680            .await
7681            .unwrap();
7682
7683        // migrate() should return 0 when manifest is not enabled
7684        let migrated_count = namespace.migrate().await.unwrap();
7685        assert_eq!(migrated_count, 0);
7686    }
7687
7688    #[tokio::test]
7689    async fn test_register_table() {
7690        use lance_namespace::models::{RegisterTableRequest, TableExistsRequest};
7691
7692        let temp_dir = TempStdDir::default();
7693        let temp_path = temp_dir.to_str().unwrap();
7694
7695        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7696            .dir_listing_to_manifest_migration_enabled(true)
7697            .build()
7698            .await
7699            .unwrap();
7700
7701        // Create a physical table first using lance directly
7702        let schema = create_test_schema();
7703        let ipc_data = create_test_ipc_data(&schema);
7704
7705        let table_uri = format!("{}/external_table.lance", temp_path);
7706        let cursor = Cursor::new(ipc_data);
7707        let stream_reader = StreamReader::try_new(cursor, None).unwrap();
7708        let batches: Vec<_> = stream_reader
7709            .collect::<std::result::Result<Vec<_>, _>>()
7710            .unwrap();
7711        let schema = batches[0].schema();
7712        let batch_results: Vec<_> = batches.into_iter().map(Ok).collect();
7713        let reader = RecordBatchIterator::new(batch_results, schema);
7714        Dataset::write(Box::new(reader), &table_uri, None)
7715            .await
7716            .unwrap();
7717
7718        // Register the table
7719        let mut register_req = RegisterTableRequest::new("external_table.lance".to_string());
7720        register_req.id = Some(vec!["registered_table".to_string()]);
7721
7722        let response = namespace.register_table(register_req).await.unwrap();
7723        assert_eq!(response.location, Some("external_table.lance".to_string()));
7724
7725        // Verify table exists in namespace
7726        let mut exists_req = TableExistsRequest::new();
7727        exists_req.id = Some(vec!["registered_table".to_string()]);
7728        assert!(namespace.table_exists(exists_req).await.is_ok());
7729
7730        // Verify we can list the table
7731        let mut list_req = ListTablesRequest::new();
7732        list_req.id = Some(vec![]);
7733        let tables = namespace.list_tables(list_req).await.unwrap();
7734        assert!(tables.tables.contains(&"registered_table".to_string()));
7735    }
7736
7737    #[tokio::test]
7738    async fn test_register_table_duplicate_fails() {
7739        use lance_namespace::models::RegisterTableRequest;
7740
7741        let temp_dir = TempStdDir::default();
7742        let temp_path = temp_dir.to_str().unwrap();
7743
7744        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7745            .build()
7746            .await
7747            .unwrap();
7748
7749        // Register a table
7750        let mut register_req = RegisterTableRequest::new("test_table.lance".to_string());
7751        register_req.id = Some(vec!["test_table".to_string()]);
7752
7753        namespace
7754            .register_table(register_req.clone())
7755            .await
7756            .unwrap();
7757
7758        // Try to register again - should fail
7759        let result = namespace.register_table(register_req).await;
7760        assert!(result.is_err());
7761        assert!(result.unwrap_err().to_string().contains("already exists"));
7762    }
7763
7764    #[tokio::test]
7765    async fn test_deregister_table() {
7766        use lance_namespace::models::{DeregisterTableRequest, TableExistsRequest};
7767
7768        let temp_dir = TempStdDir::default();
7769        let temp_path = temp_dir.to_str().unwrap();
7770
7771        // Create namespace with manifest-only mode (no directory listing fallback)
7772        // This ensures deregistered tables are truly invisible
7773        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7774            .manifest_enabled(true)
7775            .dir_listing_enabled(false)
7776            .build()
7777            .await
7778            .unwrap();
7779
7780        // Create a table
7781        let schema = create_test_schema();
7782        let ipc_data = create_test_ipc_data(&schema);
7783
7784        let mut create_req = CreateTableRequest::new();
7785        create_req.id = Some(vec!["test_table".to_string()]);
7786        namespace
7787            .create_table(create_req, bytes::Bytes::from(ipc_data))
7788            .await
7789            .unwrap();
7790
7791        // Verify table exists
7792        let mut exists_req = TableExistsRequest::new();
7793        exists_req.id = Some(vec!["test_table".to_string()]);
7794        assert!(namespace.table_exists(exists_req.clone()).await.is_ok());
7795
7796        // Deregister the table
7797        let mut deregister_req = DeregisterTableRequest::new();
7798        deregister_req.id = Some(vec!["test_table".to_string()]);
7799        let response = namespace.deregister_table(deregister_req).await.unwrap();
7800
7801        // Should return location and id
7802        assert!(
7803            response.location.is_some(),
7804            "Deregister should return location"
7805        );
7806        let location = response.location.as_ref().unwrap();
7807        // Location should be a proper file:// URI with the temp path
7808        // Use uri_to_url to normalize the temp path to a URL for comparison
7809        let expected_url = lance_io::object_store::uri_to_url(temp_path)
7810            .expect("Failed to convert temp path to URL");
7811        let expected_prefix = expected_url.to_string();
7812        assert!(
7813            location.starts_with(&expected_prefix),
7814            "Location should start with '{}', got: {}",
7815            expected_prefix,
7816            location
7817        );
7818        assert!(
7819            location.contains("test_table"),
7820            "Location should contain table name: {}",
7821            location
7822        );
7823        assert_eq!(response.id, Some(vec!["test_table".to_string()]));
7824
7825        // Verify table no longer exists in namespace (removed from manifest)
7826        assert!(namespace.table_exists(exists_req).await.is_err());
7827
7828        // Verify physical data still exists at the returned location
7829        let dataset = Dataset::open(location).await;
7830        assert!(
7831            dataset.is_ok(),
7832            "Physical table data should still exist at {}",
7833            location
7834        );
7835    }
7836
7837    #[tokio::test]
7838    async fn test_deregister_table_in_child_namespace() {
7839        use lance_namespace::models::{
7840            CreateNamespaceRequest, DeregisterTableRequest, TableExistsRequest,
7841        };
7842
7843        let temp_dir = TempStdDir::default();
7844        let temp_path = temp_dir.to_str().unwrap();
7845
7846        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7847            .build()
7848            .await
7849            .unwrap();
7850
7851        // Create child namespace
7852        let mut create_ns_req = CreateNamespaceRequest::new();
7853        create_ns_req.id = Some(vec!["test_ns".to_string()]);
7854        namespace.create_namespace(create_ns_req).await.unwrap();
7855
7856        // Create a table in the child namespace
7857        let schema = create_test_schema();
7858        let ipc_data = create_test_ipc_data(&schema);
7859
7860        let mut create_req = CreateTableRequest::new();
7861        create_req.id = Some(vec!["test_ns".to_string(), "test_table".to_string()]);
7862        namespace
7863            .create_table(create_req, bytes::Bytes::from(ipc_data))
7864            .await
7865            .unwrap();
7866
7867        // Deregister the table
7868        let mut deregister_req = DeregisterTableRequest::new();
7869        deregister_req.id = Some(vec!["test_ns".to_string(), "test_table".to_string()]);
7870        let response = namespace.deregister_table(deregister_req).await.unwrap();
7871
7872        // Should return location and id in child namespace
7873        assert!(
7874            response.location.is_some(),
7875            "Deregister should return location"
7876        );
7877        let location = response.location.as_ref().unwrap();
7878        // Location should be a proper file:// URI with the temp path
7879        // Use uri_to_url to normalize the temp path to a URL for comparison
7880        let expected_url = lance_io::object_store::uri_to_url(temp_path)
7881            .expect("Failed to convert temp path to URL");
7882        let expected_prefix = expected_url.to_string();
7883        assert!(
7884            location.starts_with(&expected_prefix),
7885            "Location should start with '{}', got: {}",
7886            expected_prefix,
7887            location
7888        );
7889        assert!(
7890            location.contains("test_ns") && location.contains("test_table"),
7891            "Location should contain namespace and table name: {}",
7892            location
7893        );
7894        assert_eq!(
7895            response.id,
7896            Some(vec!["test_ns".to_string(), "test_table".to_string()])
7897        );
7898
7899        // Verify table no longer exists
7900        let mut exists_req = TableExistsRequest::new();
7901        exists_req.id = Some(vec!["test_ns".to_string(), "test_table".to_string()]);
7902        assert!(namespace.table_exists(exists_req).await.is_err());
7903    }
7904
7905    #[tokio::test]
7906    async fn test_register_without_manifest_fails() {
7907        use lance_namespace::models::RegisterTableRequest;
7908
7909        let temp_dir = TempStdDir::default();
7910        let temp_path = temp_dir.to_str().unwrap();
7911
7912        // Create namespace without manifest
7913        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7914            .manifest_enabled(false)
7915            .build()
7916            .await
7917            .unwrap();
7918
7919        // Try to register - should fail (register requires manifest)
7920        let mut register_req = RegisterTableRequest::new("test_table.lance".to_string());
7921        register_req.id = Some(vec!["test_table".to_string()]);
7922        let result = namespace.register_table(register_req).await;
7923        assert!(result.is_err());
7924        assert!(
7925            result
7926                .unwrap_err()
7927                .to_string()
7928                .contains("manifest mode is enabled")
7929        );
7930
7931        // Note: deregister_table now works in V1 mode via .lance-deregistered marker files
7932        // See test_deregister_table_v1_mode for that test case
7933    }
7934
7935    #[tokio::test]
7936    async fn test_register_table_rejects_absolute_uri() {
7937        use lance_namespace::models::RegisterTableRequest;
7938
7939        let temp_dir = TempStdDir::default();
7940        let temp_path = temp_dir.to_str().unwrap();
7941
7942        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7943            .build()
7944            .await
7945            .unwrap();
7946
7947        // Try to register with absolute URI - should fail
7948        let mut register_req = RegisterTableRequest::new("s3://bucket/table.lance".to_string());
7949        register_req.id = Some(vec!["test_table".to_string()]);
7950        let result = namespace.register_table(register_req).await;
7951        assert!(result.is_err());
7952        let err_msg = result.unwrap_err().to_string();
7953        assert!(err_msg.contains("Absolute URIs are not allowed"));
7954    }
7955
7956    #[tokio::test]
7957    async fn test_register_table_rejects_absolute_path() {
7958        use lance_namespace::models::RegisterTableRequest;
7959
7960        let temp_dir = TempStdDir::default();
7961        let temp_path = temp_dir.to_str().unwrap();
7962
7963        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7964            .build()
7965            .await
7966            .unwrap();
7967
7968        // Try to register with absolute path - should fail
7969        let mut register_req = RegisterTableRequest::new("/tmp/table.lance".to_string());
7970        register_req.id = Some(vec!["test_table".to_string()]);
7971        let result = namespace.register_table(register_req).await;
7972        assert!(result.is_err());
7973        let err_msg = result.unwrap_err().to_string();
7974        assert!(err_msg.contains("Absolute paths are not allowed"));
7975    }
7976
7977    #[tokio::test]
7978    async fn test_register_table_rejects_path_traversal() {
7979        use lance_namespace::models::RegisterTableRequest;
7980
7981        let temp_dir = TempStdDir::default();
7982        let temp_path = temp_dir.to_str().unwrap();
7983
7984        let namespace = DirectoryNamespaceBuilder::new(temp_path)
7985            .build()
7986            .await
7987            .unwrap();
7988
7989        // Try to register with path traversal - should fail
7990        let mut register_req = RegisterTableRequest::new("../outside/table.lance".to_string());
7991        register_req.id = Some(vec!["test_table".to_string()]);
7992        let result = namespace.register_table(register_req).await;
7993        assert!(result.is_err());
7994        let err_msg = result.unwrap_err().to_string();
7995        assert!(err_msg.contains("Path traversal is not allowed"));
7996    }
7997
7998    #[tokio::test]
7999    async fn test_namespace_write() {
8000        use arrow::array::Int32Array;
8001        use arrow::datatypes::{DataType, Field as ArrowField, Schema as ArrowSchema};
8002        use arrow::record_batch::{RecordBatch, RecordBatchIterator};
8003        use lance::dataset::{Dataset, WriteMode, WriteParams};
8004        use lance_namespace::LanceNamespace;
8005
8006        let (namespace, _temp_dir) = create_test_namespace().await;
8007        let namespace = Arc::new(namespace) as Arc<dyn LanceNamespace>;
8008
8009        // Use child namespace instead of root
8010        let table_id = vec!["test_ns".to_string(), "test_table".to_string()];
8011        let schema = Arc::new(ArrowSchema::new(vec![
8012            ArrowField::new("a", DataType::Int32, false),
8013            ArrowField::new("b", DataType::Int32, false),
8014        ]));
8015
8016        // Test 1: CREATE mode
8017        let data1 = RecordBatch::try_new(
8018            schema.clone(),
8019            vec![
8020                Arc::new(Int32Array::from(vec![1, 2, 3])),
8021                Arc::new(Int32Array::from(vec![10, 20, 30])),
8022            ],
8023        )
8024        .unwrap();
8025
8026        let reader1 = RecordBatchIterator::new(vec![data1].into_iter().map(Ok), schema.clone());
8027        let dataset =
8028            Dataset::write_into_namespace(reader1, namespace.clone(), table_id.clone(), None)
8029                .await
8030                .unwrap();
8031
8032        assert_eq!(dataset.count_rows(None).await.unwrap(), 3);
8033        assert_eq!(dataset.version().version, 1);
8034
8035        // Test 2: APPEND mode
8036        let data2 = RecordBatch::try_new(
8037            schema.clone(),
8038            vec![
8039                Arc::new(Int32Array::from(vec![4, 5])),
8040                Arc::new(Int32Array::from(vec![40, 50])),
8041            ],
8042        )
8043        .unwrap();
8044
8045        let params_append = WriteParams {
8046            mode: WriteMode::Append,
8047            ..Default::default()
8048        };
8049
8050        let reader2 = RecordBatchIterator::new(vec![data2].into_iter().map(Ok), schema.clone());
8051        let dataset = Dataset::write_into_namespace(
8052            reader2,
8053            namespace.clone(),
8054            table_id.clone(),
8055            Some(params_append),
8056        )
8057        .await
8058        .unwrap();
8059
8060        assert_eq!(dataset.count_rows(None).await.unwrap(), 5);
8061        assert_eq!(dataset.version().version, 2);
8062
8063        // Test 3: OVERWRITE mode
8064        let data3 = RecordBatch::try_new(
8065            schema.clone(),
8066            vec![
8067                Arc::new(Int32Array::from(vec![100, 200])),
8068                Arc::new(Int32Array::from(vec![1000, 2000])),
8069            ],
8070        )
8071        .unwrap();
8072
8073        let params_overwrite = WriteParams {
8074            mode: WriteMode::Overwrite,
8075            ..Default::default()
8076        };
8077
8078        let reader3 = RecordBatchIterator::new(vec![data3].into_iter().map(Ok), schema.clone());
8079        let dataset = Dataset::write_into_namespace(
8080            reader3,
8081            namespace.clone(),
8082            table_id.clone(),
8083            Some(params_overwrite),
8084        )
8085        .await
8086        .unwrap();
8087
8088        assert_eq!(dataset.count_rows(None).await.unwrap(), 2);
8089        assert_eq!(dataset.version().version, 3);
8090
8091        // Verify old data was replaced
8092        let result = dataset.scan().try_into_batch().await.unwrap();
8093        let a_col = result
8094            .column_by_name("a")
8095            .unwrap()
8096            .as_any()
8097            .downcast_ref::<Int32Array>()
8098            .unwrap();
8099        assert_eq!(a_col.values(), &[100, 200]);
8100    }
8101
8102    // ============================================================
8103    // Tests for declare_table
8104    // ============================================================
8105
8106    #[tokio::test]
8107    async fn test_declare_table_v1_mode() {
8108        use lance_namespace::models::{
8109            DeclareTableRequest, DescribeTableRequest, ListTablesRequest, TableExistsRequest,
8110        };
8111
8112        let temp_dir = TempStdDir::default();
8113        let temp_path = temp_dir.to_str().unwrap();
8114
8115        // Create namespace in V1 mode (no manifest)
8116        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8117            .manifest_enabled(false)
8118            .build()
8119            .await
8120            .unwrap();
8121
8122        // Declare a table
8123        let mut declare_req = DeclareTableRequest::new();
8124        declare_req.id = Some(vec!["test_table".to_string()]);
8125        let response = namespace.declare_table(declare_req).await.unwrap();
8126
8127        // Should return location
8128        assert!(response.location.is_some());
8129        let location = response.location.as_ref().unwrap();
8130        assert!(location.ends_with("test_table.lance"));
8131
8132        // Table should exist (via reserved file)
8133        let mut exists_req = TableExistsRequest::new();
8134        exists_req.id = Some(vec!["test_table".to_string()]);
8135        assert!(namespace.table_exists(exists_req).await.is_ok());
8136
8137        // Describe should work but return no version/schema (not written yet)
8138        let mut describe_req = DescribeTableRequest::new();
8139        describe_req.id = Some(vec!["test_table".to_string()]);
8140        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8141        assert!(describe_response.location.is_some());
8142        assert!(describe_response.version.is_none()); // Not written yet
8143        assert!(describe_response.schema.is_none()); // Not written yet
8144        assert_eq!(describe_response.is_only_declared, None);
8145
8146        let mut describe_req = DescribeTableRequest::new();
8147        describe_req.id = Some(vec!["test_table".to_string()]);
8148        describe_req.check_declared = Some(true);
8149        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8150        assert_eq!(describe_response.is_only_declared, Some(true));
8151
8152        let mut list_req = ListTablesRequest::new();
8153        list_req.id = Some(vec![]);
8154        let list_response = namespace.list_tables(list_req.clone()).await.unwrap();
8155        assert_eq!(list_response.tables, vec!["test_table".to_string()]);
8156
8157        list_req.include_declared = Some(false);
8158        let list_response = namespace.list_tables(list_req).await.unwrap();
8159        assert!(list_response.tables.is_empty());
8160    }
8161
8162    #[tokio::test]
8163    async fn test_insert_into_declared_table_promotes_it_from_declared_state() {
8164        use lance_namespace::models::{
8165            DeclareTableRequest, DescribeTableRequest, InsertIntoTableRequest,
8166        };
8167
8168        let temp_dir = TempStdDir::default();
8169        let temp_path = temp_dir.to_str().unwrap();
8170
8171        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8172            .manifest_enabled(false)
8173            .build()
8174            .await
8175            .unwrap();
8176
8177        let mut declare_req = DeclareTableRequest::new();
8178        declare_req.id = Some(vec!["test_table".to_string()]);
8179        namespace.declare_table(declare_req).await.unwrap();
8180
8181        let schema = create_test_schema();
8182        let ipc_data = create_test_ipc_data(&schema);
8183        let mut insert_req = InsertIntoTableRequest::new();
8184        insert_req.id = Some(vec!["test_table".to_string()]);
8185        namespace
8186            .insert_into_table(insert_req, bytes::Bytes::from(ipc_data))
8187            .await
8188            .unwrap();
8189
8190        let mut describe_req = DescribeTableRequest::new();
8191        describe_req.id = Some(vec!["test_table".to_string()]);
8192        describe_req.load_detailed_metadata = Some(true);
8193        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8194
8195        assert_eq!(describe_response.is_only_declared, Some(false));
8196        assert_eq!(describe_response.version, Some(1));
8197        assert!(describe_response.schema.is_some());
8198
8199        let mut list_req = ListTablesRequest::new();
8200        list_req.id = Some(vec![]);
8201        list_req.include_declared = Some(false);
8202        assert_eq!(
8203            namespace.list_tables(list_req).await.unwrap().tables,
8204            vec!["test_table".to_string()]
8205        );
8206    }
8207
8208    #[tokio::test]
8209    async fn test_create_table_after_declare_table_v1_mode_creates_table() {
8210        use lance_namespace::models::{
8211            DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8212        };
8213
8214        let temp_dir = TempStdDir::default();
8215        let temp_path = temp_dir.to_str().unwrap();
8216
8217        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8218            .manifest_enabled(false)
8219            .build()
8220            .await
8221            .unwrap();
8222
8223        let mut declare_req = DeclareTableRequest::new();
8224        declare_req.id = Some(vec!["test_table".to_string()]);
8225        namespace.declare_table(declare_req).await.unwrap();
8226
8227        let mut create_req = CreateTableRequest::new();
8228        create_req.id = Some(vec!["test_table".to_string()]);
8229        let response = namespace
8230            .create_table(
8231                create_req,
8232                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8233            )
8234            .await
8235            .unwrap();
8236
8237        assert_eq!(response.version, Some(1));
8238
8239        let mut describe_req = DescribeTableRequest::new();
8240        describe_req.id = Some(vec!["test_table".to_string()]);
8241        describe_req.load_detailed_metadata = Some(true);
8242        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8243        assert_eq!(describe_response.is_only_declared, Some(false));
8244        assert_eq!(describe_response.version, Some(1));
8245
8246        let mut list_req = ListTablesRequest::new();
8247        list_req.id = Some(vec![]);
8248        list_req.include_declared = Some(false);
8249        assert_eq!(
8250            namespace.list_tables(list_req).await.unwrap().tables,
8251            vec!["test_table".to_string()]
8252        );
8253    }
8254
8255    #[tokio::test]
8256    async fn test_insert_into_declared_table_with_manifest_promotes_it() {
8257        use lance_namespace::models::{
8258            DeclareTableRequest, DescribeTableRequest, InsertIntoTableRequest, ListTablesRequest,
8259        };
8260
8261        let temp_dir = TempStdDir::default();
8262        let temp_path = temp_dir.to_str().unwrap();
8263
8264        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8265            .manifest_enabled(true)
8266            .dir_listing_enabled(false)
8267            .build()
8268            .await
8269            .unwrap();
8270
8271        let mut declare_req = DeclareTableRequest::new();
8272        declare_req.id = Some(vec!["test_table".to_string()]);
8273        namespace.declare_table(declare_req).await.unwrap();
8274
8275        let mut insert_req = InsertIntoTableRequest::new();
8276        insert_req.id = Some(vec!["test_table".to_string()]);
8277        namespace
8278            .insert_into_table(
8279                insert_req,
8280                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8281            )
8282            .await
8283            .unwrap();
8284
8285        let mut describe_req = DescribeTableRequest::new();
8286        describe_req.id = Some(vec!["test_table".to_string()]);
8287        describe_req.load_detailed_metadata = Some(true);
8288        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8289        assert_eq!(describe_response.is_only_declared, Some(false));
8290        assert_eq!(describe_response.version, Some(1));
8291
8292        let mut list_req = ListTablesRequest::new();
8293        list_req.id = Some(vec![]);
8294        list_req.include_declared = Some(false);
8295        assert_eq!(
8296            namespace.list_tables(list_req).await.unwrap().tables,
8297            vec!["test_table".to_string()]
8298        );
8299    }
8300
8301    #[tokio::test]
8302    async fn test_create_table_after_declare_table_with_manifest_creates_table() {
8303        use lance_namespace::models::{
8304            CreateTableRequest, DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8305        };
8306
8307        let temp_dir = TempStdDir::default();
8308        let temp_path = temp_dir.to_str().unwrap();
8309
8310        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8311            .manifest_enabled(true)
8312            .dir_listing_enabled(false)
8313            .build()
8314            .await
8315            .unwrap();
8316
8317        let mut declare_req = DeclareTableRequest::new();
8318        declare_req.id = Some(vec!["test_table".to_string()]);
8319        declare_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8320        namespace.declare_table(declare_req).await.unwrap();
8321
8322        let mut create_req = CreateTableRequest::new();
8323        create_req.id = Some(vec!["test_table".to_string()]);
8324        create_req.mode = Some("Overwrite".to_string());
8325        let response = namespace
8326            .create_table(
8327                create_req,
8328                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8329            )
8330            .await
8331            .unwrap();
8332
8333        assert_eq!(response.version, Some(1));
8334        assert_eq!(
8335            response
8336                .properties
8337                .as_ref()
8338                .and_then(|properties| properties.get("owner")),
8339            Some(&"alice".to_string())
8340        );
8341
8342        let mut describe_req = DescribeTableRequest::new();
8343        describe_req.id = Some(vec!["test_table".to_string()]);
8344        describe_req.load_detailed_metadata = Some(true);
8345        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8346        assert_eq!(describe_response.is_only_declared, Some(false));
8347        assert_eq!(describe_response.version, Some(1));
8348        assert_eq!(
8349            describe_response
8350                .properties
8351                .as_ref()
8352                .and_then(|properties| properties.get("owner")),
8353            Some(&"alice".to_string())
8354        );
8355
8356        let mut list_req = ListTablesRequest::new();
8357        list_req.id = Some(vec![]);
8358        list_req.include_declared = Some(false);
8359        assert_eq!(
8360            namespace.list_tables(list_req).await.unwrap().tables,
8361            vec!["test_table".to_string()]
8362        );
8363    }
8364
8365    #[tokio::test]
8366    async fn test_create_table_after_declare_table_with_manifest_rejects_new_properties() {
8367        use lance_namespace::models::{CreateTableRequest, DeclareTableRequest};
8368
8369        let temp_dir = TempStdDir::default();
8370        let temp_path = temp_dir.to_str().unwrap();
8371
8372        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8373            .manifest_enabled(true)
8374            .dir_listing_enabled(false)
8375            .build()
8376            .await
8377            .unwrap();
8378
8379        let mut declare_req = DeclareTableRequest::new();
8380        declare_req.id = Some(vec!["test_table".to_string()]);
8381        declare_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8382        namespace.declare_table(declare_req).await.unwrap();
8383
8384        let mut create_req = CreateTableRequest::new();
8385        create_req.id = Some(vec!["test_table".to_string()]);
8386        create_req.properties = Some(HashMap::from([("owner".to_string(), "bob".to_string())]));
8387
8388        let result = namespace
8389            .create_table(
8390                create_req,
8391                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8392            )
8393            .await;
8394
8395        assert!(result.is_err());
8396        assert!(
8397            result
8398                .unwrap_err()
8399                .to_string()
8400                .contains("cannot set properties for already declared table")
8401        );
8402    }
8403
8404    #[tokio::test]
8405    async fn test_create_table_with_manifest_exist_ok_keeps_existing_table() {
8406        use lance_namespace::models::{CreateTableRequest, DescribeTableRequest};
8407
8408        let temp_dir = TempStdDir::default();
8409        let temp_path = temp_dir.to_str().unwrap();
8410
8411        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8412            .manifest_enabled(true)
8413            .dir_listing_enabled(false)
8414            .build()
8415            .await
8416            .unwrap();
8417
8418        let mut create_req = CreateTableRequest::new();
8419        create_req.id = Some(vec!["test_table".to_string()]);
8420        create_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8421        namespace
8422            .create_table(
8423                create_req,
8424                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8425            )
8426            .await
8427            .unwrap();
8428
8429        let mut create_req = CreateTableRequest::new();
8430        create_req.id = Some(vec!["test_table".to_string()]);
8431        create_req.mode = Some("ExistOk".to_string());
8432        create_req.properties = Some(HashMap::from([("owner".to_string(), "bob".to_string())]));
8433        let response = namespace
8434            .create_table(
8435                create_req,
8436                bytes::Bytes::from(create_single_row_test_ipc_data()),
8437            )
8438            .await
8439            .unwrap();
8440
8441        assert_eq!(
8442            response
8443                .properties
8444                .as_ref()
8445                .and_then(|properties| properties.get("owner")),
8446            Some(&"alice".to_string())
8447        );
8448        assert_eq!(
8449            open_dataset(&namespace, "test_table")
8450                .await
8451                .count_rows(None)
8452                .await
8453                .unwrap(),
8454            2
8455        );
8456
8457        let mut describe_req = DescribeTableRequest::new();
8458        describe_req.id = Some(vec!["test_table".to_string()]);
8459        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8460        assert_eq!(
8461            describe_response
8462                .properties
8463                .as_ref()
8464                .and_then(|properties| properties.get("owner")),
8465            Some(&"alice".to_string())
8466        );
8467    }
8468
8469    #[tokio::test]
8470    async fn test_create_table_with_manifest_overwrite_replaces_existing_table() {
8471        use lance_namespace::models::{CreateTableRequest, DescribeTableRequest};
8472
8473        let temp_dir = TempStdDir::default();
8474        let temp_path = temp_dir.to_str().unwrap();
8475
8476        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8477            .manifest_enabled(true)
8478            .dir_listing_enabled(false)
8479            .build()
8480            .await
8481            .unwrap();
8482
8483        let mut create_req = CreateTableRequest::new();
8484        create_req.id = Some(vec!["test_table".to_string()]);
8485        create_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8486        namespace
8487            .create_table(
8488                create_req,
8489                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8490            )
8491            .await
8492            .unwrap();
8493
8494        let mut create_req = CreateTableRequest::new();
8495        create_req.id = Some(vec!["test_table".to_string()]);
8496        create_req.mode = Some("overwrite".to_string());
8497        create_req.properties = Some(HashMap::from([("owner".to_string(), "bob".to_string())]));
8498        let response = namespace
8499            .create_table(
8500                create_req,
8501                bytes::Bytes::from(create_single_row_test_ipc_data()),
8502            )
8503            .await
8504            .unwrap();
8505
8506        assert_eq!(response.version, Some(2));
8507        assert_eq!(
8508            response
8509                .properties
8510                .as_ref()
8511                .and_then(|properties| properties.get("owner")),
8512            Some(&"bob".to_string())
8513        );
8514        assert_eq!(
8515            open_dataset(&namespace, "test_table")
8516                .await
8517                .count_rows(None)
8518                .await
8519                .unwrap(),
8520            1
8521        );
8522
8523        let mut describe_req = DescribeTableRequest::new();
8524        describe_req.id = Some(vec!["test_table".to_string()]);
8525        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8526        assert_eq!(
8527            describe_response
8528                .properties
8529                .as_ref()
8530                .and_then(|properties| properties.get("owner")),
8531            Some(&"bob".to_string())
8532        );
8533    }
8534
8535    #[tokio::test]
8536    async fn test_create_table_with_manifest_invalid_mode_rejected() {
8537        use lance_namespace::models::CreateTableRequest;
8538
8539        let temp_dir = TempStdDir::default();
8540        let temp_path = temp_dir.to_str().unwrap();
8541
8542        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8543            .manifest_enabled(true)
8544            .dir_listing_enabled(false)
8545            .build()
8546            .await
8547            .unwrap();
8548
8549        let mut create_req = CreateTableRequest::new();
8550        create_req.id = Some(vec!["test_table".to_string()]);
8551        create_req.mode = Some("append".to_string());
8552        let result = namespace
8553            .create_table(
8554                create_req,
8555                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8556            )
8557            .await;
8558
8559        assert!(result.is_err());
8560        assert!(
8561            result
8562                .unwrap_err()
8563                .to_string()
8564                .contains("Unsupported create_table mode")
8565        );
8566    }
8567
8568    #[tokio::test]
8569    async fn test_merge_insert_into_declared_table_v1_mode_creates_table() {
8570        use lance_namespace::models::{
8571            DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8572            MergeInsertIntoTableRequest,
8573        };
8574
8575        let temp_dir = TempStdDir::default();
8576        let temp_path = temp_dir.to_str().unwrap();
8577
8578        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8579            .manifest_enabled(false)
8580            .build()
8581            .await
8582            .unwrap();
8583
8584        let mut declare_req = DeclareTableRequest::new();
8585        declare_req.id = Some(vec!["test_table".to_string()]);
8586        namespace.declare_table(declare_req).await.unwrap();
8587
8588        let mut merge_req = MergeInsertIntoTableRequest::new();
8589        merge_req.id = Some(vec!["test_table".to_string()]);
8590        merge_req.on = Some("id".to_string());
8591        let response = namespace
8592            .merge_insert_into_table(
8593                merge_req,
8594                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8595            )
8596            .await
8597            .unwrap();
8598
8599        assert_eq!(response.num_inserted_rows, Some(2));
8600        assert_eq!(response.num_updated_rows, Some(0));
8601
8602        let mut describe_req = DescribeTableRequest::new();
8603        describe_req.id = Some(vec!["test_table".to_string()]);
8604        describe_req.load_detailed_metadata = Some(true);
8605        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8606        assert_eq!(describe_response.is_only_declared, Some(false));
8607        assert_eq!(describe_response.version, Some(1));
8608
8609        let mut list_req = ListTablesRequest::new();
8610        list_req.id = Some(vec![]);
8611        list_req.include_declared = Some(false);
8612        assert_eq!(
8613            namespace.list_tables(list_req).await.unwrap().tables,
8614            vec!["test_table".to_string()]
8615        );
8616    }
8617
8618    #[tokio::test]
8619    async fn test_merge_insert_into_declared_table_with_manifest_creates_table() {
8620        use lance_namespace::models::{
8621            DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8622            MergeInsertIntoTableRequest,
8623        };
8624
8625        let temp_dir = TempStdDir::default();
8626        let temp_path = temp_dir.to_str().unwrap();
8627
8628        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8629            .manifest_enabled(true)
8630            .dir_listing_enabled(false)
8631            .build()
8632            .await
8633            .unwrap();
8634
8635        let mut declare_req = DeclareTableRequest::new();
8636        declare_req.id = Some(vec!["test_table".to_string()]);
8637        namespace.declare_table(declare_req).await.unwrap();
8638
8639        let mut merge_req = MergeInsertIntoTableRequest::new();
8640        merge_req.id = Some(vec!["test_table".to_string()]);
8641        merge_req.on = Some("id".to_string());
8642        let response = namespace
8643            .merge_insert_into_table(
8644                merge_req,
8645                bytes::Bytes::from(create_non_empty_test_ipc_data()),
8646            )
8647            .await
8648            .unwrap();
8649
8650        assert_eq!(response.num_inserted_rows, Some(2));
8651        assert_eq!(response.num_updated_rows, Some(0));
8652
8653        let mut describe_req = DescribeTableRequest::new();
8654        describe_req.id = Some(vec!["test_table".to_string()]);
8655        describe_req.load_detailed_metadata = Some(true);
8656        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8657        assert_eq!(describe_response.is_only_declared, Some(false));
8658        assert_eq!(describe_response.version, Some(1));
8659
8660        let mut list_req = ListTablesRequest::new();
8661        list_req.id = Some(vec![]);
8662        list_req.include_declared = Some(false);
8663        assert_eq!(
8664            namespace.list_tables(list_req).await.unwrap().tables,
8665            vec!["test_table".to_string()]
8666        );
8667    }
8668
8669    #[tokio::test]
8670    async fn test_declare_table_with_manifest() {
8671        use lance_namespace::models::{
8672            DeclareTableRequest, DescribeTableRequest, ListTablesRequest, TableExistsRequest,
8673        };
8674
8675        let temp_dir = TempStdDir::default();
8676        let temp_path = temp_dir.to_str().unwrap();
8677
8678        // Create namespace with manifest
8679        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8680            .manifest_enabled(true)
8681            .dir_listing_enabled(false)
8682            .build()
8683            .await
8684            .unwrap();
8685
8686        // Declare a table
8687        let mut declare_req = DeclareTableRequest::new();
8688        declare_req.id = Some(vec!["test_table".to_string()]);
8689        declare_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8690        let response = namespace.declare_table(declare_req).await.unwrap();
8691
8692        // Should return location
8693        assert!(response.location.is_some());
8694        assert_eq!(
8695            response
8696                .properties
8697                .as_ref()
8698                .and_then(|properties| properties.get("owner")),
8699            Some(&"alice".to_string())
8700        );
8701
8702        // Table should exist in manifest
8703        let mut exists_req = TableExistsRequest::new();
8704        exists_req.id = Some(vec!["test_table".to_string()]);
8705        assert!(namespace.table_exists(exists_req).await.is_ok());
8706
8707        let mut describe_req = DescribeTableRequest::new();
8708        describe_req.id = Some(vec!["test_table".to_string()]);
8709        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8710        assert_eq!(describe_response.is_only_declared, None);
8711
8712        let mut describe_req = DescribeTableRequest::new();
8713        describe_req.id = Some(vec!["test_table".to_string()]);
8714        describe_req.check_declared = Some(true);
8715        let describe_response = namespace.describe_table(describe_req).await.unwrap();
8716        assert_eq!(describe_response.is_only_declared, Some(true));
8717        assert_eq!(
8718            describe_response
8719                .properties
8720                .as_ref()
8721                .and_then(|properties| properties.get("owner")),
8722            Some(&"alice".to_string())
8723        );
8724
8725        let mut list_req = ListTablesRequest::new();
8726        list_req.id = Some(vec![]);
8727        assert_eq!(
8728            namespace
8729                .list_tables(list_req.clone())
8730                .await
8731                .unwrap()
8732                .tables,
8733            vec!["test_table".to_string()]
8734        );
8735        list_req.include_declared = Some(false);
8736        assert!(
8737            namespace
8738                .list_tables(list_req)
8739                .await
8740                .unwrap()
8741                .tables
8742                .is_empty()
8743        );
8744    }
8745
8746    #[tokio::test]
8747    async fn test_declare_table_when_table_exists() {
8748        use lance_namespace::models::DeclareTableRequest;
8749
8750        let temp_dir = TempStdDir::default();
8751        let temp_path = temp_dir.to_str().unwrap();
8752
8753        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8754            .manifest_enabled(false)
8755            .build()
8756            .await
8757            .unwrap();
8758
8759        // First create a table with actual data
8760        let schema = create_test_schema();
8761        let ipc_data = create_test_ipc_data(&schema);
8762        let mut create_req = CreateTableRequest::new();
8763        create_req.id = Some(vec!["test_table".to_string()]);
8764        namespace
8765            .create_table(create_req, bytes::Bytes::from(ipc_data))
8766            .await
8767            .unwrap();
8768
8769        // Try to declare the same table - should fail because it already has data
8770        let mut declare_req = DeclareTableRequest::new();
8771        declare_req.id = Some(vec!["test_table".to_string()]);
8772        let result = namespace.declare_table(declare_req).await;
8773        assert!(result.is_err());
8774    }
8775
8776    // ============================================================
8777    // Tests for deregister_table in V1 mode
8778    // ============================================================
8779
8780    #[tokio::test]
8781    async fn test_deregister_table_v1_mode() {
8782        use lance_namespace::models::{DeregisterTableRequest, TableExistsRequest};
8783
8784        let temp_dir = TempStdDir::default();
8785        let temp_path = temp_dir.to_str().unwrap();
8786
8787        // Create namespace in V1 mode (no manifest, with dir listing)
8788        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8789            .manifest_enabled(false)
8790            .dir_listing_enabled(true)
8791            .build()
8792            .await
8793            .unwrap();
8794
8795        // Create a table with data
8796        let schema = create_test_schema();
8797        let ipc_data = create_test_ipc_data(&schema);
8798        let mut create_req = CreateTableRequest::new();
8799        create_req.id = Some(vec!["test_table".to_string()]);
8800        namespace
8801            .create_table(create_req, bytes::Bytes::from(ipc_data))
8802            .await
8803            .unwrap();
8804
8805        // Verify table exists
8806        let mut exists_req = TableExistsRequest::new();
8807        exists_req.id = Some(vec!["test_table".to_string()]);
8808        assert!(namespace.table_exists(exists_req.clone()).await.is_ok());
8809
8810        // Deregister the table
8811        let mut deregister_req = DeregisterTableRequest::new();
8812        deregister_req.id = Some(vec!["test_table".to_string()]);
8813        let response = namespace.deregister_table(deregister_req).await.unwrap();
8814
8815        // Should return location
8816        assert!(response.location.is_some());
8817        let location = response.location.as_ref().unwrap();
8818        assert!(location.contains("test_table"));
8819
8820        // Table should no longer exist (deregistered)
8821        let result = namespace.table_exists(exists_req).await;
8822        assert!(result.is_err());
8823        assert!(result.unwrap_err().to_string().contains("deregistered"));
8824
8825        // Physical data should still exist
8826        let dataset = Dataset::open(location).await;
8827        assert!(dataset.is_ok(), "Physical table data should still exist");
8828    }
8829
8830    #[tokio::test]
8831    async fn test_deregister_table_v1_already_deregistered() {
8832        use lance_namespace::models::DeregisterTableRequest;
8833
8834        let temp_dir = TempStdDir::default();
8835        let temp_path = temp_dir.to_str().unwrap();
8836
8837        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8838            .manifest_enabled(false)
8839            .dir_listing_enabled(true)
8840            .build()
8841            .await
8842            .unwrap();
8843
8844        // Create a table
8845        let schema = create_test_schema();
8846        let ipc_data = create_test_ipc_data(&schema);
8847        let mut create_req = CreateTableRequest::new();
8848        create_req.id = Some(vec!["test_table".to_string()]);
8849        namespace
8850            .create_table(create_req, bytes::Bytes::from(ipc_data))
8851            .await
8852            .unwrap();
8853
8854        // Deregister once
8855        let mut deregister_req = DeregisterTableRequest::new();
8856        deregister_req.id = Some(vec!["test_table".to_string()]);
8857        namespace
8858            .deregister_table(deregister_req.clone())
8859            .await
8860            .unwrap();
8861
8862        // Try to deregister again - should fail
8863        let result = namespace.deregister_table(deregister_req).await;
8864        assert!(result.is_err());
8865        assert!(
8866            result
8867                .unwrap_err()
8868                .to_string()
8869                .contains("already deregistered")
8870        );
8871    }
8872
8873    // ============================================================
8874    // Tests for list_tables skipping deregistered tables
8875    // ============================================================
8876
8877    #[tokio::test]
8878    async fn test_list_tables_skips_deregistered_v1() {
8879        use lance_namespace::models::DeregisterTableRequest;
8880
8881        let temp_dir = TempStdDir::default();
8882        let temp_path = temp_dir.to_str().unwrap();
8883
8884        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8885            .manifest_enabled(false)
8886            .dir_listing_enabled(true)
8887            .build()
8888            .await
8889            .unwrap();
8890
8891        // Create two tables
8892        let schema = create_test_schema();
8893        let ipc_data = create_test_ipc_data(&schema);
8894
8895        let mut create_req1 = CreateTableRequest::new();
8896        create_req1.id = Some(vec!["table1".to_string()]);
8897        namespace
8898            .create_table(create_req1, bytes::Bytes::from(ipc_data.clone()))
8899            .await
8900            .unwrap();
8901
8902        let mut create_req2 = CreateTableRequest::new();
8903        create_req2.id = Some(vec!["table2".to_string()]);
8904        namespace
8905            .create_table(create_req2, bytes::Bytes::from(ipc_data))
8906            .await
8907            .unwrap();
8908
8909        // List tables - should see both (root namespace = empty vec)
8910        let mut list_req = ListTablesRequest::new();
8911        list_req.id = Some(vec![]);
8912        let list_response = namespace.list_tables(list_req.clone()).await.unwrap();
8913        assert_eq!(list_response.tables.len(), 2);
8914
8915        // Deregister table1
8916        let mut deregister_req = DeregisterTableRequest::new();
8917        deregister_req.id = Some(vec!["table1".to_string()]);
8918        namespace.deregister_table(deregister_req).await.unwrap();
8919
8920        // List tables - should only see table2
8921        let list_response = namespace.list_tables(list_req).await.unwrap();
8922        assert_eq!(list_response.tables.len(), 1);
8923        assert!(list_response.tables.contains(&"table2".to_string()));
8924        assert!(!list_response.tables.contains(&"table1".to_string()));
8925    }
8926
8927    // ============================================================
8928    // Tests for describe_table and table_exists with deregistered tables
8929    // ============================================================
8930
8931    #[tokio::test]
8932    async fn test_describe_table_fails_for_deregistered_v1() {
8933        use lance_namespace::models::{DeregisterTableRequest, DescribeTableRequest};
8934
8935        let temp_dir = TempStdDir::default();
8936        let temp_path = temp_dir.to_str().unwrap();
8937
8938        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8939            .manifest_enabled(false)
8940            .dir_listing_enabled(true)
8941            .build()
8942            .await
8943            .unwrap();
8944
8945        // Create a table
8946        let schema = create_test_schema();
8947        let ipc_data = create_test_ipc_data(&schema);
8948        let mut create_req = CreateTableRequest::new();
8949        create_req.id = Some(vec!["test_table".to_string()]);
8950        namespace
8951            .create_table(create_req, bytes::Bytes::from(ipc_data))
8952            .await
8953            .unwrap();
8954
8955        // Describe should work before deregistration
8956        let mut describe_req = DescribeTableRequest::new();
8957        describe_req.id = Some(vec!["test_table".to_string()]);
8958        assert!(namespace.describe_table(describe_req.clone()).await.is_ok());
8959
8960        // Deregister
8961        let mut deregister_req = DeregisterTableRequest::new();
8962        deregister_req.id = Some(vec!["test_table".to_string()]);
8963        namespace.deregister_table(deregister_req).await.unwrap();
8964
8965        // Describe should fail after deregistration
8966        let result = namespace.describe_table(describe_req).await;
8967        assert!(result.is_err());
8968        let err = result.unwrap_err();
8969        assert!(matches!(err, Error::Namespace { .. }));
8970        let err_msg = err.to_string();
8971        assert!(err_msg.contains("deregistered"));
8972        assert!(err_msg.contains("table id 'test_table'"));
8973    }
8974
8975    #[tokio::test]
8976    async fn test_table_exists_fails_for_deregistered_v1() {
8977        use lance_namespace::models::{DeregisterTableRequest, TableExistsRequest};
8978
8979        let temp_dir = TempStdDir::default();
8980        let temp_path = temp_dir.to_str().unwrap();
8981
8982        let namespace = DirectoryNamespaceBuilder::new(temp_path)
8983            .manifest_enabled(false)
8984            .dir_listing_enabled(true)
8985            .build()
8986            .await
8987            .unwrap();
8988
8989        // Create a table
8990        let schema = create_test_schema();
8991        let ipc_data = create_test_ipc_data(&schema);
8992        let mut create_req = CreateTableRequest::new();
8993        create_req.id = Some(vec!["test_table".to_string()]);
8994        namespace
8995            .create_table(create_req, bytes::Bytes::from(ipc_data))
8996            .await
8997            .unwrap();
8998
8999        // Table exists should work before deregistration
9000        let mut exists_req = TableExistsRequest::new();
9001        exists_req.id = Some(vec!["test_table".to_string()]);
9002        assert!(namespace.table_exists(exists_req.clone()).await.is_ok());
9003
9004        // Deregister
9005        let mut deregister_req = DeregisterTableRequest::new();
9006        deregister_req.id = Some(vec!["test_table".to_string()]);
9007        namespace.deregister_table(deregister_req).await.unwrap();
9008
9009        // Table exists should fail after deregistration
9010        let result = namespace.table_exists(exists_req).await;
9011        assert!(result.is_err());
9012        let err = result.unwrap_err();
9013        assert!(matches!(err, Error::Namespace { .. }));
9014        let err_msg = err.to_string();
9015        assert!(err_msg.contains("deregistered"));
9016        assert!(err_msg.contains("table id 'test_table'"));
9017    }
9018
9019    #[tokio::test]
9020    async fn test_atomic_table_status_check() {
9021        // This test verifies that the TableStatus check is atomic
9022        // by ensuring a single directory listing is used
9023
9024        let temp_dir = TempStdDir::default();
9025        let temp_path = temp_dir.to_str().unwrap();
9026
9027        let namespace = DirectoryNamespaceBuilder::new(temp_path)
9028            .manifest_enabled(false)
9029            .dir_listing_enabled(true)
9030            .build()
9031            .await
9032            .unwrap();
9033
9034        // Create a table
9035        let schema = create_test_schema();
9036        let ipc_data = create_test_ipc_data(&schema);
9037        let mut create_req = CreateTableRequest::new();
9038        create_req.id = Some(vec!["test_table".to_string()]);
9039        namespace
9040            .create_table(create_req, bytes::Bytes::from(ipc_data))
9041            .await
9042            .unwrap();
9043
9044        // Table status should show exists=true, is_deregistered=false
9045        let status = namespace.check_table_status("test_table").await;
9046        assert!(status.exists);
9047        assert!(!status.is_deregistered);
9048        assert!(!status.has_reserved_file);
9049    }
9050
9051    #[tokio::test]
9052    async fn test_table_version_tracking_enabled_managed_versioning() {
9053        use lance_namespace::models::DescribeTableRequest;
9054
9055        let temp_dir = TempStdDir::default();
9056        let temp_path = temp_dir.to_str().unwrap();
9057
9058        // Create namespace with table_version_tracking_enabled=true
9059        let namespace = DirectoryNamespaceBuilder::new(temp_path)
9060            .table_version_tracking_enabled(true)
9061            .build()
9062            .await
9063            .unwrap();
9064
9065        // Create a table
9066        let schema = create_test_schema();
9067        let ipc_data = create_test_ipc_data(&schema);
9068        let mut create_req = CreateTableRequest::new();
9069        create_req.id = Some(vec!["test_table".to_string()]);
9070        namespace
9071            .create_table(create_req, bytes::Bytes::from(ipc_data))
9072            .await
9073            .unwrap();
9074
9075        // Describe table should return managed_versioning=true
9076        let mut describe_req = DescribeTableRequest::new();
9077        describe_req.id = Some(vec!["test_table".to_string()]);
9078        let describe_resp = namespace.describe_table(describe_req).await.unwrap();
9079
9080        // managed_versioning should be true
9081        assert_eq!(
9082            describe_resp.managed_versioning,
9083            Some(true),
9084            "managed_versioning should be true when table_version_tracking_enabled=true"
9085        );
9086    }
9087
9088    #[tokio::test]
9089    async fn test_table_version_tracking_disabled_no_managed_versioning() {
9090        use lance_namespace::models::DescribeTableRequest;
9091
9092        let temp_dir = TempStdDir::default();
9093        let temp_path = temp_dir.to_str().unwrap();
9094
9095        // Create namespace with table_version_tracking_enabled=false (default)
9096        let namespace = DirectoryNamespaceBuilder::new(temp_path)
9097            .table_version_tracking_enabled(false)
9098            .build()
9099            .await
9100            .unwrap();
9101
9102        // Create a table
9103        let schema = create_test_schema();
9104        let ipc_data = create_test_ipc_data(&schema);
9105        let mut create_req = CreateTableRequest::new();
9106        create_req.id = Some(vec!["test_table".to_string()]);
9107        namespace
9108            .create_table(create_req, bytes::Bytes::from(ipc_data))
9109            .await
9110            .unwrap();
9111
9112        // Describe table should not have managed_versioning set
9113        let mut describe_req = DescribeTableRequest::new();
9114        describe_req.id = Some(vec!["test_table".to_string()]);
9115        let describe_resp = namespace.describe_table(describe_req).await.unwrap();
9116
9117        // managed_versioning should be None when table_version_tracking_enabled=false
9118        assert!(
9119            describe_resp.managed_versioning.is_none(),
9120            "managed_versioning should be None when table_version_tracking_enabled=false, got: {:?}",
9121            describe_resp.managed_versioning
9122        );
9123    }
9124
9125    #[tokio::test]
9126    async fn test_list_table_versions() {
9127        use arrow::array::{Int32Array, RecordBatchIterator};
9128        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9129        use arrow::record_batch::RecordBatch;
9130        use lance::dataset::{Dataset, WriteMode, WriteParams};
9131        use lance_namespace::models::{CreateNamespaceRequest, ListTableVersionsRequest};
9132
9133        let temp_dir = TempStrDir::default();
9134        let temp_path: &str = &temp_dir;
9135
9136        let namespace: Arc<dyn LanceNamespace> = Arc::new(
9137            DirectoryNamespaceBuilder::new(temp_path)
9138                .table_version_tracking_enabled(true)
9139                .build()
9140                .await
9141                .unwrap(),
9142        );
9143
9144        // Create parent namespace first
9145        let mut create_ns_req = CreateNamespaceRequest::new();
9146        create_ns_req.id = Some(vec!["workspace".to_string()]);
9147        namespace.create_namespace(create_ns_req).await.unwrap();
9148
9149        // Create a table using write_into_namespace (version 1)
9150        let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9151        let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
9152            "id",
9153            DataType::Int32,
9154            false,
9155        )]));
9156        let batch = RecordBatch::try_new(
9157            arrow_schema.clone(),
9158            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
9159        )
9160        .unwrap();
9161        let batches = RecordBatchIterator::new(vec![Ok(batch.clone())], arrow_schema.clone());
9162        let write_params = WriteParams {
9163            mode: WriteMode::Create,
9164            ..Default::default()
9165        };
9166        let mut dataset = Dataset::write_into_namespace(
9167            batches,
9168            namespace.clone(),
9169            table_id.clone(),
9170            Some(write_params),
9171        )
9172        .await
9173        .unwrap();
9174
9175        // Append to create version 2
9176        let batch2 = RecordBatch::try_new(
9177            arrow_schema.clone(),
9178            vec![Arc::new(Int32Array::from(vec![100, 200]))],
9179        )
9180        .unwrap();
9181        let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema.clone());
9182        dataset.append(batches, None).await.unwrap();
9183
9184        // Append to create version 3
9185        let batch3 = RecordBatch::try_new(
9186            arrow_schema.clone(),
9187            vec![Arc::new(Int32Array::from(vec![300, 400]))],
9188        )
9189        .unwrap();
9190        let batches = RecordBatchIterator::new(vec![Ok(batch3)], arrow_schema);
9191        dataset.append(batches, None).await.unwrap();
9192
9193        // List versions - should have versions 1, 2, and 3
9194        let mut list_req = ListTableVersionsRequest::new();
9195        list_req.id = Some(table_id.clone());
9196        let list_resp = namespace.list_table_versions(list_req).await.unwrap();
9197
9198        assert_eq!(
9199            list_resp.versions.len(),
9200            3,
9201            "Should have 3 versions, got: {:?}",
9202            list_resp.versions
9203        );
9204
9205        // Verify each version
9206        for expected_version in 1..=3 {
9207            let version = list_resp
9208                .versions
9209                .iter()
9210                .find(|v| v.version == expected_version)
9211                .unwrap_or_else(|| panic!("Expected version {}", expected_version));
9212
9213            assert!(
9214                !version.manifest_path.is_empty(),
9215                "manifest_path should be set for version {}",
9216                expected_version
9217            );
9218            assert!(
9219                version.manifest_path.contains(".manifest"),
9220                "manifest_path should contain .manifest for version {}",
9221                expected_version
9222            );
9223            assert!(
9224                version.manifest_size.is_some(),
9225                "manifest_size should be set for version {}",
9226                expected_version
9227            );
9228            assert!(
9229                version.manifest_size.unwrap() > 0,
9230                "manifest_size should be > 0 for version {}",
9231                expected_version
9232            );
9233            assert!(
9234                version.timestamp_millis.is_some(),
9235                "timestamp_millis should be set for version {}",
9236                expected_version
9237            );
9238        }
9239    }
9240
9241    #[tokio::test]
9242    async fn test_describe_table_version() {
9243        use arrow::array::{Int32Array, RecordBatchIterator};
9244        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9245        use arrow::record_batch::RecordBatch;
9246        use lance::dataset::{Dataset, WriteMode, WriteParams};
9247        use lance_namespace::models::{CreateNamespaceRequest, DescribeTableVersionRequest};
9248
9249        let temp_dir = TempStrDir::default();
9250        let temp_path: &str = &temp_dir;
9251
9252        let namespace: Arc<dyn LanceNamespace> = Arc::new(
9253            DirectoryNamespaceBuilder::new(temp_path)
9254                .table_version_tracking_enabled(true)
9255                .build()
9256                .await
9257                .unwrap(),
9258        );
9259
9260        // Create parent namespace first
9261        let mut create_ns_req = CreateNamespaceRequest::new();
9262        create_ns_req.id = Some(vec!["workspace".to_string()]);
9263        namespace.create_namespace(create_ns_req).await.unwrap();
9264
9265        // Create a table using write_into_namespace (version 1)
9266        let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9267        let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
9268            "id",
9269            DataType::Int32,
9270            false,
9271        )]));
9272        let batch = RecordBatch::try_new(
9273            arrow_schema.clone(),
9274            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
9275        )
9276        .unwrap();
9277        let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
9278        let write_params = WriteParams {
9279            mode: WriteMode::Create,
9280            ..Default::default()
9281        };
9282        let mut dataset = Dataset::write_into_namespace(
9283            batches,
9284            namespace.clone(),
9285            table_id.clone(),
9286            Some(write_params),
9287        )
9288        .await
9289        .unwrap();
9290
9291        // Append data to create version 2
9292        let batch2 = RecordBatch::try_new(
9293            arrow_schema.clone(),
9294            vec![Arc::new(Int32Array::from(vec![100, 200]))],
9295        )
9296        .unwrap();
9297        let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema);
9298        dataset.append(batches, None).await.unwrap();
9299
9300        // Describe version 1
9301        let mut describe_req = DescribeTableVersionRequest::new();
9302        describe_req.id = Some(table_id.clone());
9303        describe_req.version = Some(1);
9304        let describe_resp = namespace
9305            .describe_table_version(describe_req)
9306            .await
9307            .unwrap();
9308
9309        let version = &describe_resp.version;
9310        assert_eq!(version.version, 1);
9311        assert!(version.timestamp_millis.is_some());
9312        assert!(
9313            !version.manifest_path.is_empty(),
9314            "manifest_path should be set"
9315        );
9316        assert!(
9317            version.manifest_path.contains(".manifest"),
9318            "manifest_path should contain .manifest"
9319        );
9320        assert!(
9321            version.manifest_size.is_some(),
9322            "manifest_size should be set"
9323        );
9324        assert!(
9325            version.manifest_size.unwrap() > 0,
9326            "manifest_size should be > 0"
9327        );
9328
9329        // Describe version 2
9330        let mut describe_req = DescribeTableVersionRequest::new();
9331        describe_req.id = Some(table_id.clone());
9332        describe_req.version = Some(2);
9333        let describe_resp = namespace
9334            .describe_table_version(describe_req)
9335            .await
9336            .unwrap();
9337
9338        let version = &describe_resp.version;
9339        assert_eq!(version.version, 2);
9340        assert!(version.timestamp_millis.is_some());
9341        assert!(
9342            !version.manifest_path.is_empty(),
9343            "manifest_path should be set"
9344        );
9345        assert!(
9346            version.manifest_size.is_some(),
9347            "manifest_size should be set"
9348        );
9349        assert!(
9350            version.manifest_size.unwrap() > 0,
9351            "manifest_size should be > 0"
9352        );
9353    }
9354
9355    #[tokio::test]
9356    async fn test_describe_table_version_latest() {
9357        use arrow::array::{Int32Array, RecordBatchIterator};
9358        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9359        use arrow::record_batch::RecordBatch;
9360        use lance::dataset::{Dataset, WriteMode, WriteParams};
9361        use lance_namespace::models::{CreateNamespaceRequest, DescribeTableVersionRequest};
9362
9363        let temp_dir = TempStrDir::default();
9364        let temp_path: &str = &temp_dir;
9365
9366        let namespace: Arc<dyn LanceNamespace> = Arc::new(
9367            DirectoryNamespaceBuilder::new(temp_path)
9368                .table_version_tracking_enabled(true)
9369                .build()
9370                .await
9371                .unwrap(),
9372        );
9373
9374        // Create parent namespace first
9375        let mut create_ns_req = CreateNamespaceRequest::new();
9376        create_ns_req.id = Some(vec!["workspace".to_string()]);
9377        namespace.create_namespace(create_ns_req).await.unwrap();
9378
9379        // Create a table using write_into_namespace (version 1)
9380        let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9381        let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
9382            "id",
9383            DataType::Int32,
9384            false,
9385        )]));
9386        let batch = RecordBatch::try_new(
9387            arrow_schema.clone(),
9388            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
9389        )
9390        .unwrap();
9391        let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
9392        let write_params = WriteParams {
9393            mode: WriteMode::Create,
9394            ..Default::default()
9395        };
9396        let mut dataset = Dataset::write_into_namespace(
9397            batches,
9398            namespace.clone(),
9399            table_id.clone(),
9400            Some(write_params),
9401        )
9402        .await
9403        .unwrap();
9404
9405        // Append to create version 2
9406        let batch2 = RecordBatch::try_new(
9407            arrow_schema.clone(),
9408            vec![Arc::new(Int32Array::from(vec![100, 200]))],
9409        )
9410        .unwrap();
9411        let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema.clone());
9412        dataset.append(batches, None).await.unwrap();
9413
9414        // Append to create version 3
9415        let batch3 = RecordBatch::try_new(
9416            arrow_schema.clone(),
9417            vec![Arc::new(Int32Array::from(vec![300, 400]))],
9418        )
9419        .unwrap();
9420        let batches = RecordBatchIterator::new(vec![Ok(batch3)], arrow_schema);
9421        dataset.append(batches, None).await.unwrap();
9422
9423        // Describe latest version (no version specified)
9424        let mut describe_req = DescribeTableVersionRequest::new();
9425        describe_req.id = Some(table_id.clone());
9426        describe_req.version = None;
9427        let describe_resp = namespace
9428            .describe_table_version(describe_req)
9429            .await
9430            .unwrap();
9431
9432        // Should return version 3 as it's the latest
9433        assert_eq!(describe_resp.version.version, 3);
9434    }
9435
9436    #[tokio::test]
9437    async fn test_create_table_version() {
9438        use futures::TryStreamExt;
9439        use lance::dataset::builder::DatasetBuilder;
9440        use lance_namespace::models::CreateTableVersionRequest;
9441
9442        let temp_dir = TempStrDir::default();
9443        let temp_path: &str = &temp_dir;
9444
9445        let namespace: Arc<dyn LanceNamespace> = Arc::new(
9446            DirectoryNamespaceBuilder::new(temp_path)
9447                .table_version_tracking_enabled(true)
9448                .build()
9449                .await
9450                .unwrap(),
9451        );
9452
9453        // Create a table
9454        let schema = create_test_schema();
9455        let ipc_data = create_test_ipc_data(&schema);
9456        let mut create_req = CreateTableRequest::new();
9457        create_req.id = Some(vec!["test_table".to_string()]);
9458        namespace
9459            .create_table(create_req, bytes::Bytes::from(ipc_data))
9460            .await
9461            .unwrap();
9462
9463        // Open the dataset using from_namespace to get proper object_store and paths
9464        let table_id = vec!["test_table".to_string()];
9465        let dataset = DatasetBuilder::from_namespace(namespace.clone(), table_id.clone())
9466            .await
9467            .unwrap()
9468            .load()
9469            .await
9470            .unwrap();
9471
9472        // Use dataset's object_store to find and copy the manifest
9473        let versions_path = dataset.versions_dir();
9474        let manifest_metas: Vec<_> = dataset
9475            .object_store(None)
9476            .await
9477            .unwrap()
9478            .inner
9479            .list(Some(&versions_path))
9480            .try_collect()
9481            .await
9482            .unwrap();
9483
9484        let manifest_meta = manifest_metas
9485            .iter()
9486            .find(|m| {
9487                m.location
9488                    .filename()
9489                    .map(|f| f.ends_with(".manifest"))
9490                    .unwrap_or(false)
9491            })
9492            .expect("No manifest file found");
9493
9494        // Read the existing manifest data
9495        let manifest_data = dataset
9496            .object_store(None)
9497            .await
9498            .unwrap()
9499            .inner
9500            .get(&manifest_meta.location)
9501            .await
9502            .unwrap()
9503            .bytes()
9504            .await
9505            .unwrap();
9506
9507        // Write to a staging location using the dataset's object_store
9508        let staging_path = dataset.versions_dir().join("staging_manifest");
9509        dataset
9510            .object_store(None)
9511            .await
9512            .unwrap()
9513            .inner
9514            .put(&staging_path, manifest_data.into())
9515            .await
9516            .unwrap();
9517
9518        // Create version 2 from staging manifest
9519        // Use the same naming scheme as the existing dataset (V2)
9520        let mut create_version_req = CreateTableVersionRequest::new(2, staging_path.to_string());
9521        create_version_req.id = Some(table_id.clone());
9522        create_version_req.naming_scheme = Some("V2".to_string());
9523
9524        let result = namespace.create_table_version(create_version_req).await;
9525        assert!(
9526            result.is_ok(),
9527            "create_table_version should succeed: {:?}",
9528            result
9529        );
9530
9531        // Verify version 2 was created at the path returned in the response
9532        let response = result.unwrap();
9533        let version_info = response
9534            .version
9535            .expect("response should contain version info");
9536        let version_2_path = Path::parse(&version_info.manifest_path).unwrap();
9537        let head_result = dataset
9538            .object_store(None)
9539            .await
9540            .unwrap()
9541            .inner
9542            .head(&version_2_path)
9543            .await;
9544        assert!(
9545            head_result.is_ok(),
9546            "Version 2 manifest should exist at {}",
9547            version_2_path
9548        );
9549
9550        // Verify the staging file has been deleted
9551        let staging_head_result = dataset
9552            .object_store(None)
9553            .await
9554            .unwrap()
9555            .inner
9556            .head(&staging_path)
9557            .await;
9558        assert!(
9559            staging_head_result.is_err(),
9560            "Staging manifest should have been deleted after create_table_version"
9561        );
9562    }
9563
9564    #[tokio::test]
9565    async fn test_create_table_version_conflict() {
9566        // create_table_version should fail if the version already exists.
9567        // Each version always writes to a new file location.
9568        use futures::TryStreamExt;
9569        use lance::dataset::builder::DatasetBuilder;
9570        use lance_namespace::models::CreateTableVersionRequest;
9571
9572        let temp_dir = TempStrDir::default();
9573        let temp_path: &str = &temp_dir;
9574
9575        let namespace: Arc<dyn LanceNamespace> = Arc::new(
9576            DirectoryNamespaceBuilder::new(temp_path)
9577                .table_version_tracking_enabled(true)
9578                .build()
9579                .await
9580                .unwrap(),
9581        );
9582
9583        // Create a table
9584        let schema = create_test_schema();
9585        let ipc_data = create_test_ipc_data(&schema);
9586        let mut create_req = CreateTableRequest::new();
9587        create_req.id = Some(vec!["test_table".to_string()]);
9588        namespace
9589            .create_table(create_req, bytes::Bytes::from(ipc_data))
9590            .await
9591            .unwrap();
9592
9593        // Open the dataset using from_namespace to get proper object_store and paths
9594        let table_id = vec!["test_table".to_string()];
9595        let dataset = DatasetBuilder::from_namespace(namespace.clone(), table_id.clone())
9596            .await
9597            .unwrap()
9598            .load()
9599            .await
9600            .unwrap();
9601
9602        // Use dataset's object_store to find and copy the manifest
9603        let versions_path = dataset.versions_dir();
9604        let manifest_metas: Vec<_> = dataset
9605            .object_store(None)
9606            .await
9607            .unwrap()
9608            .inner
9609            .list(Some(&versions_path))
9610            .try_collect()
9611            .await
9612            .unwrap();
9613
9614        let manifest_meta = manifest_metas
9615            .iter()
9616            .find(|m| {
9617                m.location
9618                    .filename()
9619                    .map(|f| f.ends_with(".manifest"))
9620                    .unwrap_or(false)
9621            })
9622            .expect("No manifest file found");
9623
9624        // Read the existing manifest data
9625        let manifest_data = dataset
9626            .object_store(None)
9627            .await
9628            .unwrap()
9629            .inner
9630            .get(&manifest_meta.location)
9631            .await
9632            .unwrap()
9633            .bytes()
9634            .await
9635            .unwrap();
9636
9637        // Write to a staging location using the dataset's object_store
9638        let staging_path = dataset.versions_dir().join("staging_manifest");
9639        dataset
9640            .object_store(None)
9641            .await
9642            .unwrap()
9643            .inner
9644            .put(&staging_path, manifest_data.into())
9645            .await
9646            .unwrap();
9647
9648        // First create version 2 (should succeed)
9649        let mut create_version_req = CreateTableVersionRequest::new(2, staging_path.to_string());
9650        create_version_req.id = Some(table_id.clone());
9651        create_version_req.naming_scheme = Some("V2".to_string());
9652        let first_result = namespace.create_table_version(create_version_req).await;
9653        assert!(
9654            first_result.is_ok(),
9655            "First create_table_version for version 2 should succeed: {:?}",
9656            first_result
9657        );
9658
9659        // Get the path from the response for verification
9660        let version_2_path = Path::parse(
9661            &first_result
9662                .unwrap()
9663                .version
9664                .expect("response should contain version info")
9665                .manifest_path,
9666        )
9667        .unwrap();
9668
9669        // Create version 2 again (should fail - conflict)
9670        let mut create_version_req = CreateTableVersionRequest::new(2, staging_path.to_string());
9671        create_version_req.id = Some(table_id.clone());
9672        create_version_req.naming_scheme = Some("V2".to_string());
9673
9674        let result = namespace.create_table_version(create_version_req).await;
9675        assert!(
9676            result.is_err(),
9677            "create_table_version should fail for existing version"
9678        );
9679
9680        // Verify version 2 still exists using the dataset's object_store
9681        let head_result = dataset
9682            .object_store(None)
9683            .await
9684            .unwrap()
9685            .inner
9686            .head(&version_2_path)
9687            .await;
9688        assert!(
9689            head_result.is_ok(),
9690            "Version 2 manifest should still exist at {}",
9691            version_2_path
9692        );
9693    }
9694
9695    #[tokio::test]
9696    async fn test_create_table_version_table_not_found() {
9697        use lance_namespace::models::CreateTableVersionRequest;
9698
9699        let temp_dir = TempStdDir::default();
9700        let temp_path = temp_dir.to_str().unwrap();
9701
9702        let namespace = DirectoryNamespaceBuilder::new(temp_path)
9703            .table_version_tracking_enabled(true)
9704            .build()
9705            .await
9706            .unwrap();
9707
9708        // Try to create version for non-existent table
9709        let mut create_version_req =
9710            CreateTableVersionRequest::new(1, "/some/staging/path".to_string());
9711        create_version_req.id = Some(vec!["non_existent_table".to_string()]);
9712
9713        let result = namespace.create_table_version(create_version_req).await;
9714        assert!(
9715            result.is_err(),
9716            "create_table_version should fail for non-existent table"
9717        );
9718        let err_msg = result.unwrap_err().to_string();
9719        assert!(
9720            err_msg.contains("Table not found"),
9721            "Error should mention table not found, got: {}",
9722            err_msg
9723        );
9724    }
9725
9726    /// End-to-end integration test module for table version tracking.
9727    mod e2e_table_version_tracking {
9728        use super::*;
9729        use std::sync::atomic::{AtomicUsize, Ordering};
9730
9731        /// Tracking wrapper around a namespace that counts method invocations.
9732        struct TrackingNamespace {
9733            inner: DirectoryNamespace,
9734            create_table_version_count: AtomicUsize,
9735            describe_table_version_count: AtomicUsize,
9736            list_table_versions_count: AtomicUsize,
9737        }
9738
9739        impl TrackingNamespace {
9740            fn new(inner: DirectoryNamespace) -> Self {
9741                Self {
9742                    inner,
9743                    create_table_version_count: AtomicUsize::new(0),
9744                    describe_table_version_count: AtomicUsize::new(0),
9745                    list_table_versions_count: AtomicUsize::new(0),
9746                }
9747            }
9748
9749            fn create_table_version_calls(&self) -> usize {
9750                self.create_table_version_count.load(Ordering::SeqCst)
9751            }
9752
9753            fn describe_table_version_calls(&self) -> usize {
9754                self.describe_table_version_count.load(Ordering::SeqCst)
9755            }
9756
9757            fn list_table_versions_calls(&self) -> usize {
9758                self.list_table_versions_count.load(Ordering::SeqCst)
9759            }
9760        }
9761
9762        impl std::fmt::Debug for TrackingNamespace {
9763            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9764                f.debug_struct("TrackingNamespace")
9765                    .field(
9766                        "create_table_version_calls",
9767                        &self.create_table_version_calls(),
9768                    )
9769                    .finish()
9770            }
9771        }
9772
9773        #[async_trait]
9774        impl LanceNamespace for TrackingNamespace {
9775            async fn create_namespace(
9776                &self,
9777                request: CreateNamespaceRequest,
9778            ) -> Result<CreateNamespaceResponse> {
9779                self.inner.create_namespace(request).await
9780            }
9781
9782            async fn describe_namespace(
9783                &self,
9784                request: DescribeNamespaceRequest,
9785            ) -> Result<DescribeNamespaceResponse> {
9786                self.inner.describe_namespace(request).await
9787            }
9788
9789            async fn namespace_exists(&self, request: NamespaceExistsRequest) -> Result<()> {
9790                self.inner.namespace_exists(request).await
9791            }
9792
9793            async fn list_namespaces(
9794                &self,
9795                request: ListNamespacesRequest,
9796            ) -> Result<ListNamespacesResponse> {
9797                self.inner.list_namespaces(request).await
9798            }
9799
9800            async fn drop_namespace(
9801                &self,
9802                request: DropNamespaceRequest,
9803            ) -> Result<DropNamespaceResponse> {
9804                self.inner.drop_namespace(request).await
9805            }
9806
9807            async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
9808                self.inner.list_tables(request).await
9809            }
9810
9811            async fn describe_table(
9812                &self,
9813                request: DescribeTableRequest,
9814            ) -> Result<DescribeTableResponse> {
9815                self.inner.describe_table(request).await
9816            }
9817
9818            async fn table_exists(&self, request: TableExistsRequest) -> Result<()> {
9819                self.inner.table_exists(request).await
9820            }
9821
9822            async fn drop_table(&self, request: DropTableRequest) -> Result<DropTableResponse> {
9823                self.inner.drop_table(request).await
9824            }
9825
9826            async fn create_table(
9827                &self,
9828                request: CreateTableRequest,
9829                request_data: Bytes,
9830            ) -> Result<CreateTableResponse> {
9831                self.inner.create_table(request, request_data).await
9832            }
9833
9834            async fn declare_table(
9835                &self,
9836                request: DeclareTableRequest,
9837            ) -> Result<DeclareTableResponse> {
9838                self.inner.declare_table(request).await
9839            }
9840
9841            async fn list_table_versions(
9842                &self,
9843                request: ListTableVersionsRequest,
9844            ) -> Result<ListTableVersionsResponse> {
9845                self.list_table_versions_count
9846                    .fetch_add(1, Ordering::SeqCst);
9847                self.inner.list_table_versions(request).await
9848            }
9849
9850            async fn create_table_version(
9851                &self,
9852                request: CreateTableVersionRequest,
9853            ) -> Result<CreateTableVersionResponse> {
9854                self.create_table_version_count
9855                    .fetch_add(1, Ordering::SeqCst);
9856                self.inner.create_table_version(request).await
9857            }
9858
9859            async fn describe_table_version(
9860                &self,
9861                request: DescribeTableVersionRequest,
9862            ) -> Result<DescribeTableVersionResponse> {
9863                self.describe_table_version_count
9864                    .fetch_add(1, Ordering::SeqCst);
9865                self.inner.describe_table_version(request).await
9866            }
9867
9868            async fn batch_delete_table_versions(
9869                &self,
9870                request: BatchDeleteTableVersionsRequest,
9871            ) -> Result<BatchDeleteTableVersionsResponse> {
9872                self.inner.batch_delete_table_versions(request).await
9873            }
9874
9875            fn namespace_id(&self) -> String {
9876                self.inner.namespace_id()
9877            }
9878        }
9879
9880        #[tokio::test]
9881        async fn test_describe_table_returns_managed_versioning() {
9882            use lance_namespace::models::{CreateNamespaceRequest, DescribeTableRequest};
9883
9884            let temp_dir = TempStdDir::default();
9885            let temp_path = temp_dir.to_str().unwrap();
9886
9887            // Create namespace with table_version_tracking_enabled and manifest_enabled
9888            let ns = DirectoryNamespaceBuilder::new(temp_path)
9889                .table_version_tracking_enabled(true)
9890                .manifest_enabled(true)
9891                .build()
9892                .await
9893                .unwrap();
9894
9895            // Create parent namespace
9896            let mut create_ns_req = CreateNamespaceRequest::new();
9897            create_ns_req.id = Some(vec!["workspace".to_string()]);
9898            ns.create_namespace(create_ns_req).await.unwrap();
9899
9900            // Create a table with multi-level ID (namespace + table)
9901            let schema = create_test_schema();
9902            let ipc_data = create_test_ipc_data(&schema);
9903            let mut create_req = CreateTableRequest::new();
9904            create_req.id = Some(vec!["workspace".to_string(), "test_table".to_string()]);
9905            ns.create_table(create_req, bytes::Bytes::from(ipc_data))
9906                .await
9907                .unwrap();
9908
9909            // Describe table should return managed_versioning=true
9910            let mut describe_req = DescribeTableRequest::new();
9911            describe_req.id = Some(vec!["workspace".to_string(), "test_table".to_string()]);
9912            let describe_resp = ns.describe_table(describe_req).await.unwrap();
9913
9914            // managed_versioning should be true
9915            assert_eq!(
9916                describe_resp.managed_versioning,
9917                Some(true),
9918                "managed_versioning should be true when table_version_tracking_enabled=true"
9919            );
9920        }
9921
9922        #[tokio::test]
9923        async fn test_external_manifest_store_invokes_namespace_apis() {
9924            use arrow::array::{Int32Array, StringArray};
9925            use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9926            use arrow::record_batch::RecordBatch;
9927            use lance::Dataset;
9928            use lance::dataset::builder::DatasetBuilder;
9929            use lance::dataset::{WriteMode, WriteParams};
9930            use lance_namespace::models::CreateNamespaceRequest;
9931
9932            let temp_dir = TempStdDir::default();
9933            let temp_path = temp_dir.to_str().unwrap();
9934
9935            // Create namespace with table_version_tracking_enabled and manifest_enabled
9936            let inner_ns = DirectoryNamespaceBuilder::new(temp_path)
9937                .table_version_tracking_enabled(true)
9938                .manifest_enabled(true)
9939                .build()
9940                .await
9941                .unwrap();
9942
9943            let tracking_ns = Arc::new(TrackingNamespace::new(inner_ns));
9944            let ns: Arc<dyn LanceNamespace> = tracking_ns.clone();
9945
9946            // Create parent namespace
9947            let mut create_ns_req = CreateNamespaceRequest::new();
9948            create_ns_req.id = Some(vec!["workspace".to_string()]);
9949            ns.create_namespace(create_ns_req).await.unwrap();
9950
9951            // Create a table with multi-level ID (namespace + table)
9952            let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9953
9954            // Create some initial data
9955            let arrow_schema = Arc::new(ArrowSchema::new(vec![
9956                Field::new("id", DataType::Int32, false),
9957                Field::new("name", DataType::Utf8, true),
9958            ]));
9959            let batch = RecordBatch::try_new(
9960                arrow_schema.clone(),
9961                vec![
9962                    Arc::new(Int32Array::from(vec![1, 2, 3])),
9963                    Arc::new(StringArray::from(vec!["a", "b", "c"])),
9964                ],
9965            )
9966            .unwrap();
9967
9968            // Create a table using write_into_namespace
9969            let batches = RecordBatchIterator::new(vec![Ok(batch.clone())], arrow_schema.clone());
9970            let write_params = WriteParams {
9971                mode: WriteMode::Create,
9972                ..Default::default()
9973            };
9974            let mut dataset = Dataset::write_into_namespace(
9975                batches,
9976                ns.clone(),
9977                table_id.clone(),
9978                Some(write_params),
9979            )
9980            .await
9981            .unwrap();
9982            assert_eq!(dataset.version().version, 1);
9983
9984            // Verify create_table_version was called once during initial write_into_namespace
9985            assert_eq!(
9986                tracking_ns.create_table_version_calls(),
9987                1,
9988                "create_table_version should have been called once during initial write_into_namespace"
9989            );
9990
9991            // Append data - this should call create_table_version again
9992            let append_batch = RecordBatch::try_new(
9993                arrow_schema.clone(),
9994                vec![
9995                    Arc::new(Int32Array::from(vec![4, 5, 6])),
9996                    Arc::new(StringArray::from(vec!["d", "e", "f"])),
9997                ],
9998            )
9999            .unwrap();
10000            let append_batches = RecordBatchIterator::new(vec![Ok(append_batch)], arrow_schema);
10001            dataset.append(append_batches, None).await.unwrap();
10002
10003            assert_eq!(
10004                tracking_ns.create_table_version_calls(),
10005                2,
10006                "create_table_version should have been called twice (once for create, once for append)"
10007            );
10008
10009            // checkout_latest should call list_table_versions exactly once
10010            let initial_list_calls = tracking_ns.list_table_versions_calls();
10011            let latest_dataset = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
10012                .await
10013                .unwrap()
10014                .load()
10015                .await
10016                .unwrap();
10017            assert_eq!(latest_dataset.version().version, 2);
10018            assert_eq!(
10019                tracking_ns.list_table_versions_calls(),
10020                initial_list_calls + 1,
10021                "list_table_versions should have been called exactly once during checkout_latest"
10022            );
10023
10024            // checkout to specific version should call describe_table_version exactly once
10025            let initial_describe_calls = tracking_ns.describe_table_version_calls();
10026            let v1_dataset = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
10027                .await
10028                .unwrap()
10029                .with_version(1)
10030                .load()
10031                .await
10032                .unwrap();
10033            assert_eq!(v1_dataset.version().version, 1);
10034            assert_eq!(
10035                tracking_ns.describe_table_version_calls(),
10036                initial_describe_calls + 1,
10037                "describe_table_version should have been called exactly once during checkout to version 1"
10038            );
10039        }
10040
10041        #[tokio::test]
10042        async fn test_dataset_commit_with_external_manifest_store() {
10043            use arrow::array::{Int32Array, StringArray};
10044            use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
10045            use arrow::record_batch::RecordBatch;
10046            use futures::TryStreamExt;
10047            use lance::dataset::{Dataset, WriteMode, WriteParams};
10048            use lance_namespace::models::CreateNamespaceRequest;
10049            use lance_table::io::commit::ManifestNamingScheme;
10050
10051            let temp_dir = TempStdDir::default();
10052            let temp_path = temp_dir.to_str().unwrap();
10053
10054            // Create namespace with table_version_tracking_enabled and manifest_enabled
10055            let inner_ns = DirectoryNamespaceBuilder::new(temp_path)
10056                .table_version_tracking_enabled(true)
10057                .manifest_enabled(true)
10058                .build()
10059                .await
10060                .unwrap();
10061
10062            let tracking_ns: Arc<dyn LanceNamespace> = Arc::new(TrackingNamespace::new(inner_ns));
10063
10064            // Create parent namespace
10065            let mut create_ns_req = CreateNamespaceRequest::new();
10066            create_ns_req.id = Some(vec!["workspace".to_string()]);
10067            tracking_ns.create_namespace(create_ns_req).await.unwrap();
10068
10069            // Create a table using write_into_namespace
10070            let table_id = vec!["workspace".to_string(), "test_table".to_string()];
10071            let arrow_schema = Arc::new(ArrowSchema::new(vec![
10072                Field::new("id", DataType::Int32, false),
10073                Field::new("name", DataType::Utf8, true),
10074            ]));
10075            let batch = RecordBatch::try_new(
10076                arrow_schema.clone(),
10077                vec![
10078                    Arc::new(Int32Array::from(vec![1, 2, 3])),
10079                    Arc::new(StringArray::from(vec!["a", "b", "c"])),
10080                ],
10081            )
10082            .unwrap();
10083            let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
10084            let write_params = WriteParams {
10085                mode: WriteMode::Create,
10086                ..Default::default()
10087            };
10088            let dataset = Dataset::write_into_namespace(
10089                batches,
10090                tracking_ns.clone(),
10091                table_id.clone(),
10092                Some(write_params),
10093            )
10094            .await
10095            .unwrap();
10096            assert_eq!(dataset.version().version, 1);
10097
10098            // Append data using write_into_namespace (APPEND mode)
10099            let batch2 = RecordBatch::try_new(
10100                arrow_schema.clone(),
10101                vec![
10102                    Arc::new(Int32Array::from(vec![4, 5, 6])),
10103                    Arc::new(StringArray::from(vec!["d", "e", "f"])),
10104                ],
10105            )
10106            .unwrap();
10107            let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema);
10108            let write_params = WriteParams {
10109                mode: WriteMode::Append,
10110                ..Default::default()
10111            };
10112            Dataset::write_into_namespace(
10113                batches,
10114                tracking_ns.clone(),
10115                table_id.clone(),
10116                Some(write_params),
10117            )
10118            .await
10119            .unwrap();
10120
10121            // Verify version 2 was created using the dataset's object_store
10122            // List manifests in the versions directory to find the V2 named manifest
10123            let manifest_metas: Vec<_> = dataset
10124                .object_store(None)
10125                .await
10126                .unwrap()
10127                .inner
10128                .list(Some(&dataset.versions_dir()))
10129                .try_collect()
10130                .await
10131                .unwrap();
10132            let version_2_found = manifest_metas.iter().any(|m| {
10133                m.location
10134                    .filename()
10135                    .map(|f| {
10136                        f.ends_with(".manifest")
10137                            && ManifestNamingScheme::V2.parse_version(f) == Some(2)
10138                    })
10139                    .unwrap_or(false)
10140            });
10141            assert!(
10142                version_2_found,
10143                "Version 2 manifest should exist in versions directory"
10144            );
10145        }
10146
10147        /// Helper: create a namespace and a table with some rows, returning (namespace, table_id)
10148        async fn create_ns_with_table() -> (DirectoryNamespace, TempStdDir, Vec<String>) {
10149            use arrow::array::{Int32Array, StringArray};
10150            use arrow::ipc::writer::StreamWriter;
10151
10152            let (namespace, temp_dir) = create_test_namespace().await;
10153
10154            let schema = create_test_schema();
10155            let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10156            let arrow_schema = Arc::new(arrow_schema);
10157
10158            let id_array = Int32Array::from(vec![1, 2, 3]);
10159            let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
10160            let batch = arrow::record_batch::RecordBatch::try_new(
10161                arrow_schema.clone(),
10162                vec![Arc::new(id_array), Arc::new(name_array)],
10163            )
10164            .unwrap();
10165
10166            let mut buffer = Vec::new();
10167            {
10168                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10169                writer.write(&batch).unwrap();
10170                writer.finish().unwrap();
10171            }
10172
10173            let mut request = CreateTableRequest::new();
10174            let table_id = vec!["test_ops_table".to_string()];
10175            request.id = Some(table_id.clone());
10176
10177            namespace
10178                .create_table(request, Bytes::from(buffer))
10179                .await
10180                .unwrap();
10181
10182            (namespace, temp_dir, table_id)
10183        }
10184
10185        #[tokio::test]
10186        async fn test_count_table_rows_basic() {
10187            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10188
10189            let request = CountTableRowsRequest {
10190                id: Some(table_id),
10191                version: None,
10192                predicate: None,
10193                ..Default::default()
10194            };
10195
10196            let count = namespace.count_table_rows(request).await.unwrap();
10197            assert_eq!(count, 3);
10198        }
10199
10200        #[tokio::test]
10201        async fn test_count_table_rows_with_predicate() {
10202            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10203
10204            let request = CountTableRowsRequest {
10205                id: Some(table_id),
10206                version: None,
10207                predicate: Some("id > 1".to_string()),
10208                ..Default::default()
10209            };
10210
10211            let count = namespace.count_table_rows(request).await.unwrap();
10212            assert_eq!(count, 2);
10213        }
10214
10215        #[tokio::test]
10216        async fn test_query_table_invalid_distance_type() {
10217            let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10218
10219            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10220                single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10221                multi_vector: None,
10222            });
10223
10224            let request = QueryTableRequest {
10225                id: Some(table_id),
10226                k: 2,
10227                vector,
10228                vector_column: Some("vector".to_string()),
10229                distance_type: Some("invalid_metric".to_string()),
10230                filter: None,
10231                offset: None,
10232                version: None,
10233                ..Default::default()
10234            };
10235
10236            let result = namespace.query_table(request).await;
10237            assert!(result.is_err());
10238            let err_msg = result.unwrap_err().to_string();
10239            assert!(
10240                err_msg.contains("Unknown distance type"),
10241                "Expected error about unknown distance type, got: {}",
10242                err_msg
10243            );
10244        }
10245
10246        #[tokio::test]
10247        async fn test_insert_into_table_append() {
10248            use arrow::array::{Int32Array, StringArray};
10249            use arrow::ipc::writer::StreamWriter;
10250
10251            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10252
10253            // Prepare new data to insert
10254            let schema = create_test_schema();
10255            let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10256            let arrow_schema = Arc::new(arrow_schema);
10257
10258            let id_array = Int32Array::from(vec![4, 5]);
10259            let name_array = StringArray::from(vec!["Dave", "Eve"]);
10260            let batch = arrow::record_batch::RecordBatch::try_new(
10261                arrow_schema.clone(),
10262                vec![Arc::new(id_array), Arc::new(name_array)],
10263            )
10264            .unwrap();
10265
10266            let mut buffer = Vec::new();
10267            {
10268                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10269                writer.write(&batch).unwrap();
10270                writer.finish().unwrap();
10271            }
10272
10273            let request = InsertIntoTableRequest {
10274                id: Some(table_id.clone()),
10275                mode: Some("append".to_string()),
10276                ..Default::default()
10277            };
10278
10279            let response = namespace
10280                .insert_into_table(request, Bytes::from(buffer))
10281                .await
10282                .unwrap();
10283            assert!(response.transaction_id.is_none());
10284
10285            // Verify total rows
10286            let count_req = CountTableRowsRequest {
10287                id: Some(table_id),
10288                version: None,
10289                predicate: None,
10290                ..Default::default()
10291            };
10292            let count = namespace.count_table_rows(count_req).await.unwrap();
10293            assert_eq!(count, 5);
10294        }
10295
10296        #[tokio::test]
10297        async fn test_insert_into_table_overwrite() {
10298            use arrow::array::{Int32Array, StringArray};
10299            use arrow::ipc::writer::StreamWriter;
10300
10301            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10302
10303            let schema = create_test_schema();
10304            let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10305            let arrow_schema = Arc::new(arrow_schema);
10306
10307            let id_array = Int32Array::from(vec![10, 20]);
10308            let name_array = StringArray::from(vec!["X", "Y"]);
10309            let batch = arrow::record_batch::RecordBatch::try_new(
10310                arrow_schema.clone(),
10311                vec![Arc::new(id_array), Arc::new(name_array)],
10312            )
10313            .unwrap();
10314
10315            let mut buffer = Vec::new();
10316            {
10317                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10318                writer.write(&batch).unwrap();
10319                writer.finish().unwrap();
10320            }
10321
10322            let request = InsertIntoTableRequest {
10323                id: Some(table_id.clone()),
10324                mode: Some("overwrite".to_string()),
10325                ..Default::default()
10326            };
10327
10328            namespace
10329                .insert_into_table(request, Bytes::from(buffer))
10330                .await
10331                .unwrap();
10332
10333            // Verify overwrite: only 2 rows remain
10334            let count_req = CountTableRowsRequest {
10335                id: Some(table_id),
10336                version: None,
10337                predicate: None,
10338                ..Default::default()
10339            };
10340            let count = namespace.count_table_rows(count_req).await.unwrap();
10341            assert_eq!(count, 2);
10342        }
10343
10344        #[tokio::test]
10345        async fn test_insert_into_table_empty_data() {
10346            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10347
10348            let request = InsertIntoTableRequest {
10349                id: Some(table_id),
10350                mode: None,
10351                ..Default::default()
10352            };
10353
10354            let result = namespace.insert_into_table(request, Bytes::new()).await;
10355            assert!(result.is_err());
10356            assert!(
10357                result
10358                    .unwrap_err()
10359                    .to_string()
10360                    .contains("Arrow IPC stream) is required")
10361            );
10362        }
10363
10364        #[tokio::test]
10365        async fn test_insert_into_table_with_storage_options() {
10366            use arrow::array::{Int32Array, StringArray};
10367            use arrow::ipc::writer::StreamWriter;
10368
10369            let temp_dir = TempStdDir::default();
10370
10371            // Build namespace with a (no-op) storage option so self.storage_options is Some
10372            let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
10373                .storage_option("allow_http", "true")
10374                .build()
10375                .await
10376                .unwrap();
10377
10378            // Create a table first
10379            let schema = create_test_schema();
10380            let ipc_data = create_test_ipc_data(&schema);
10381            let mut create_req = CreateTableRequest::new();
10382            let table_id = vec!["so_table".to_string()];
10383            create_req.id = Some(table_id.clone());
10384            namespace
10385                .create_table(create_req, Bytes::from(ipc_data))
10386                .await
10387                .unwrap();
10388
10389            // Insert with storage_options present — covers store_params closure
10390            let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10391            let arrow_schema = Arc::new(arrow_schema);
10392
10393            let id_array = Int32Array::from(vec![10, 20]);
10394            let name_array = StringArray::from(vec!["X", "Y"]);
10395            let batch = arrow::record_batch::RecordBatch::try_new(
10396                arrow_schema.clone(),
10397                vec![Arc::new(id_array), Arc::new(name_array)],
10398            )
10399            .unwrap();
10400
10401            let mut buffer = Vec::new();
10402            {
10403                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10404                writer.write(&batch).unwrap();
10405                writer.finish().unwrap();
10406            }
10407
10408            let request = InsertIntoTableRequest {
10409                id: Some(table_id.clone()),
10410                mode: Some("append".to_string()),
10411                ..Default::default()
10412            };
10413
10414            let response = namespace
10415                .insert_into_table(request, Bytes::from(buffer))
10416                .await
10417                .unwrap();
10418            assert!(response.transaction_id.is_none());
10419
10420            // Verify rows were inserted
10421            let count_req = CountTableRowsRequest {
10422                id: Some(table_id),
10423                version: None,
10424                predicate: None,
10425                ..Default::default()
10426            };
10427            let count = namespace.count_table_rows(count_req).await.unwrap();
10428            assert_eq!(count, 2);
10429        }
10430
10431        #[tokio::test]
10432        async fn test_query_table_basic() {
10433            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10434
10435            let request = QueryTableRequest {
10436                id: Some(table_id),
10437                k: 10,
10438                filter: None,
10439                offset: None,
10440                version: None,
10441                ..Default::default()
10442            };
10443
10444            let bytes = namespace.query_table(request).await.unwrap();
10445
10446            // Decode IPC and verify
10447            let cursor = Cursor::new(bytes.to_vec());
10448            let reader = FileReader::try_new(cursor, None).unwrap();
10449            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10450            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10451            assert_eq!(total_rows, 3);
10452        }
10453
10454        #[tokio::test]
10455        async fn test_query_table_with_filter() {
10456            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10457
10458            let request = QueryTableRequest {
10459                id: Some(table_id),
10460                k: 10,
10461                filter: Some("id <= 2".to_string()),
10462                offset: None,
10463                version: None,
10464                ..Default::default()
10465            };
10466
10467            let bytes = namespace.query_table(request).await.unwrap();
10468
10469            let cursor = Cursor::new(bytes.to_vec());
10470            let reader = FileReader::try_new(cursor, None).unwrap();
10471            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10472            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10473            assert_eq!(total_rows, 2);
10474        }
10475
10476        #[tokio::test]
10477        async fn test_query_table_with_limit_and_offset() {
10478            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10479
10480            let request = QueryTableRequest {
10481                id: Some(table_id),
10482                k: 2,
10483                filter: None,
10484                offset: Some(1),
10485                version: None,
10486                ..Default::default()
10487            };
10488
10489            let bytes = namespace.query_table(request).await.unwrap();
10490
10491            let cursor = Cursor::new(bytes.to_vec());
10492            let reader = FileReader::try_new(cursor, None).unwrap();
10493            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10494            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10495            assert_eq!(total_rows, 2);
10496        }
10497
10498        #[tokio::test]
10499        async fn test_query_table_no_limit() {
10500            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10501
10502            // k=0 means no limit
10503            let request = QueryTableRequest {
10504                id: Some(table_id),
10505                k: 0,
10506                filter: None,
10507                offset: None,
10508                version: None,
10509                ..Default::default()
10510            };
10511
10512            let bytes = namespace.query_table(request).await.unwrap();
10513
10514            let cursor = Cursor::new(bytes.to_vec());
10515            let reader = FileReader::try_new(cursor, None).unwrap();
10516            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10517            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10518            assert_eq!(total_rows, 3);
10519        }
10520
10521        #[tokio::test]
10522        async fn test_query_table_with_columns() {
10523            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10524
10525            let columns = Box::new(lance_namespace::models::QueryTableRequestColumns {
10526                column_names: Some(vec!["id".to_string()]),
10527                column_aliases: None,
10528            });
10529
10530            let request = QueryTableRequest {
10531                id: Some(table_id),
10532                k: 10,
10533                filter: None,
10534                offset: None,
10535                version: None,
10536                columns: Some(columns),
10537                ..Default::default()
10538            };
10539
10540            let bytes = namespace.query_table(request).await.unwrap();
10541
10542            let cursor = Cursor::new(bytes.to_vec());
10543            let reader = FileReader::try_new(cursor, None).unwrap();
10544            let schema = reader.schema();
10545            assert_eq!(schema.fields().len(), 1);
10546            assert_eq!(schema.field(0).name(), "id");
10547            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10548            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10549            assert_eq!(total_rows, 3);
10550        }
10551
10552        #[tokio::test]
10553        async fn test_count_table_rows_with_version() {
10554            use arrow::array::{Int32Array, StringArray};
10555            use arrow::ipc::writer::StreamWriter;
10556
10557            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10558
10559            // Insert more data to create version 2
10560            let schema = create_test_schema();
10561            let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10562            let arrow_schema = Arc::new(arrow_schema);
10563
10564            let id_array = Int32Array::from(vec![4, 5]);
10565            let name_array = StringArray::from(vec!["Dave", "Eve"]);
10566            let batch = arrow::record_batch::RecordBatch::try_new(
10567                arrow_schema.clone(),
10568                vec![Arc::new(id_array), Arc::new(name_array)],
10569            )
10570            .unwrap();
10571
10572            let mut buffer = Vec::new();
10573            {
10574                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10575                writer.write(&batch).unwrap();
10576                writer.finish().unwrap();
10577            }
10578
10579            let request = InsertIntoTableRequest {
10580                id: Some(table_id.clone()),
10581                mode: None,
10582                ..Default::default()
10583            };
10584            namespace
10585                .insert_into_table(request, Bytes::from(buffer))
10586                .await
10587                .unwrap();
10588
10589            // Version 1 should have 3 rows
10590            let count_req = CountTableRowsRequest {
10591                id: Some(table_id.clone()),
10592                version: Some(1),
10593                predicate: None,
10594                ..Default::default()
10595            };
10596            let count = namespace.count_table_rows(count_req).await.unwrap();
10597            assert_eq!(count, 3);
10598
10599            // Latest version should have 5 rows
10600            let count_req = CountTableRowsRequest {
10601                id: Some(table_id),
10602                version: None,
10603                predicate: None,
10604                ..Default::default()
10605            };
10606            let count = namespace.count_table_rows(count_req).await.unwrap();
10607            assert_eq!(count, 5);
10608        }
10609
10610        #[tokio::test]
10611        async fn test_query_table_with_version() {
10612            use arrow::array::{Int32Array, StringArray};
10613            use arrow::ipc::writer::StreamWriter;
10614
10615            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10616
10617            // Insert more data to create version 2
10618            let schema = create_test_schema();
10619            let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10620            let arrow_schema = Arc::new(arrow_schema);
10621
10622            let id_array = Int32Array::from(vec![4, 5]);
10623            let name_array = StringArray::from(vec!["Dave", "Eve"]);
10624            let batch = arrow::record_batch::RecordBatch::try_new(
10625                arrow_schema.clone(),
10626                vec![Arc::new(id_array), Arc::new(name_array)],
10627            )
10628            .unwrap();
10629
10630            let mut buffer = Vec::new();
10631            {
10632                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10633                writer.write(&batch).unwrap();
10634                writer.finish().unwrap();
10635            }
10636
10637            let request = InsertIntoTableRequest {
10638                id: Some(table_id.clone()),
10639                mode: None,
10640                ..Default::default()
10641            };
10642            namespace
10643                .insert_into_table(request, Bytes::from(buffer))
10644                .await
10645                .unwrap();
10646
10647            // Query version 1 should return 3 rows
10648            let request = QueryTableRequest {
10649                id: Some(table_id.clone()),
10650                k: 100,
10651                filter: None,
10652                offset: None,
10653                version: Some(1),
10654                ..Default::default()
10655            };
10656
10657            let bytes = namespace.query_table(request).await.unwrap();
10658            let cursor = Cursor::new(bytes.to_vec());
10659            let reader = FileReader::try_new(cursor, None).unwrap();
10660            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10661            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10662            assert_eq!(total_rows, 3);
10663
10664            // Query latest version should return 5 rows
10665            let request = QueryTableRequest {
10666                id: Some(table_id),
10667                k: 100,
10668                filter: None,
10669                offset: None,
10670                version: None,
10671                ..Default::default()
10672            };
10673
10674            let bytes = namespace.query_table(request).await.unwrap();
10675            let cursor = Cursor::new(bytes.to_vec());
10676            let reader = FileReader::try_new(cursor, None).unwrap();
10677            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10678            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10679            assert_eq!(total_rows, 5);
10680        }
10681
10682        /// Helper to create a namespace with a table that has a vector column for
10683        /// vector search tests.
10684        async fn create_ns_with_vector_table() -> (DirectoryNamespace, TempStdDir, Vec<String>) {
10685            use arrow::array::{FixedSizeListArray, Float32Array, Int32Array};
10686            use arrow::ipc::writer::StreamWriter;
10687
10688            let (namespace, temp_dir) = create_test_namespace().await;
10689
10690            // Build schema: id (int32), vector (fixed_size_list<float32>[4])
10691            let arrow_schema = Arc::new(arrow::datatypes::Schema::new(vec![
10692                arrow::datatypes::Field::new("id", arrow::datatypes::DataType::Int32, false),
10693                arrow::datatypes::Field::new(
10694                    "vector",
10695                    arrow::datatypes::DataType::FixedSizeList(
10696                        Arc::new(arrow::datatypes::Field::new(
10697                            "item",
10698                            arrow::datatypes::DataType::Float32,
10699                            true,
10700                        )),
10701                        4,
10702                    ),
10703                    true,
10704                ),
10705            ]));
10706
10707            let id_array = Int32Array::from(vec![1, 2, 3]);
10708            let values = Float32Array::from(vec![
10709                1.0, 0.0, 0.0, 0.0, // vector for id=1
10710                0.0, 1.0, 0.0, 0.0, // vector for id=2
10711                0.0, 0.0, 1.0, 0.0, // vector for id=3
10712            ]);
10713            let vector_array = FixedSizeListArray::try_new(
10714                Arc::new(arrow::datatypes::Field::new(
10715                    "item",
10716                    arrow::datatypes::DataType::Float32,
10717                    true,
10718                )),
10719                4,
10720                Arc::new(values),
10721                None,
10722            )
10723            .unwrap();
10724
10725            let batch = arrow::record_batch::RecordBatch::try_new(
10726                arrow_schema.clone(),
10727                vec![Arc::new(id_array), Arc::new(vector_array)],
10728            )
10729            .unwrap();
10730
10731            let mut buffer = Vec::new();
10732            {
10733                let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10734                writer.write(&batch).unwrap();
10735                writer.finish().unwrap();
10736            }
10737
10738            // Write as a Lance dataset directly
10739            let table_name = "vector_table";
10740            let table_uri = format!("{}/{}.lance", temp_dir.to_str().unwrap(), table_name);
10741            let reader = arrow::record_batch::RecordBatchIterator::new(
10742                vec![Ok(batch)],
10743                arrow_schema.clone(),
10744            );
10745            Dataset::write(reader, &table_uri, None).await.unwrap();
10746
10747            let table_id = vec![table_name.to_string()];
10748            (namespace, temp_dir, table_id)
10749        }
10750
10751        #[tokio::test]
10752        async fn test_query_table_vector_search() {
10753            let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10754
10755            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10756                single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10757                multi_vector: None,
10758            });
10759
10760            let request = QueryTableRequest {
10761                id: Some(table_id),
10762                k: 2,
10763                vector,
10764                filter: None,
10765                offset: None,
10766                version: None,
10767                ..Default::default()
10768            };
10769
10770            let bytes = namespace.query_table(request).await.unwrap();
10771
10772            let cursor = Cursor::new(bytes.to_vec());
10773            let reader = FileReader::try_new(cursor, None).unwrap();
10774            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10775            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10776            assert_eq!(total_rows, 2);
10777        }
10778
10779        #[tokio::test]
10780        async fn test_query_table_vector_search_with_distance_type() {
10781            let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10782
10783            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10784                single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10785                multi_vector: None,
10786            });
10787
10788            let request = QueryTableRequest {
10789                id: Some(table_id),
10790                k: 3,
10791                vector,
10792                filter: None,
10793                offset: None,
10794                version: None,
10795                distance_type: Some("cosine".to_string()),
10796                ..Default::default()
10797            };
10798
10799            let bytes = namespace.query_table(request).await.unwrap();
10800
10801            let cursor = Cursor::new(bytes.to_vec());
10802            let reader = FileReader::try_new(cursor, None).unwrap();
10803            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10804            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10805            assert_eq!(total_rows, 3);
10806        }
10807
10808        #[tokio::test]
10809        async fn test_query_table_vector_search_with_filter() {
10810            let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10811
10812            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10813                single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10814                multi_vector: None,
10815            });
10816
10817            let request = QueryTableRequest {
10818                id: Some(table_id),
10819                k: 10,
10820                vector,
10821                filter: Some("id <= 2".to_string()),
10822                offset: None,
10823                version: None,
10824                ..Default::default()
10825            };
10826
10827            let bytes = namespace.query_table(request).await.unwrap();
10828
10829            let cursor = Cursor::new(bytes.to_vec());
10830            let reader = FileReader::try_new(cursor, None).unwrap();
10831            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10832            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10833            assert!(total_rows <= 2);
10834        }
10835
10836        #[tokio::test]
10837        async fn test_query_table_vector_search_with_nprobes_and_refine() {
10838            let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10839
10840            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10841                single_vector: Some(vec![0.0, 1.0, 0.0, 0.0]),
10842                multi_vector: None,
10843            });
10844
10845            let request = QueryTableRequest {
10846                id: Some(table_id),
10847                k: 2,
10848                vector,
10849                filter: None,
10850                offset: None,
10851                version: None,
10852                nprobes: Some(1),
10853                refine_factor: Some(1),
10854                prefilter: Some(true),
10855                ..Default::default()
10856            };
10857
10858            let bytes = namespace.query_table(request).await.unwrap();
10859
10860            let cursor = Cursor::new(bytes.to_vec());
10861            let reader = FileReader::try_new(cursor, None).unwrap();
10862            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10863            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10864            assert_eq!(total_rows, 2);
10865        }
10866
10867        #[tokio::test]
10868        async fn test_namespace_id() {
10869            let (namespace, _temp_dir) = create_test_namespace().await;
10870            let id = namespace.namespace_id();
10871            assert!(id.contains("DirectoryNamespace"));
10872            assert!(id.contains("root"));
10873        }
10874
10875        #[tokio::test]
10876        async fn test_query_table_empty_table() {
10877            let (namespace, _temp_dir) = create_test_namespace().await;
10878
10879            // Create table with empty IPC data (schema only, no rows)
10880            let schema = create_test_schema();
10881            let ipc_data = create_test_ipc_data(&schema);
10882            let mut create_request = CreateTableRequest::new();
10883            create_request.id = Some(vec!["empty_table".to_string()]);
10884            namespace
10885                .create_table(create_request, bytes::Bytes::from(ipc_data))
10886                .await
10887                .unwrap();
10888
10889            // Query the empty table — should hit the "no batches" else branch
10890            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10891                single_vector: None,
10892                multi_vector: None,
10893            });
10894            let request = QueryTableRequest {
10895                id: Some(vec!["empty_table".to_string()]),
10896                k: 10,
10897                vector,
10898                ..Default::default()
10899            };
10900            let bytes = namespace.query_table(request).await.unwrap();
10901
10902            let cursor = Cursor::new(bytes.to_vec());
10903            let reader = FileReader::try_new(cursor, None).unwrap();
10904            let batches: Vec<_> = reader.collect::<std::result::Result<Vec<_>, _>>().unwrap();
10905            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10906            assert_eq!(total_rows, 0, "empty table should yield no rows");
10907        }
10908
10909        #[tokio::test]
10910        async fn test_query_table_with_plain_filter_no_vector() {
10911            let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10912
10913            // Query with filter but no vector (plain scan path + filter)
10914            let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10915                single_vector: None,
10916                multi_vector: None,
10917            });
10918            let request = QueryTableRequest {
10919                id: Some(table_id),
10920                k: 0,
10921                vector,
10922                filter: Some("id > 1".to_string()),
10923                ..Default::default()
10924            };
10925            let bytes = namespace.query_table(request).await.unwrap();
10926
10927            let cursor = Cursor::new(bytes.to_vec());
10928            let reader = FileReader::try_new(cursor, None).unwrap();
10929            let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10930            let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10931            assert!(total_rows > 0);
10932            assert!(total_rows < 3);
10933        }
10934    }
10935
10936    #[tokio::test]
10937    async fn test_list_all_tables() {
10938        use lance_namespace::models::ListTablesRequest;
10939
10940        let (namespace, _temp_dir) = create_test_namespace().await;
10941        create_scalar_table(&namespace, "alpha").await;
10942        create_scalar_table(&namespace, "beta").await;
10943
10944        let request = ListTablesRequest {
10945            id: Some(vec![]),
10946            page_token: None,
10947            limit: None,
10948            ..Default::default()
10949        };
10950        let response = namespace.list_all_tables(request).await.unwrap();
10951        let mut tables = response.tables;
10952        tables.sort();
10953        assert_eq!(tables, vec!["alpha", "beta"]);
10954    }
10955
10956    #[tokio::test]
10957    async fn test_restore_table() {
10958        use lance_namespace::models::RestoreTableRequest;
10959
10960        let (namespace, _temp_dir) = create_test_namespace().await;
10961        create_scalar_table(&namespace, "users").await;
10962
10963        // Create a second version by creating a scalar index (this adds a new version)
10964        create_scalar_index(&namespace, "users", "users_id_idx").await;
10965
10966        let dataset = open_dataset(&namespace, "users").await;
10967        let current_version = dataset.version().version;
10968        assert!(current_version >= 2, "Should have at least 2 versions");
10969
10970        // Restore to version 1
10971        let mut restore_req = RestoreTableRequest::new(1);
10972        restore_req.id = Some(vec!["users".to_string()]);
10973        let response = namespace.restore_table(restore_req).await.unwrap();
10974
10975        // transaction_id should be present (the restore operation)
10976        assert!(
10977            response.transaction_id.is_some(),
10978            "restore_table should return a transaction_id"
10979        );
10980
10981        // Verify the dataset now has a new version (restore creates a new version)
10982        let dataset_after = open_dataset(&namespace, "users").await;
10983        assert!(
10984            dataset_after.version().version > current_version,
10985            "Restore should create a new version"
10986        );
10987    }
10988
10989    #[tokio::test]
10990    async fn test_update_table_schema_metadata() {
10991        use lance_namespace::models::UpdateTableSchemaMetadataRequest;
10992
10993        let (namespace, _temp_dir) = create_test_namespace().await;
10994        create_scalar_table(&namespace, "products").await;
10995
10996        let mut metadata = HashMap::new();
10997        metadata.insert("owner".to_string(), "team_a".to_string());
10998        metadata.insert("version".to_string(), "1.0".to_string());
10999
11000        let mut req = UpdateTableSchemaMetadataRequest::new();
11001        req.id = Some(vec!["products".to_string()]);
11002        req.metadata = Some(metadata.clone());
11003
11004        let response = namespace.update_table_schema_metadata(req).await.unwrap();
11005
11006        assert!(response.metadata.is_some());
11007        let returned = response.metadata.unwrap();
11008        assert_eq!(returned.get("owner"), Some(&"team_a".to_string()));
11009        assert_eq!(returned.get("version"), Some(&"1.0".to_string()));
11010        assert!(
11011            response.transaction_id.is_some(),
11012            "update_table_schema_metadata should return a transaction_id"
11013        );
11014    }
11015
11016    #[tokio::test]
11017    async fn test_get_table_stats() {
11018        use lance_namespace::models::GetTableStatsRequest;
11019
11020        let (namespace, _temp_dir) = create_test_namespace().await;
11021        create_scalar_table(&namespace, "items").await;
11022        create_scalar_index(&namespace, "items", "items_id_idx").await;
11023
11024        let mut req = GetTableStatsRequest::new();
11025        req.id = Some(vec!["items".to_string()]);
11026
11027        let response = namespace.get_table_stats(req).await.unwrap();
11028        assert_eq!(response.num_rows, 3);
11029        assert_eq!(response.num_indices, 1);
11030    }
11031
11032    #[tokio::test]
11033    async fn test_explain_table_query_plan() {
11034        use lance_namespace::models::QueryTableRequestVector;
11035        use lance_namespace::models::{ExplainTableQueryPlanRequest, QueryTableRequest};
11036
11037        let (namespace, _temp_dir) = create_test_namespace().await;
11038        create_scalar_table(&namespace, "catalog").await;
11039
11040        let mut query = QueryTableRequest::new(1, QueryTableRequestVector::new());
11041        query.filter = Some("id > 1".to_string());
11042        query.columns = Some(Box::new(QueryTableRequestColumns {
11043            column_names: Some(vec!["id".to_string(), "name".to_string()]),
11044            column_aliases: None,
11045        }));
11046        query.with_row_id = Some(true);
11047
11048        let mut req = ExplainTableQueryPlanRequest::new(query);
11049        req.id = Some(vec!["catalog".to_string()]);
11050
11051        let plan_str = namespace.explain_table_query_plan(req).await.unwrap();
11052        assert_plan_contains_all(
11053            &plan_str,
11054            &[
11055                "ProjectionExec: expr=[id@0 as id, name@2 as name",
11056                "Take: columns=\"id, _rowid, (name)\"",
11057                "LanceRead: uri=",
11058                "projection=[id]",
11059                "row_id=true, row_addr=false",
11060                "full_filter=id > Int32(1)",
11061                "refine_filter=id > Int32(1)",
11062            ],
11063            "Filtered explain plan should preserve late materialization and filter pushdown",
11064        );
11065    }
11066
11067    #[tokio::test]
11068    async fn test_analyze_table_query_plan() {
11069        use lance_namespace::models::AnalyzeTableQueryPlanRequest;
11070        use lance_namespace::models::QueryTableRequestVector;
11071
11072        let (namespace, _temp_dir) = create_test_namespace().await;
11073        create_scalar_table(&namespace, "catalog").await;
11074
11075        let mut req = AnalyzeTableQueryPlanRequest::new(1, QueryTableRequestVector::new());
11076        req.id = Some(vec!["catalog".to_string()]);
11077        req.filter = Some("id > 0".to_string());
11078        req.columns = Some(Box::new(QueryTableRequestColumns {
11079            column_names: Some(vec!["id".to_string(), "name".to_string()]),
11080            column_aliases: None,
11081        }));
11082        req.with_row_id = Some(true);
11083
11084        let analysis_str = namespace.analyze_table_query_plan(req).await.unwrap();
11085        assert_plan_contains_all(
11086            &analysis_str,
11087            &[
11088                "AnalyzeExec verbose=true",
11089                "ProjectionExec: elapsed=",
11090                "expr=[id@0 as id, name@2 as name",
11091                "Take: elapsed=",
11092                "columns=\"id, _rowid, (name)\"",
11093                "CoalesceBatchesExec: elapsed=",
11094                "LanceRead: elapsed=",
11095                "projection=[id]",
11096                "row_id=true, row_addr=false",
11097                "full_filter=id > Int32(0)",
11098                "refine_filter=id > Int32(0)",
11099                "metrics=[output_rows=",
11100            ],
11101            "Filtered analyze plan should preserve late materialization and filter pushdown",
11102        );
11103    }
11104
11105    #[tokio::test]
11106    async fn test_dir_listing_no_extra_calls_without_migration() {
11107        let temp_dir = TempStdDir::default();
11108        let temp_path = temp_dir.to_str().unwrap();
11109        let root_uri = file_object_store_uri(temp_path);
11110        let listing_count = Arc::new(AtomicUsize::new(0));
11111        let session = build_listing_counting_session(listing_count.clone());
11112
11113        // Create a table using dir-listing-only namespace
11114        let dir_only_ns = DirectoryNamespaceBuilder::new(root_uri.clone())
11115            .session(session.clone())
11116            .manifest_enabled(false)
11117            .dir_listing_enabled(true)
11118            .build()
11119            .await
11120            .unwrap();
11121
11122        let schema = create_test_schema();
11123        let ipc_data = create_test_ipc_data(&schema);
11124        let mut create_req = CreateTableRequest::new();
11125        create_req.id = Some(vec!["test_table".to_string()]);
11126        dir_only_ns
11127            .create_table(create_req, Bytes::from(ipc_data))
11128            .await
11129            .unwrap();
11130
11131        // Build a namespace with both enabled but migration disabled (default)
11132        let hybrid_ns = DirectoryNamespaceBuilder::new(root_uri)
11133            .session(session)
11134            .manifest_enabled(true)
11135            .dir_listing_enabled(true)
11136            .dir_listing_to_manifest_migration_enabled(false)
11137            .build()
11138            .await
11139            .unwrap();
11140
11141        // Reset counter before the operation we want to measure
11142        listing_count.store(0, Ordering::SeqCst);
11143
11144        // table_exists should use dir listing directly, making only 1 listing call
11145        let mut exists_req = TableExistsRequest::new();
11146        exists_req.id = Some(vec!["test_table".to_string()]);
11147        hybrid_ns.table_exists(exists_req).await.unwrap();
11148
11149        let count = listing_count.load(Ordering::SeqCst);
11150        assert_eq!(
11151            count, 1,
11152            "Expected exactly 1 listing call for table_exists \
11153             without migration mode, but got {}",
11154            count
11155        );
11156
11157        // Reset and test describe_table
11158        listing_count.store(0, Ordering::SeqCst);
11159
11160        let mut describe_req = DescribeTableRequest::new();
11161        describe_req.id = Some(vec!["test_table".to_string()]);
11162        hybrid_ns.describe_table(describe_req).await.unwrap();
11163
11164        let count = listing_count.load(Ordering::SeqCst);
11165        assert_eq!(
11166            count, 1,
11167            "Expected exactly 1 listing call for describe_table \
11168             without migration mode, but got {}",
11169            count
11170        );
11171    }
11172
11173    #[tokio::test]
11174    async fn test_describe_declared_table_checks_versions_only_when_requested() {
11175        let temp_dir = TempStdDir::default();
11176        let temp_path = temp_dir.to_str().unwrap();
11177        let root_uri = file_object_store_uri(temp_path);
11178        let listing_count = Arc::new(AtomicUsize::new(0));
11179        let session = build_listing_counting_session(listing_count.clone());
11180
11181        let namespace = DirectoryNamespaceBuilder::new(root_uri)
11182            .session(session)
11183            .manifest_enabled(false)
11184            .dir_listing_enabled(true)
11185            .build()
11186            .await
11187            .unwrap();
11188
11189        let mut declare_req = DeclareTableRequest::new();
11190        declare_req.id = Some(vec!["test_table".to_string()]);
11191        namespace.declare_table(declare_req).await.unwrap();
11192
11193        listing_count.store(0, Ordering::SeqCst);
11194
11195        let mut describe_req = DescribeTableRequest::new();
11196        describe_req.id = Some(vec!["test_table".to_string()]);
11197        let describe_response = namespace.describe_table(describe_req).await.unwrap();
11198
11199        assert_eq!(describe_response.is_only_declared, None);
11200        assert_eq!(
11201            listing_count.load(Ordering::SeqCst),
11202            1,
11203            "Default describe_table should only list the table directory"
11204        );
11205
11206        listing_count.store(0, Ordering::SeqCst);
11207
11208        let mut describe_req = DescribeTableRequest::new();
11209        describe_req.id = Some(vec!["test_table".to_string()]);
11210        describe_req.check_declared = Some(true);
11211        let describe_response = namespace.describe_table(describe_req).await.unwrap();
11212
11213        assert_eq!(describe_response.is_only_declared, Some(true));
11214        assert_eq!(
11215            listing_count.load(Ordering::SeqCst),
11216            2,
11217            "check_declared describe_table should list the table directory and _versions"
11218        );
11219    }
11220
11221    #[tokio::test]
11222    async fn test_dir_listing_extra_calls_with_migration() {
11223        let temp_dir = TempStdDir::default();
11224        let temp_path = temp_dir.to_str().unwrap();
11225        let root_uri = file_object_store_uri(temp_path);
11226        let listing_count = Arc::new(AtomicUsize::new(0));
11227        let session = build_listing_counting_session(listing_count.clone());
11228
11229        // Create a table using dir-listing-only namespace so it exists physically but is absent from __manifest.
11230        let dir_only_ns = DirectoryNamespaceBuilder::new(root_uri.clone())
11231            .session(session.clone())
11232            .manifest_enabled(false)
11233            .dir_listing_enabled(true)
11234            .build()
11235            .await
11236            .unwrap();
11237
11238        let schema = create_test_schema();
11239        let ipc_data = create_test_ipc_data(&schema);
11240        let mut create_req = CreateTableRequest::new();
11241        create_req.id = Some(vec!["test_table".to_string()]);
11242        dir_only_ns
11243            .create_table(create_req, Bytes::from(ipc_data))
11244            .await
11245            .unwrap();
11246
11247        let hybrid_ns = DirectoryNamespaceBuilder::new(root_uri)
11248            .session(session)
11249            .manifest_enabled(true)
11250            .dir_listing_enabled(true)
11251            .dir_listing_to_manifest_migration_enabled(true)
11252            .build()
11253            .await
11254            .unwrap();
11255
11256        // table_exists first checks __manifest (which on local FS uses the
11257        // version hint and does no list call), then falls back to the table
11258        // directory (one list_with_delimiter on test_table.lance).
11259        listing_count.store(0, Ordering::SeqCst);
11260
11261        let mut exists_req = TableExistsRequest::new();
11262        exists_req.id = Some(vec!["test_table".to_string()]);
11263        hybrid_ns.table_exists(exists_req).await.unwrap();
11264
11265        let count = listing_count.load(Ordering::SeqCst);
11266        assert_eq!(
11267            count, 1,
11268            "Expected exactly 1 listing call for table_exists with migration mode \
11269             (table directory fallback; manifest reload uses the version hint), but got {}",
11270            count
11271        );
11272
11273        // describe_table follows the same path when the table is not yet registered in __manifest.
11274        listing_count.store(0, Ordering::SeqCst);
11275
11276        let mut describe_req = DescribeTableRequest::new();
11277        describe_req.id = Some(vec!["test_table".to_string()]);
11278        hybrid_ns.describe_table(describe_req).await.unwrap();
11279
11280        let count = listing_count.load(Ordering::SeqCst);
11281        assert_eq!(
11282            count, 1,
11283            "Expected exactly 1 listing call for describe_table with migration mode \
11284             (table directory fallback; manifest reload uses the version hint), but got {}",
11285            count
11286        );
11287    }
11288
11289    #[tokio::test]
11290    async fn test_manifest_reload_observes_new_version_from_other_namespace() {
11291        let temp_dir = TempStdDir::default();
11292        let temp_path = temp_dir.to_str().unwrap();
11293
11294        let namespace_a = DirectoryNamespaceBuilder::new(temp_path)
11295            .manifest_enabled(true)
11296            .dir_listing_enabled(false)
11297            .build()
11298            .await
11299            .unwrap();
11300        create_scalar_table(&namespace_a, "alpha").await;
11301
11302        let namespace_b = DirectoryNamespaceBuilder::new(temp_path)
11303            .manifest_enabled(true)
11304            .dir_listing_enabled(false)
11305            .build()
11306            .await
11307            .unwrap();
11308        create_scalar_table(&namespace_b, "beta").await;
11309
11310        let response = namespace_a
11311            .list_tables(ListTablesRequest {
11312                id: Some(vec![]),
11313                ..Default::default()
11314            })
11315            .await
11316            .unwrap();
11317
11318        let mut tables = response.tables;
11319        tables.sort();
11320        assert_eq!(tables, vec!["alpha", "beta"]);
11321    }
11322
11323    #[tokio::test]
11324    async fn test_migration_not_found_errors_include_table_id() {
11325        let temp_dir = TempStdDir::default();
11326        let temp_path = temp_dir.to_str().unwrap();
11327
11328        let namespace = DirectoryNamespaceBuilder::new(temp_path)
11329            .manifest_enabled(true)
11330            .dir_listing_enabled(true)
11331            .dir_listing_to_manifest_migration_enabled(true)
11332            .build()
11333            .await
11334            .unwrap();
11335
11336        let mut exists_req = TableExistsRequest::new();
11337        exists_req.id = Some(vec!["missing_table".to_string()]);
11338        let err = namespace.table_exists(exists_req).await.unwrap_err();
11339        assert!(matches!(err, Error::Namespace { .. }));
11340        let err_msg = err.to_string();
11341        assert!(err_msg.contains("Table not found"));
11342        assert!(err_msg.contains("table id 'missing_table'"));
11343
11344        let mut describe_req = DescribeTableRequest::new();
11345        describe_req.id = Some(vec!["missing_table".to_string()]);
11346        let err = namespace.describe_table(describe_req).await.unwrap_err();
11347        assert!(matches!(err, Error::Namespace { .. }));
11348        let err_msg = err.to_string();
11349        assert!(err_msg.contains("Table not found"));
11350        assert!(err_msg.contains("table id 'missing_table'"));
11351    }
11352
11353    #[tokio::test]
11354    async fn test_manifest_not_found_errors_include_full_table_id() {
11355        use lance_namespace::models::CreateNamespaceRequest;
11356
11357        let temp_dir = TempStdDir::default();
11358        let temp_path = temp_dir.to_str().unwrap();
11359
11360        let namespace = DirectoryNamespaceBuilder::new(temp_path)
11361            .manifest_enabled(true)
11362            .dir_listing_enabled(true)
11363            .build()
11364            .await
11365            .unwrap();
11366
11367        let mut create_ns_req = CreateNamespaceRequest::new();
11368        create_ns_req.id = Some(vec!["workspace".to_string()]);
11369        namespace.create_namespace(create_ns_req).await.unwrap();
11370
11371        let missing_table_id = vec!["workspace".to_string(), "missing_table".to_string()];
11372
11373        let mut exists_req = TableExistsRequest::new();
11374        exists_req.id = Some(missing_table_id.clone());
11375        let err = namespace.table_exists(exists_req).await.unwrap_err();
11376        assert!(matches!(err, Error::Namespace { .. }));
11377        let err_msg = err.to_string();
11378        assert!(err_msg.contains("Table not found"));
11379        assert!(err_msg.contains("table id 'workspace$missing_table'"));
11380
11381        let mut describe_req = DescribeTableRequest::new();
11382        describe_req.id = Some(missing_table_id);
11383        let err = namespace.describe_table(describe_req).await.unwrap_err();
11384        assert!(matches!(err, Error::Namespace { .. }));
11385        let err_msg = err.to_string();
11386        assert!(err_msg.contains("Table not found"));
11387        assert!(err_msg.contains("table id 'workspace$missing_table'"));
11388    }
11389
11390    /// Helper used by tag tests: creates a table with `versions` total versions
11391    /// (1 create + N-1 appends) and returns the namespace plus the table id.
11392    async fn create_tagged_test_table(
11393        versions: u32,
11394    ) -> (Arc<DirectoryNamespace>, TempStdDir, Vec<String>) {
11395        use arrow::array::{Int32Array, RecordBatchIterator};
11396        use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
11397        use arrow::record_batch::RecordBatch;
11398        use lance::dataset::{Dataset, WriteMode, WriteParams};
11399
11400        assert!(versions >= 1, "versions must be at least 1");
11401
11402        let temp_dir = TempStdDir::default();
11403        let temp_path = temp_dir.to_str().unwrap();
11404
11405        let namespace = Arc::new(
11406            DirectoryNamespaceBuilder::new(temp_path)
11407                .build()
11408                .await
11409                .unwrap(),
11410        );
11411        let table_id = vec!["tag_table".to_string()];
11412        let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
11413            "id",
11414            DataType::Int32,
11415            false,
11416        )]));
11417        let initial_batch = RecordBatch::try_new(
11418            arrow_schema.clone(),
11419            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
11420        )
11421        .unwrap();
11422        let batches = RecordBatchIterator::new(vec![Ok(initial_batch)], arrow_schema.clone());
11423        let write_params = WriteParams {
11424            mode: WriteMode::Create,
11425            ..Default::default()
11426        };
11427
11428        let mut dataset = Dataset::write_into_namespace(
11429            batches,
11430            namespace.clone() as Arc<dyn LanceNamespace>,
11431            table_id.clone(),
11432            Some(write_params),
11433        )
11434        .await
11435        .unwrap();
11436
11437        for i in 1..versions {
11438            let value_start = (i as i32) * 10;
11439            let batch = RecordBatch::try_new(
11440                arrow_schema.clone(),
11441                vec![Arc::new(Int32Array::from(vec![
11442                    value_start,
11443                    value_start + 1,
11444                ]))],
11445            )
11446            .unwrap();
11447            let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
11448            dataset.append(batches, None).await.unwrap();
11449        }
11450
11451        (namespace, temp_dir, table_id)
11452    }
11453
11454    /// Downcast a lance-core error to its NamespaceError code for precise assertions.
11455    fn namespace_code(err: &Error) -> Option<ErrorCode> {
11456        match err {
11457            Error::Namespace { source, .. } => {
11458                source.downcast_ref::<NamespaceError>().map(|e| e.code())
11459            }
11460            _ => None,
11461        }
11462    }
11463
11464    #[tokio::test]
11465    async fn test_create_and_list_branches() {
11466        let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11467
11468        namespace
11469            .create_table_branch(CreateTableBranchRequest {
11470                id: Some(table_id.clone()),
11471                name: "dev".to_string(),
11472                ..Default::default()
11473            })
11474            .await
11475            .unwrap();
11476        namespace
11477            .create_table_branch(CreateTableBranchRequest {
11478                id: Some(table_id.clone()),
11479                name: "staging".to_string(),
11480                ..Default::default()
11481            })
11482            .await
11483            .unwrap();
11484
11485        let resp = namespace
11486            .list_table_branches(ListTableBranchesRequest {
11487                id: Some(table_id.clone()),
11488                ..Default::default()
11489            })
11490            .await
11491            .unwrap();
11492        assert_eq!(
11493            resp.branches.len(),
11494            2,
11495            "expected 2 branches, got: {:?}",
11496            resp.branches
11497        );
11498        assert!(resp.branches.contains_key("dev"));
11499        assert!(resp.branches.contains_key("staging"));
11500        assert!(resp.page_token.is_none());
11501
11502        // Deleting one branch is reflected in a subsequent list.
11503        namespace
11504            .delete_table_branch(DeleteTableBranchRequest {
11505                id: Some(table_id.clone()),
11506                name: "dev".to_string(),
11507                ..Default::default()
11508            })
11509            .await
11510            .unwrap();
11511
11512        let resp = namespace
11513            .list_table_branches(ListTableBranchesRequest {
11514                id: Some(table_id),
11515                ..Default::default()
11516            })
11517            .await
11518            .unwrap();
11519        assert_eq!(resp.branches.len(), 1, "expected 1 branch after delete");
11520        assert!(!resp.branches.contains_key("dev"));
11521        assert!(resp.branches.contains_key("staging"));
11522    }
11523
11524    #[tokio::test]
11525    async fn test_create_branch_from_version() {
11526        let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11527
11528        // Fork explicitly from version 1 of main.
11529        namespace
11530            .create_table_branch(CreateTableBranchRequest {
11531                id: Some(table_id.clone()),
11532                name: "from-v1".to_string(),
11533                from_version: Some(1),
11534                ..Default::default()
11535            })
11536            .await
11537            .unwrap();
11538
11539        let resp = namespace
11540            .list_table_branches(ListTableBranchesRequest {
11541                id: Some(table_id),
11542                ..Default::default()
11543            })
11544            .await
11545            .unwrap();
11546        let branch = resp
11547            .branches
11548            .get("from-v1")
11549            .expect("forked branch should be listed");
11550        assert_eq!(
11551            branch.parent_version, 1,
11552            "branch should fork from version 1"
11553        );
11554        assert!(
11555            branch.parent_branch.is_none(),
11556            "a branch forked from main has no parent branch"
11557        );
11558    }
11559
11560    /// Forking from a NON-main source branch must clone that branch's chain.
11561    /// Both chains are given a version 2 with diverged content, so a clone that
11562    /// wrongly resolves the version under main succeeds silently with main's
11563    /// data instead of erroring.
11564    #[tokio::test]
11565    async fn test_create_branch_from_other_branch() {
11566        use lance::dataset::builder::DatasetBuilder;
11567
11568        let (namespace, _temp_dir) = create_test_namespace().await;
11569        create_scalar_table(&namespace, "users").await; // main v1: ids [1, 2, 3]
11570        // dev: forked at v1, one append (ids 100, 101) -> dev v2
11571        create_branch_with_commits(&namespace, "users", "dev", 1).await;
11572        // Diverge main to the same version number with different content.
11573        let main_ds = open_dataset(&namespace, "users").await;
11574        append_scalar_version(main_ds.uri(), 500).await; // main v2: + ids [500, 501]
11575
11576        namespace
11577            .create_table_branch(CreateTableBranchRequest {
11578                id: Some(vec!["users".to_string()]),
11579                name: "child".to_string(),
11580                from_branch: Some("dev".to_string()),
11581                from_version: Some(2),
11582                ..Default::default()
11583            })
11584            .await
11585            .unwrap();
11586
11587        let child_ds = DatasetBuilder::from_uri(main_ds.uri())
11588            .with_branch("child", None)
11589            .load()
11590            .await
11591            .unwrap();
11592        let ids = scan_id_column(&child_ds).await;
11593        assert!(
11594            ids.contains(&100) && ids.contains(&101),
11595            "child must contain dev's appended rows, got: {:?}",
11596            ids
11597        );
11598        assert!(
11599            !ids.contains(&500),
11600            "child must not contain main's diverged rows, got: {:?}",
11601            ids
11602        );
11603
11604        // The recorded metadata and the cloned data must agree on the parent.
11605        let listed = namespace
11606            .list_table_branches(ListTableBranchesRequest {
11607                id: Some(vec!["users".to_string()]),
11608                ..Default::default()
11609            })
11610            .await
11611            .unwrap();
11612        assert_eq!(
11613            listed
11614                .branches
11615                .get("child")
11616                .unwrap()
11617                .parent_branch
11618                .as_deref(),
11619            Some("dev")
11620        );
11621    }
11622
11623    #[tokio::test]
11624    async fn test_create_existing_branch_conflict() {
11625        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11626
11627        namespace
11628            .create_table_branch(CreateTableBranchRequest {
11629                id: Some(table_id.clone()),
11630                name: "dev".to_string(),
11631                ..Default::default()
11632            })
11633            .await
11634            .unwrap();
11635
11636        let err = namespace
11637            .create_table_branch(CreateTableBranchRequest {
11638                id: Some(table_id),
11639                name: "dev".to_string(),
11640                ..Default::default()
11641            })
11642            .await
11643            .unwrap_err();
11644        assert_eq!(
11645            namespace_code(&err),
11646            Some(ErrorCode::TableBranchAlreadyExists),
11647            "expected TableBranchAlreadyExists, got: {}",
11648            err
11649        );
11650        assert!(
11651            err.to_string().to_lowercase().contains("already exists"),
11652            "expected already-exists message, got: {}",
11653            err
11654        );
11655    }
11656
11657    #[tokio::test]
11658    async fn test_delete_unknown_branch() {
11659        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11660
11661        let err = namespace
11662            .delete_table_branch(DeleteTableBranchRequest {
11663                id: Some(table_id),
11664                name: "does-not-exist".to_string(),
11665                ..Default::default()
11666            })
11667            .await
11668            .unwrap_err();
11669        assert_eq!(
11670            namespace_code(&err),
11671            Some(ErrorCode::TableBranchNotFound),
11672            "expected TableBranchNotFound, got: {}",
11673            err
11674        );
11675        assert!(
11676            err.to_string().to_lowercase().contains("not found"),
11677            "expected not-found message, got: {}",
11678            err
11679        );
11680    }
11681
11682    #[tokio::test]
11683    async fn test_delete_referenced_branch_conflict() {
11684        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11685
11686        // A child forked from `parent` (via from_branch) makes `parent` a referenced branch.
11687        namespace
11688            .create_table_branch(CreateTableBranchRequest {
11689                id: Some(table_id.clone()),
11690                name: "parent".to_string(),
11691                ..Default::default()
11692            })
11693            .await
11694            .unwrap();
11695        namespace
11696            .create_table_branch(CreateTableBranchRequest {
11697                id: Some(table_id.clone()),
11698                name: "child".to_string(),
11699                from_branch: Some("parent".to_string()),
11700                ..Default::default()
11701            })
11702            .await
11703            .unwrap();
11704
11705        // from_branch resolution: the child records its parent branch as its fork point.
11706        let listed = namespace
11707            .list_table_branches(ListTableBranchesRequest {
11708                id: Some(table_id.clone()),
11709                ..Default::default()
11710            })
11711            .await
11712            .unwrap();
11713        let child = listed
11714            .branches
11715            .get("child")
11716            .expect("child branch should be listed");
11717        assert_eq!(
11718            child.parent_branch.as_deref(),
11719            Some("parent"),
11720            "child should record parent branch as its fork point"
11721        );
11722        assert!(
11723            child.parent_version >= 1,
11724            "child should record the parent version it forked from, got {}",
11725            child.parent_version
11726        );
11727
11728        // Deleting a branch that still has dependents is refused. The delete spec has no 409,
11729        // so it surfaces as a documented InvalidInput (400), not a conflict status.
11730        let err = namespace
11731            .delete_table_branch(DeleteTableBranchRequest {
11732                id: Some(table_id),
11733                name: "parent".to_string(),
11734                ..Default::default()
11735            })
11736            .await
11737            .unwrap_err();
11738        assert_eq!(
11739            namespace_code(&err),
11740            Some(ErrorCode::InvalidInput),
11741            "expected InvalidInput for deleting a referenced branch, got: {}",
11742            err
11743        );
11744        assert!(
11745            err.to_string().to_lowercase().contains("referenced"),
11746            "error should explain the branch is still referenced, got: {}",
11747            err
11748        );
11749    }
11750
11751    #[tokio::test]
11752    async fn test_branch_name_required() {
11753        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11754
11755        let create_err = namespace
11756            .create_table_branch(CreateTableBranchRequest {
11757                id: Some(table_id.clone()),
11758                name: String::new(),
11759                ..Default::default()
11760            })
11761            .await
11762            .unwrap_err();
11763        assert_eq!(
11764            namespace_code(&create_err),
11765            Some(ErrorCode::InvalidInput),
11766            "empty name on create should be InvalidInput, got: {}",
11767            create_err
11768        );
11769        assert!(
11770            create_err
11771                .to_string()
11772                .to_lowercase()
11773                .contains("must not be empty")
11774        );
11775
11776        let delete_err = namespace
11777            .delete_table_branch(DeleteTableBranchRequest {
11778                id: Some(table_id),
11779                name: String::new(),
11780                ..Default::default()
11781            })
11782            .await
11783            .unwrap_err();
11784        assert_eq!(
11785            namespace_code(&delete_err),
11786            Some(ErrorCode::InvalidInput),
11787            "empty name on delete should be InvalidInput, got: {}",
11788            delete_err
11789        );
11790        assert!(
11791            delete_err
11792                .to_string()
11793                .to_lowercase()
11794                .contains("must not be empty")
11795        );
11796    }
11797
11798    #[tokio::test]
11799    async fn test_create_branch_rejects_negative_from_version() {
11800        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11801
11802        let err = namespace
11803            .create_table_branch(CreateTableBranchRequest {
11804                id: Some(table_id),
11805                name: "dev".to_string(),
11806                from_version: Some(-1),
11807                ..Default::default()
11808            })
11809            .await
11810            .unwrap_err();
11811        assert_eq!(
11812            namespace_code(&err),
11813            Some(ErrorCode::InvalidInput),
11814            "negative from_version should be InvalidInput, got: {}",
11815            err
11816        );
11817        assert!(err.to_string().to_lowercase().contains("from_version"));
11818    }
11819
11820    #[tokio::test]
11821    async fn test_create_branch_nonexistent_from_version() {
11822        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11823
11824        // Version 999 does not exist (the table has 2 versions). create_branch's clone phase
11825        // raises DatasetNotFound, which we map to a documented InvalidInput (400).
11826        let err = namespace
11827            .create_table_branch(CreateTableBranchRequest {
11828                id: Some(table_id),
11829                name: "dev".to_string(),
11830                from_version: Some(999),
11831                ..Default::default()
11832            })
11833            .await
11834            .unwrap_err();
11835        assert_eq!(
11836            namespace_code(&err),
11837            Some(ErrorCode::InvalidInput),
11838            "non-existent from_version should map to InvalidInput, got: {}",
11839            err
11840        );
11841        assert!(
11842            err.to_string().to_lowercase().contains("does not exist"),
11843            "error should name the missing source, got: {}",
11844            err
11845        );
11846    }
11847
11848    #[tokio::test]
11849    async fn test_create_and_list_tags() {
11850        let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11851
11852        let mut req = CreateTableTagRequest::new("v1".to_string(), 1);
11853        req.id = Some(table_id.clone());
11854        namespace.create_table_tag(req).await.unwrap();
11855
11856        let mut req = CreateTableTagRequest::new("v2".to_string(), 2);
11857        req.id = Some(table_id.clone());
11858        namespace.create_table_tag(req).await.unwrap();
11859
11860        let mut list_req = ListTableTagsRequest::new();
11861        list_req.id = Some(table_id);
11862        let resp = namespace.list_table_tags(list_req).await.unwrap();
11863
11864        assert_eq!(resp.tags.len(), 2, "expected 2 tags, got: {:?}", resp.tags);
11865        assert_eq!(resp.tags.get("v1").unwrap().version, 1);
11866        assert_eq!(resp.tags.get("v2").unwrap().version, 2);
11867        assert!(resp.page_token.is_none());
11868    }
11869
11870    #[tokio::test]
11871    async fn test_create_existing_tag_conflict() {
11872        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11873
11874        let mut req = CreateTableTagRequest::new("v1".to_string(), 1);
11875        req.id = Some(table_id.clone());
11876        namespace.create_table_tag(req).await.unwrap();
11877
11878        let mut req = CreateTableTagRequest::new("v1".to_string(), 2);
11879        req.id = Some(table_id);
11880        let err = namespace.create_table_tag(req).await.unwrap_err();
11881        assert!(
11882            err.to_string().to_lowercase().contains("already exists"),
11883            "expected already-exists error, got: {}",
11884            err
11885        );
11886    }
11887
11888    #[tokio::test]
11889    async fn test_get_tag_version() {
11890        let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11891
11892        let mut req = CreateTableTagRequest::new("release".to_string(), 2);
11893        req.id = Some(table_id.clone());
11894        namespace.create_table_tag(req).await.unwrap();
11895
11896        let mut get_req = GetTableTagVersionRequest::new("release".to_string());
11897        get_req.id = Some(table_id);
11898        let resp = namespace.get_table_tag_version(get_req).await.unwrap();
11899        assert_eq!(resp.version, 2);
11900        assert_eq!(resp.branch, None);
11901    }
11902
11903    #[tokio::test]
11904    async fn test_get_unknown_tag() {
11905        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11906
11907        let mut get_req = GetTableTagVersionRequest::new("does-not-exist".to_string());
11908        get_req.id = Some(table_id);
11909        let err = namespace.get_table_tag_version(get_req).await.unwrap_err();
11910        assert!(
11911            err.to_string().to_lowercase().contains("not found"),
11912            "expected not-found error, got: {}",
11913            err
11914        );
11915    }
11916
11917    #[tokio::test]
11918    async fn test_update_tag_to_new_version() {
11919        let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11920
11921        let mut req = CreateTableTagRequest::new("rolling".to_string(), 1);
11922        req.id = Some(table_id.clone());
11923        namespace.create_table_tag(req).await.unwrap();
11924
11925        let mut update_req = UpdateTableTagRequest::new("rolling".to_string(), 3);
11926        update_req.id = Some(table_id.clone());
11927        namespace.update_table_tag(update_req).await.unwrap();
11928
11929        let mut get_req = GetTableTagVersionRequest::new("rolling".to_string());
11930        get_req.id = Some(table_id);
11931        let resp = namespace.get_table_tag_version(get_req).await.unwrap();
11932        assert_eq!(resp.version, 3);
11933    }
11934
11935    #[tokio::test]
11936    async fn test_update_unknown_tag() {
11937        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11938
11939        let mut update_req = UpdateTableTagRequest::new("ghost".to_string(), 1);
11940        update_req.id = Some(table_id);
11941        let err = namespace.update_table_tag(update_req).await.unwrap_err();
11942        assert!(
11943            err.to_string().to_lowercase().contains("not found"),
11944            "expected not-found error, got: {}",
11945            err
11946        );
11947    }
11948
11949    #[tokio::test]
11950    async fn test_delete_tag() {
11951        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11952
11953        let mut req = CreateTableTagRequest::new("doomed".to_string(), 1);
11954        req.id = Some(table_id.clone());
11955        namespace.create_table_tag(req).await.unwrap();
11956
11957        let mut delete_req = DeleteTableTagRequest::new("doomed".to_string());
11958        delete_req.id = Some(table_id.clone());
11959        namespace.delete_table_tag(delete_req).await.unwrap();
11960
11961        let mut list_req = ListTableTagsRequest::new();
11962        list_req.id = Some(table_id.clone());
11963        let resp = namespace.list_table_tags(list_req).await.unwrap();
11964        assert!(resp.tags.is_empty(), "tag should be removed after delete");
11965
11966        // A second get should return NotFound.
11967        let mut get_req = GetTableTagVersionRequest::new("doomed".to_string());
11968        get_req.id = Some(table_id);
11969        let err = namespace.get_table_tag_version(get_req).await.unwrap_err();
11970        assert!(err.to_string().to_lowercase().contains("not found"));
11971    }
11972
11973    #[tokio::test]
11974    async fn test_delete_unknown_tag() {
11975        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11976
11977        let mut delete_req = DeleteTableTagRequest::new("nope".to_string());
11978        delete_req.id = Some(table_id);
11979        let err = namespace.delete_table_tag(delete_req).await.unwrap_err();
11980        assert!(
11981            err.to_string().to_lowercase().contains("not found"),
11982            "expected not-found error, got: {}",
11983            err
11984        );
11985    }
11986
11987    #[tokio::test]
11988    async fn test_create_tag_invalid_version() {
11989        let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11990
11991        // version 0 should be rejected as InvalidInput before reaching the dataset.
11992        let mut req = CreateTableTagRequest::new("v0".to_string(), 0);
11993        req.id = Some(table_id.clone());
11994        let err = namespace.create_table_tag(req).await.unwrap_err();
11995        assert!(
11996            err.to_string().to_lowercase().contains("positive"),
11997            "expected positive-version error, got: {}",
11998            err
11999        );
12000
12001        // empty tag name should also be rejected.
12002        let mut req = CreateTableTagRequest::new(String::new(), 1);
12003        req.id = Some(table_id);
12004        let err = namespace.create_table_tag(req).await.unwrap_err();
12005        assert!(
12006            err.to_string().to_lowercase().contains("must not be empty"),
12007            "expected empty-tag-name error, got: {}",
12008            err
12009        );
12010    }
12011
12012    #[tokio::test]
12013    async fn test_create_tag_table_not_found() {
12014        let (namespace, _temp_dir) = create_test_namespace().await;
12015
12016        let mut req = CreateTableTagRequest::new("v1".to_string(), 1);
12017        req.id = Some(vec!["does_not_exist".to_string()]);
12018        let err = namespace.create_table_tag(req).await.unwrap_err();
12019        let msg = err.to_string();
12020        assert!(
12021            msg.contains("Table") && msg.to_lowercase().contains("not found"),
12022            "expected TableNotFound error, got: {}",
12023            err
12024        );
12025    }
12026}