Skip to main content

lance_namespace_impls/dir/
namespace.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use arrow::array::Float32Array;
8use async_trait::async_trait;
9use bytes::Bytes;
10use lance::dataset::statistics::DatasetStatisticsExt;
11use lance::dataset::{
12    Dataset, MergeInsertBuilder, UpdateBuilder, WhenMatched, WhenNotMatched,
13    WhenNotMatchedBySource, WriteMode,
14};
15use lance::index::DatasetIndexExt;
16use lance_core::{Error, Result, box_error};
17use lance_index::is_system_index;
18use lance_index::scalar::FullTextSearchQuery;
19use lance_linalg::distance::MetricType;
20use lance_namespace::LanceNamespace;
21use lance_namespace::error::NamespaceError;
22use lance_namespace::models::{
23    AlterTableAddColumnsRequest, AlterTableAddColumnsResponse, AlterTableAlterColumnsRequest,
24    AlterTableAlterColumnsResponse, AlterTableDropColumnsRequest, AlterTableDropColumnsResponse,
25    AlterTransactionRequest, AlterTransactionResponse, AnalyzeTableQueryPlanRequest,
26    BatchDeleteTableVersionsRequest, BatchDeleteTableVersionsResponse,
27    BranchContents as ModelBranchContents, CountTableRowsRequest, CreateNamespaceRequest,
28    CreateNamespaceResponse, CreateTableBranchRequest, CreateTableBranchResponse,
29    CreateTableIndexRequest, CreateTableIndexResponse, CreateTableRequest, CreateTableResponse,
30    CreateTableScalarIndexResponse, CreateTableTagRequest, CreateTableTagResponse,
31    CreateTableVersionRequest, CreateTableVersionResponse, DeclareTableRequest,
32    DeclareTableResponse, DeleteFromTableRequest, DeleteFromTableResponse,
33    DeleteTableBranchRequest, DeleteTableBranchResponse, DeleteTableTagRequest,
34    DeleteTableTagResponse, DescribeNamespaceRequest, DescribeNamespaceResponse,
35    DescribeTableIndexStatsRequest, DescribeTableIndexStatsResponse, DescribeTableRequest,
36    DescribeTableResponse, DescribeTableVersionRequest, DescribeTableVersionResponse,
37    DescribeTransactionRequest, DescribeTransactionResponse, DropNamespaceRequest,
38    DropNamespaceResponse, DropTableIndexRequest, DropTableIndexResponse, DropTableRequest,
39    DropTableResponse, ExplainTableQueryPlanRequest, FragmentStats, FragmentSummary,
40    GetTableStatsRequest, GetTableStatsResponse, GetTableTagVersionRequest,
41    GetTableTagVersionResponse, IndexContent, InsertIntoTableRequest, InsertIntoTableResponse,
42    ListNamespacesRequest, ListNamespacesResponse, ListTableBranchesRequest,
43    ListTableBranchesResponse, ListTableIndicesRequest, ListTableIndicesResponse,
44    ListTableTagsRequest, ListTableTagsResponse, ListTableVersionsRequest,
45    ListTableVersionsResponse, ListTablesRequest, ListTablesResponse, MergeInsertIntoTableRequest,
46    MergeInsertIntoTableResponse, NamespaceExistsRequest, QueryTableRequest, RestoreTableRequest,
47    RestoreTableResponse, TableExistsRequest, TagContents as ModelTagContents, UpdateTableRequest,
48    UpdateTableResponse, UpdateTableSchemaMetadataRequest, UpdateTableSchemaMetadataResponse,
49    UpdateTableTagRequest, UpdateTableTagResponse,
50};
51use lance_table::io::commit::ManifestNamingScheme;
52use object_store::path::Path;
53use object_store::{Error as ObjectStoreError, ObjectStoreExt};
54
55use super::marker::{MarkerFileError, put_marker_file_atomic};
56use super::query::{apply_probe_bounds, build_engine_fts_query};
57use super::versions::{ExistingTableVersionResolve, TableDeleteEntry};
58use super::{
59    DirectoryNamespace, build_column_alterations, build_sql_expressions, manifest_feature_flags,
60};
61use crate::merge_insert_on_columns;
62
63#[async_trait]
64impl LanceNamespace for DirectoryNamespace {
65    async fn list_namespaces(
66        &self,
67        request: ListNamespacesRequest,
68    ) -> Result<ListNamespacesResponse> {
69        self.record_op("list_namespaces");
70        self.ensure_read_manifest().await?;
71        if let Some(manifest_ns) = self.manifest_ns_for_read() {
72            return manifest_ns.list_namespaces(request).await;
73        }
74
75        if request.id.as_ref().is_some_and(|id| !id.is_empty()) {
76            return Err(self.child_namespace_requires_manifest_error());
77        }
78        Self::validate_root_namespace_id(&request.id)?;
79        Ok(ListNamespacesResponse::new(vec![]))
80    }
81
82    async fn describe_namespace(
83        &self,
84        request: DescribeNamespaceRequest,
85    ) -> Result<DescribeNamespaceResponse> {
86        self.record_op("describe_namespace");
87        self.ensure_read_manifest().await?;
88        if let Some(manifest_ns) = self.manifest_ns_for_read() {
89            return manifest_ns.describe_namespace(request).await;
90        }
91
92        if request.id.as_ref().is_some_and(|id| !id.is_empty()) {
93            return Err(self.child_namespace_requires_manifest_error());
94        }
95        Self::validate_root_namespace_id(&request.id)?;
96        #[allow(clippy::needless_update)]
97        Ok(DescribeNamespaceResponse {
98            properties: Some(HashMap::new()),
99            ..Default::default()
100        })
101    }
102
103    async fn create_namespace(
104        &self,
105        request: CreateNamespaceRequest,
106    ) -> Result<CreateNamespaceResponse> {
107        self.record_op("create_namespace");
108        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
109            return manifest_ns.create_namespace(request).await;
110        }
111
112        if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
113            return Err(NamespaceError::NamespaceAlreadyExists {
114                message: "root namespace".to_string(),
115            }
116            .into());
117        }
118
119        Err(NamespaceError::Unsupported {
120            message: "Child namespaces are only supported when manifest mode is enabled"
121                .to_string(),
122        }
123        .into())
124    }
125
126    async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse> {
127        self.record_op("drop_namespace");
128        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
129            return manifest_ns.drop_namespace(request).await;
130        }
131
132        if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
133            return Err(NamespaceError::InvalidInput {
134                message: "Root namespace cannot be dropped".to_string(),
135            }
136            .into());
137        }
138
139        Err(NamespaceError::Unsupported {
140            message: "Child namespaces are only supported when manifest mode is enabled"
141                .to_string(),
142        }
143        .into())
144    }
145
146    async fn namespace_exists(&self, request: NamespaceExistsRequest) -> Result<()> {
147        self.record_op("namespace_exists");
148        self.ensure_read_manifest().await?;
149        if let Some(manifest_ns) = self.manifest_ns_for_read() {
150            return manifest_ns.namespace_exists(request).await;
151        }
152
153        if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
154            return Ok(());
155        }
156
157        Err(self.child_namespace_requires_manifest_error())
158    }
159
160    async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
161        self.record_op("list_tables");
162        // Validate that namespace ID is provided
163        let namespace_id = request.id.as_ref().ok_or_else(|| {
164            lance_core::Error::from(NamespaceError::InvalidInput {
165                message: "Namespace ID is required".to_string(),
166            })
167        })?;
168
169        // Self-heal the manifest wherever it can be authoritative: a child
170        // namespace, a manifest-only namespace, or migration mode (which merges
171        // manifest entries -- including registered aliases -- into the root
172        // listing). The bypass applies only to migration-disabled directory-backed
173        // root lists.
174        if !namespace_id.is_empty()
175            || !self.dir_listing_enabled
176            || self.dir_listing_to_manifest_migration_enabled
177        {
178            self.ensure_read_manifest().await?;
179        }
180
181        // For child namespaces, always delegate to manifest (if enabled)
182        if !namespace_id.is_empty() {
183            if let Some(manifest_ns) = self.manifest_ns_for_read() {
184                return manifest_ns.list_tables(request).await;
185            }
186            return Err(self.child_namespace_requires_manifest_error());
187        }
188
189        // When only manifest is enabled (no directory listing), delegate directly to manifest
190        if let Some(manifest_ns) = self.manifest_ns_for_read()
191            && !self.dir_listing_enabled
192        {
193            return manifest_ns.list_tables(request).await;
194        }
195        if !self.dir_listing_enabled {
196            return Ok(ListTablesResponse::new(vec![]));
197        }
198
199        // When both manifest and directory listing are enabled with migration mode,
200        // we need to merge and deduplicate
201        let mut tables = if self.manifest_ns_for_read().is_some()
202            && self.dir_listing_enabled
203            && self.dir_listing_to_manifest_migration_enabled
204        {
205            // Get all manifest table locations (for deduplication)
206            let manifest_locations = if let Some(manifest_ns) = self.manifest_ns_for_read() {
207                manifest_ns.list_manifest_table_locations().await?
208            } else {
209                std::collections::HashSet::new()
210            };
211
212            // Get all manifest tables (without pagination for merging)
213            let mut manifest_request = request.clone();
214            manifest_request.limit = None;
215            manifest_request.page_token = None;
216            let manifest_tables = if let Some(manifest_ns) = self.manifest_ns_for_read() {
217                let manifest_response = manifest_ns.list_tables(manifest_request).await?;
218                manifest_response.tables
219            } else {
220                vec![]
221            };
222
223            // Start with all manifest table names
224            // Add directory tables that aren't already in the manifest (by location)
225            let mut all_tables: Vec<String> = manifest_tables;
226            let dir_tables = self.list_directory_tables().await?;
227            for table_name in dir_tables {
228                // Check if this table's location is already in the manifest
229                // Manifest stores full URIs, so we need to check both formats
230                let full_location = format!("{}/{}.lance", self.root, table_name);
231                let relative_location = format!("{}.lance", table_name);
232                if !manifest_locations.contains(&full_location)
233                    && !manifest_locations.contains(&relative_location)
234                {
235                    all_tables.push(table_name);
236                }
237            }
238
239            all_tables
240        } else {
241            self.list_directory_tables().await?
242        };
243
244        tables = self
245            .filter_declared_tables(tables, request.include_declared.unwrap_or(true))
246            .await?;
247
248        // Apply sorting and pagination
249        let next_page_token =
250            Self::apply_pagination(&mut tables, request.page_token, request.limit);
251        let mut response = ListTablesResponse::new(tables);
252        response.page_token = next_page_token;
253        Ok(response)
254    }
255
256    async fn describe_table(&self, request: DescribeTableRequest) -> Result<DescribeTableResponse> {
257        self.record_op("describe_table");
258        self.describe_table_impl(request).await
259    }
260
261    async fn table_exists(&self, request: TableExistsRequest) -> Result<()> {
262        self.record_op("table_exists");
263        let is_root_level = request.id.as_ref().is_some_and(|id| id.len() == 1);
264        let is_child_table = request.id.as_ref().is_some_and(|id| id.len() > 1);
265        let skip_manifest_for_root = self.dir_listing_enabled
266            && is_root_level
267            && !self.dir_listing_to_manifest_migration_enabled;
268        // Child table, manifest-only, or migration mode (see describe_table_impl).
269        // Only a migration-disabled directory-backed root read bypasses the probe.
270        if is_child_table
271            || !self.dir_listing_enabled
272            || self.dir_listing_to_manifest_migration_enabled
273        {
274            self.ensure_read_manifest().await?;
275        }
276        if let Some(manifest_ns) = self.manifest_ns_for_read()
277            && !skip_manifest_for_root
278        {
279            match manifest_ns.table_exists(request.clone()).await {
280                Ok(()) => return Ok(()),
281                Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
282                    // An incompatible manifest must surface "please upgrade"
283                    // rather than degrading to a directory-listing view.
284                    return Err(e);
285                }
286                Err(e) if self.dir_listing_enabled && is_root_level => {
287                    // Only a genuinely-absent table (e.g. an unmigrated on-disk
288                    // table) may fall through to the directory check; any other
289                    // manifest error must propagate rather than be read as missing.
290                    if !Self::is_manifest_table_absent_error(&e) {
291                        return Err(Self::classify_storage_error(e));
292                    }
293                }
294                Err(e) => return Err(e),
295            }
296        }
297        if is_child_table {
298            return Err(self.child_namespace_requires_manifest_error());
299        }
300
301        let table_name = Self::table_name_from_id(&request.id)?;
302        let table_id = Self::format_table_id_from_request(&request.id);
303        if !self.dir_listing_enabled {
304            return Err(NamespaceError::TableNotFound { message: table_id }.into());
305        }
306
307        // Atomically check table existence and deregistration status
308        let status = self.check_table_status(&table_name).await?;
309
310        if !status.exists {
311            return Err(NamespaceError::TableNotFound {
312                message: table_id.clone(),
313            }
314            .into());
315        }
316
317        if status.is_deregistered {
318            return Err(NamespaceError::TableNotFound {
319                message: format!("Table is deregistered: {}", table_id),
320            }
321            .into());
322        }
323
324        Ok(())
325    }
326
327    async fn drop_table(&self, request: DropTableRequest) -> Result<DropTableResponse> {
328        self.record_op("drop_table");
329        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
330            return manifest_ns.drop_table(request).await;
331        }
332
333        let table_name = Self::table_name_from_id(&request.id)?;
334        let table_uri = self.table_full_uri(&table_name);
335        let table_path = self.table_path(&table_name);
336
337        self.object_store
338            .remove_dir_all(table_path)
339            .await
340            .map_err(|e| {
341                lance_core::Error::from(NamespaceError::Internal {
342                    message: format!("Failed to drop table {}: {:?}", table_name, e),
343                })
344            })?;
345
346        Ok(DropTableResponse {
347            id: request.id,
348            location: Some(table_uri),
349            ..Default::default()
350        })
351    }
352
353    async fn create_table(
354        &self,
355        request: CreateTableRequest,
356        request_data: Bytes,
357    ) -> Result<CreateTableResponse> {
358        self.record_op("create_table");
359        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
360            return manifest_ns.create_table(request, request_data).await;
361        }
362
363        Self::validate_dir_only_properties(request.properties.as_ref(), "create_table")?;
364
365        let table_name = Self::table_name_from_id(&request.id)?;
366        let table_uri = self.table_full_uri(&table_name);
367        let status = self.check_table_status(&table_name).await?;
368        let (reader, _num_rows) =
369            Self::ipc_reader_from_request_data(&request_data, "create_table")?;
370
371        if status.exists && self.table_has_actual_manifests(&table_name).await? {
372            return Err(NamespaceError::TableAlreadyExists {
373                message: table_name,
374            }
375            .into());
376        }
377
378        let write_result = self
379            .write_reader_to_table(
380                &table_uri,
381                reader,
382                WriteMode::Create,
383                request.storage_options.clone(),
384            )
385            .await;
386        if let Err(err) = write_result {
387            if self.table_uri_has_actual_manifests(&table_uri).await? {
388                return Err(NamespaceError::TableAlreadyExists {
389                    message: table_name,
390                }
391                .into());
392            }
393            return Err(err);
394        }
395        Ok(CreateTableResponse {
396            version: Some(1),
397            location: Some(table_uri),
398            storage_options: self.storage_options.clone(),
399            properties: request.properties,
400            ..Default::default()
401        })
402    }
403
404    async fn declare_table(&self, request: DeclareTableRequest) -> Result<DeclareTableResponse> {
405        self.record_op("declare_table");
406        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
407            let mut response = manifest_ns.declare_table(request.clone()).await?;
408            if let Some(ref location) = response.location {
409                // For backwards compatibility, only skip vending credentials when explicitly set to false
410                let vend = request.vend_credentials.unwrap_or(true);
411                let identity = request.identity.as_deref();
412                response.storage_options = self
413                    .get_storage_options_for_table(location, vend, identity)
414                    .await?;
415            }
416            // Set managed_versioning when table_version_tracking_enabled
417            if self.table_version_tracking_enabled {
418                response.managed_versioning = Some(true);
419            }
420            return Ok(response);
421        }
422
423        Self::validate_dir_only_properties(request.properties.as_ref(), "declare_table")?;
424
425        let table_name = Self::table_name_from_id(&request.id)?;
426        let table_uri = self.table_full_uri(&table_name);
427
428        // Validate location if provided
429        if let Some(location) = &request.location {
430            let location = location.trim_end_matches('/');
431            if location != table_uri {
432                return Err(NamespaceError::InvalidInput {
433                    message: format!(
434                        "Cannot declare table {} at location {}, must be at location {}",
435                        table_name, location, table_uri
436                    ),
437                }
438                .into());
439            }
440        }
441
442        // Check if table already has data (created via create_table).
443        // The atomic put only prevents races between concurrent declare_table calls,
444        // not between declare_table and existing data.
445        let status = self.check_table_status(&table_name).await?;
446        if status.exists && !status.has_reserved_file {
447            // Table has data but no reserved file - it was created with data
448            return Err(NamespaceError::TableAlreadyExists {
449                message: table_name.to_string(),
450            }
451            .into());
452        }
453
454        // Atomically create the .lance-reserved file to mark the table as declared.
455        // This uses put_if_not_exists semantics to avoid race conditions between
456        // concurrent declare_table calls.
457        let reserved_file_path = self.table_reserved_file_path(&table_name);
458
459        put_marker_file_atomic(
460            &self.object_store,
461            &reserved_file_path,
462            &format!("table {}", table_name),
463        )
464        .await
465        .map_err(|e| match e {
466            MarkerFileError::AlreadyExists { .. } => {
467                lance_core::Error::from(NamespaceError::TableAlreadyExists {
468                    message: table_name.to_string(),
469                })
470            }
471            MarkerFileError::Other { message } => {
472                lance_core::Error::from(NamespaceError::Internal { message })
473            }
474        })?;
475
476        // For backwards compatibility, only skip vending credentials when explicitly set to false
477        let vend_credentials = request.vend_credentials.unwrap_or(true);
478        let identity = request.identity.as_deref();
479        let storage_options = self
480            .get_storage_options_for_table(&table_uri, vend_credentials, identity)
481            .await?;
482
483        Ok(DeclareTableResponse {
484            location: Some(table_uri),
485            storage_options,
486            properties: request.properties,
487            managed_versioning: if self.table_version_tracking_enabled {
488                Some(true)
489            } else {
490                None
491            },
492            ..Default::default()
493        })
494    }
495
496    async fn register_table(
497        &self,
498        request: lance_namespace::models::RegisterTableRequest,
499    ) -> Result<lance_namespace::models::RegisterTableResponse> {
500        self.record_op("register_table");
501        // If manifest is enabled, delegate to manifest namespace
502        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
503            return LanceNamespace::register_table(manifest_ns.as_ref(), request).await;
504        }
505
506        // Without manifest, register_table is not supported
507        Err(NamespaceError::Unsupported {
508            message: "register_table is only supported when manifest mode is enabled".to_string(),
509        }
510        .into())
511    }
512
513    async fn deregister_table(
514        &self,
515        request: lance_namespace::models::DeregisterTableRequest,
516    ) -> Result<lance_namespace::models::DeregisterTableResponse> {
517        self.record_op("deregister_table");
518        // If manifest is enabled, delegate to manifest namespace
519        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
520            return LanceNamespace::deregister_table(manifest_ns.as_ref(), request).await;
521        }
522
523        // V1 mode: create a .lance-deregistered marker file in the table directory
524        let table_name = Self::table_name_from_id(&request.id)?;
525        let table_uri = self.table_full_uri(&table_name);
526
527        // Check table existence and deregistration status.
528        // This provides better error messages for common cases.
529        let status = self.check_table_status(&table_name).await?;
530
531        if !status.exists {
532            return Err(NamespaceError::TableNotFound {
533                message: table_name.to_string(),
534            }
535            .into());
536        }
537
538        if status.is_deregistered {
539            return Err(NamespaceError::TableNotFound {
540                message: format!("Table is already deregistered: {}", table_name),
541            }
542            .into());
543        }
544
545        // Atomically create the .lance-deregistered marker file.
546        // This uses put_if_not_exists semantics to prevent race conditions
547        // when multiple processes try to deregister the same table concurrently.
548        // If a race occurs and another process already created the file,
549        // we'll get an AlreadyExists error which we convert to a proper message.
550        let deregistered_path = self.table_deregistered_file_path(&table_name);
551        put_marker_file_atomic(
552            &self.object_store,
553            &deregistered_path,
554            &format!("deregistration marker for table {}", table_name),
555        )
556        .await
557        .map_err(|e| match e {
558            MarkerFileError::AlreadyExists { .. } => {
559                lance_core::Error::from(NamespaceError::InvalidTableState {
560                    message: format!("Table is already deregistered: {}", table_name),
561                })
562            }
563            MarkerFileError::Other { message } => {
564                lance_core::Error::from(NamespaceError::Internal { message })
565            }
566        })?;
567
568        Ok(lance_namespace::models::DeregisterTableResponse {
569            id: request.id,
570            location: Some(table_uri),
571            ..Default::default()
572        })
573    }
574
575    async fn alter_table_add_columns(
576        &self,
577        request: AlterTableAddColumnsRequest,
578    ) -> Result<AlterTableAddColumnsResponse> {
579        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
580            return manifest_ns.alter_table_add_columns(request).await;
581        }
582
583        // Non-manifest mode: open Dataset directly via table URI and perform the operation
584        let table_name = Self::table_name_from_id(&request.id)?;
585        let table_uri = self.table_full_uri(&table_name);
586
587        // Check table existence and deregistration status before opening the dataset
588        let status = self.check_table_status(&table_name).await?;
589        if !status.exists {
590            return Err(NamespaceError::TableNotFound {
591                message: table_name,
592            }
593            .into());
594        }
595        if status.is_deregistered {
596            return Err(NamespaceError::TableNotFound {
597                message: format!("Table is deregistered: {}", table_name),
598            }
599            .into());
600        }
601
602        let mut dataset = self
603            .configured_builder(&table_uri)
604            .load()
605            .await
606            .map_err(|e| {
607                Error::io_source(box_error(std::io::Error::other(format!(
608                    "Failed to open dataset: {}",
609                    e
610                ))))
611            })?;
612
613        let sql_expressions = build_sql_expressions(&request.new_columns)?;
614
615        dataset
616            .add_columns(
617                lance::dataset::NewColumnTransform::SqlExpressions(sql_expressions),
618                None,
619                None,
620            )
621            .await
622            .map_err(|e| {
623                Error::io_source(box_error(std::io::Error::other(format!(
624                    "Failed to add columns: {}",
625                    e
626                ))))
627            })?;
628
629        let version = dataset.version().version as i64;
630        Ok(AlterTableAddColumnsResponse::new(version))
631    }
632
633    async fn alter_table_alter_columns(
634        &self,
635        request: AlterTableAlterColumnsRequest,
636    ) -> Result<AlterTableAlterColumnsResponse> {
637        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
638            return manifest_ns.alter_table_alter_columns(request).await;
639        }
640
641        let table_name = Self::table_name_from_id(&request.id)?;
642        let table_uri = self.table_full_uri(&table_name);
643
644        // Check table existence and deregistration status before opening the dataset
645        let status = self.check_table_status(&table_name).await?;
646        if !status.exists {
647            return Err(NamespaceError::TableNotFound {
648                message: table_name,
649            }
650            .into());
651        }
652        if status.is_deregistered {
653            return Err(NamespaceError::TableNotFound {
654                message: format!("Table is deregistered: {}", table_name),
655            }
656            .into());
657        }
658
659        let mut dataset = self
660            .configured_builder(&table_uri)
661            .load()
662            .await
663            .map_err(|e| {
664                Error::io_source(box_error(std::io::Error::other(format!(
665                    "Failed to open dataset: {}",
666                    e
667                ))))
668            })?;
669
670        let alterations = build_column_alterations(&request.alterations)?;
671
672        dataset.alter_columns(&alterations).await.map_err(|e| {
673            Error::io_source(box_error(std::io::Error::other(format!(
674                "Failed to alter columns: {}",
675                e
676            ))))
677        })?;
678
679        let version = dataset.version().version as i64;
680        Ok(AlterTableAlterColumnsResponse::new(version))
681    }
682
683    async fn alter_table_drop_columns(
684        &self,
685        request: AlterTableDropColumnsRequest,
686    ) -> Result<AlterTableDropColumnsResponse> {
687        if let Some(manifest_ns) = self.manifest_ns_for_write().await? {
688            return manifest_ns.alter_table_drop_columns(request).await;
689        }
690
691        let table_name = Self::table_name_from_id(&request.id)?;
692        let table_uri = self.table_full_uri(&table_name);
693
694        // Check table existence and deregistration status before opening the dataset
695        let status = self.check_table_status(&table_name).await?;
696        if !status.exists {
697            return Err(NamespaceError::TableNotFound {
698                message: table_name,
699            }
700            .into());
701        }
702        if status.is_deregistered {
703            return Err(NamespaceError::TableNotFound {
704                message: format!("Table is deregistered: {}", table_name),
705            }
706            .into());
707        }
708
709        let mut dataset = self
710            .configured_builder(&table_uri)
711            .load()
712            .await
713            .map_err(|e| {
714                Error::io_source(box_error(std::io::Error::other(format!(
715                    "Failed to open dataset: {}",
716                    e
717                ))))
718            })?;
719
720        let columns: Vec<&str> = request.columns.iter().map(|s| s.as_str()).collect();
721        dataset.drop_columns(&columns).await.map_err(|e| {
722            Error::io_source(box_error(std::io::Error::other(format!(
723                "Failed to drop columns: {}",
724                e
725            ))))
726        })?;
727
728        let version = dataset.version().version as i64;
729        Ok(AlterTableDropColumnsResponse::new(version))
730    }
731
732    async fn list_table_versions(
733        &self,
734        request: ListTableVersionsRequest,
735    ) -> Result<ListTableVersionsResponse> {
736        self.record_op("list_table_versions");
737        let branch = Self::normalized_branch(request.branch.as_deref())?;
738        let table_uri = self.resolve_table_location(&request.id).await?;
739        let table_uri = match branch {
740            Some(b) => self.resolve_branch_location(&table_uri, b).await?,
741            None => table_uri,
742        };
743        let want_descending = request.descending == Some(true);
744        let table_versions = self
745            .list_table_versions_from_storage(&table_uri, want_descending, request.limit)
746            .await?;
747
748        Ok(ListTableVersionsResponse {
749            versions: table_versions,
750            page_token: None,
751            ..Default::default()
752        })
753    }
754
755    async fn create_table_version(
756        &self,
757        request: CreateTableVersionRequest,
758    ) -> Result<CreateTableVersionResponse> {
759        self.record_op("create_table_version");
760        let branch = Self::normalized_branch(request.branch.as_deref())?;
761        let table_uri = self.resolve_table_location(&request.id).await?;
762        let (table_uri, table_path, branch_parent_version) = match branch {
763            Some(b) => self.resolve_branch_for_commit(&table_uri, b).await?,
764            None => {
765                let table_path = self.object_store_path_from_uri(&table_uri)?;
766                (table_uri, table_path, None)
767            }
768        };
769
770        let staging_manifest_path = &request.manifest_path;
771        let version = request.version as u64;
772
773        // Determine naming scheme from request, default to V2
774        let naming_scheme = match request.naming_scheme.as_deref() {
775            Some("V1") => ManifestNamingScheme::V1,
776            _ => ManifestNamingScheme::V2,
777        };
778
779        // Compute final path using the naming scheme
780        let final_path = naming_scheme.manifest_path(&table_path, version);
781
782        let staging_path = Path::parse(staging_manifest_path).map_err(|e| {
783            lance_core::Error::from(NamespaceError::InvalidInput {
784                message: format!(
785                    "Invalid staging manifest path '{}': {}",
786                    staging_manifest_path, e
787                ),
788            })
789        })?;
790
791        // Idempotent retry: version path already published with the same content.
792        match self.object_store.inner.head(&final_path).await {
793            Ok(existing_meta) => {
794                return self
795                    .resolve_existing_table_version(ExistingTableVersionResolve {
796                        staging_path: &staging_path,
797                        final_path: &final_path,
798                        version,
799                        table_uri: &table_uri,
800                        final_meta: &existing_meta,
801                        request_manifest_size: request.manifest_size,
802                    })
803                    .await;
804            }
805            Err(ObjectStoreError::NotFound { .. }) => {}
806            Err(e) => {
807                return Err(lance_core::Error::from(NamespaceError::Internal {
808                    message: format!(
809                        "Failed to stat version {} for table at '{}': {}",
810                        version, table_uri, e
811                    ),
812                }));
813            }
814        }
815
816        // Strict CAS: only allow appending latest+1 (or the empty-chain bootstrap
817        // version: v1 on main, BranchContents.parent_version on a registered branch).
818        let is_branch = branch.is_some();
819        self.enforce_create_table_version_cas(
820            &table_path,
821            version,
822            &table_uri,
823            is_branch,
824            branch_parent_version,
825        )
826        .await?;
827
828        // Materialize with Create / copy_if_not_exists only — never overwrite.
829        let copy_result = self
830            .materialize_version_manifest_create(&staging_path, &final_path, staging_manifest_path)
831            .await;
832
833        match copy_result {
834            Ok(()) => {}
835            Err(ObjectStoreError::AlreadyExists { .. })
836            | Err(ObjectStoreError::Precondition { .. }) => {
837                // Lost a Create race: succeed only if the winner published identical bytes.
838                let existing_meta = self.object_store.inner.head(&final_path).await.map_err(|e| {
839                    lance_core::Error::from(NamespaceError::Internal {
840                        message: format!(
841                            "Version {} conflict for table at '{}' but failed to stat winner: {}",
842                            version, table_uri, e
843                        ),
844                    })
845                })?;
846                return self
847                    .resolve_existing_table_version(ExistingTableVersionResolve {
848                        staging_path: &staging_path,
849                        final_path: &final_path,
850                        version,
851                        table_uri: &table_uri,
852                        final_meta: &existing_meta,
853                        request_manifest_size: request.manifest_size,
854                    })
855                    .await;
856            }
857            Err(ObjectStoreError::NotFound { .. }) => {
858                return Err(lance_core::Error::from(NamespaceError::InvalidInput {
859                    message: format!(
860                        "Staging manifest not found at '{}' for version {} of table at '{}'",
861                        staging_manifest_path, version, table_uri
862                    ),
863                }));
864            }
865            Err(e) => {
866                return Err(lance_core::Error::from(NamespaceError::Internal {
867                    message: format!(
868                        "Failed to create version {} for table at '{}': {}",
869                        version, table_uri, e
870                    ),
871                }));
872            }
873        }
874
875        let final_meta = self
876            .object_store
877            .inner
878            .head(&final_path)
879            .await
880            .map_err(|e| {
881                lance_core::Error::from(NamespaceError::Internal {
882                    message: format!(
883                        "Failed to stat created version {} for table at '{}': {}",
884                        version, table_uri, e
885                    ),
886                })
887            })?;
888
889        // Delete the staging manifest after successful copy
890        if let Err(e) = self.object_store.inner.delete(&staging_path).await {
891            log::warn!(
892                "Failed to delete staging manifest at '{}': {:?}",
893                staging_path,
894                e
895            );
896        }
897
898        Ok(Self::create_table_version_response(
899            version,
900            &final_path,
901            &final_meta,
902        ))
903    }
904
905    async fn describe_table_version(
906        &self,
907        request: DescribeTableVersionRequest,
908    ) -> Result<DescribeTableVersionResponse> {
909        self.record_op("describe_table_version");
910        let branch = Self::normalized_branch(request.branch.as_deref())?;
911        let table_uri = self.resolve_table_location(&request.id).await?;
912        let table_uri = match branch {
913            Some(b) => self.resolve_branch_location(&table_uri, b).await?,
914            None => table_uri,
915        };
916        let versions = self
917            .list_table_versions_from_storage(&table_uri, true, None)
918            .await?;
919        let table_version = if let Some(requested_version) = request.version {
920            versions
921                .into_iter()
922                .find(|version| version.version == requested_version)
923                .ok_or_else(|| {
924                    lance_core::Error::from(NamespaceError::TableVersionNotFound {
925                        message: format!(
926                            "version {} for table {}",
927                            requested_version,
928                            Self::format_table_id_from_request(&request.id)
929                        ),
930                    })
931                })?
932        } else {
933            versions.into_iter().next().ok_or_else(|| {
934                lance_core::Error::from(NamespaceError::TableVersionNotFound {
935                    message: format!(
936                        "latest version for table {}",
937                        Self::format_table_id_from_request(&request.id)
938                    ),
939                })
940            })?
941        };
942
943        Ok(DescribeTableVersionResponse {
944            version: Box::new(table_version),
945            ..Default::default()
946        })
947    }
948
949    async fn batch_delete_table_versions(
950        &self,
951        request: BatchDeleteTableVersionsRequest,
952    ) -> Result<BatchDeleteTableVersionsResponse> {
953        self.record_op("batch_delete_table_versions");
954        let branch = Self::normalized_branch(request.branch.as_deref())?;
955        // Single-table mode: use `id` (from path parameter) + `ranges` to delete
956        // versions from one table.
957        let ranges: Vec<(i64, i64)> = request
958            .ranges
959            .iter()
960            .map(|r| (r.start_version, r.end_version))
961            .collect();
962
963        // Reject pathological bounded ranges up front: an explicit huge bounded
964        // range like (0, i64::MAX) is almost certainly a mistake. A through-latest
965        // range (end < 0) is bounded by the manifests that actually exist on storage.
966        const MAX_VERSIONS_PER_REQUEST: i128 = 1_000_000;
967        let requested: i128 = ranges
968            .iter()
969            .map(|(s, e)| {
970                if *e < 0 {
971                    0
972                } else {
973                    (*e as i128 - *s as i128).max(0)
974                }
975            })
976            .sum();
977        if requested > MAX_VERSIONS_PER_REQUEST {
978            return Err(NamespaceError::InvalidInput {
979                message: format!(
980                    "batch_delete requested {} versions; limit is {}",
981                    requested, MAX_VERSIONS_PER_REQUEST
982                ),
983            }
984            .into());
985        }
986
987        let table_entries = vec![TableDeleteEntry {
988            table_id: request.id.clone(),
989            ranges,
990        }];
991
992        let total_deleted_count = self
993            .delete_physical_version_files(&table_entries, branch)
994            .await?;
995
996        Ok(BatchDeleteTableVersionsResponse {
997            deleted_count: Some(total_deleted_count),
998            transaction_id: None,
999            ..Default::default()
1000        })
1001    }
1002
1003    async fn create_table_index(
1004        &self,
1005        request: CreateTableIndexRequest,
1006    ) -> Result<CreateTableIndexResponse> {
1007        self.record_op("create_table_index");
1008        let table_uri = self.resolve_table_location(&request.id).await?;
1009        let mut dataset = self
1010            .load_dataset(&table_uri, None, "create_table_index")
1011            .await?;
1012        let index_request = Self::build_index_params(&request)?;
1013
1014        dataset
1015            .create_index(
1016                &[request.column.as_str()],
1017                index_request.index_type(),
1018                request.name.clone(),
1019                index_request.params(),
1020                false,
1021            )
1022            .await
1023            .map_err(|e| {
1024                let err_msg = format!("{}", e);
1025                let ns_err = if err_msg.contains("already exists") {
1026                    NamespaceError::TableIndexAlreadyExists {
1027                        message: format!(
1028                            "Index '{}' already exists on table '{}': {:?}",
1029                            request.name.as_deref().unwrap_or("<auto-generated>"),
1030                            table_uri,
1031                            e
1032                        ),
1033                    }
1034                } else if err_msg.contains("not found") || err_msg.contains("does not exist") {
1035                    NamespaceError::TableColumnNotFound {
1036                        message: format!(
1037                            "Column '{}' not found for table '{}': {:?}",
1038                            request.column, table_uri, e
1039                        ),
1040                    }
1041                } else {
1042                    NamespaceError::Internal {
1043                        message: format!(
1044                            "Failed to create {} index '{}' on column '{}' for table '{}': {:?}",
1045                            request.index_type,
1046                            request.name.as_deref().unwrap_or("<auto-generated>"),
1047                            request.column,
1048                            table_uri,
1049                            e
1050                        ),
1051                    }
1052                };
1053                lance_core::Error::from(ns_err)
1054            })?;
1055
1056        let transaction_id = dataset
1057            .read_transaction()
1058            .await
1059            .map_err(|e| {
1060                lance_core::Error::from(NamespaceError::Internal {
1061                    message: format!(
1062                        "Failed to read committed transaction after creating index on '{}': {}",
1063                        table_uri, e
1064                    ),
1065                })
1066            })?
1067            .map(|transaction| transaction.uuid);
1068
1069        Ok(CreateTableIndexResponse {
1070            transaction_id,
1071            ..Default::default()
1072        })
1073    }
1074
1075    async fn list_table_indices(
1076        &self,
1077        request: ListTableIndicesRequest,
1078    ) -> Result<ListTableIndicesResponse> {
1079        self.record_op("list_table_indices");
1080        let table_uri = self.resolve_table_location(&request.id).await?;
1081        let dataset = self
1082            .load_dataset(&table_uri, request.version, "list_table_indices")
1083            .await?;
1084        let total_rows = dataset.count_rows(None).await.map_err(|e| {
1085            lance_core::Error::from(NamespaceError::Internal {
1086                message: format!("Failed to count rows for table '{}': {:?}", table_uri, e),
1087            })
1088        })? as u64;
1089        let mut indices = dataset
1090            .describe_indices(None)
1091            .await
1092            .map_err(|e| {
1093                lance_core::Error::from(NamespaceError::Internal {
1094                    message: format!("Failed to describe table indices for '{}': {:?}", table_uri, e),
1095                })
1096            })?
1097            .into_iter()
1098            .filter(|description| {
1099                description
1100                    .metadata()
1101                    .first()
1102                    .map(|metadata| !is_system_index(metadata))
1103                    .unwrap_or(false)
1104            })
1105            .map(|description| {
1106                let columns = description
1107                    .field_ids()
1108                    .iter()
1109                        .map(|field_id| {
1110                        dataset
1111                            .schema()
1112                            .field_path_minimal(i32::try_from(*field_id).map_err(|e| {
1113                                lance_core::Error::from(NamespaceError::Internal {
1114                                    message: format!(
1115                                        "Field id {} does not fit in i32 for table '{}': {}",
1116                                        field_id, table_uri, e
1117                                    ),
1118                                })
1119                            })?)
1120                            .map_err(|e| {
1121                            lance_core::Error::from(NamespaceError::Internal {
1122                                message: format!(
1123                                    "Failed to resolve field path for field_id {} in table '{}': {}",
1124                                    field_id, table_uri, e
1125                                ),
1126                            })
1127                        })
1128                    })
1129                    .collect::<Result<Vec<_>>>()?;
1130
1131                let segments = description.segments();
1132                let created_at = segments
1133                    .iter()
1134                    .filter_map(|segment| segment.created_at)
1135                    .min()
1136                    .map(|ts| ts.to_rfc3339());
1137
1138                // `..Default::default()` keeps this tolerant of additive reqwest
1139                // client model changes (see #7212).
1140                #[allow(clippy::needless_update)]
1141                let content = IndexContent {
1142                    index_name: description.name().to_string(),
1143                    index_uuid: description.metadata()[0].uuid.to_string(),
1144                    columns,
1145                    status: "SUCCEEDED".to_string(),
1146                    index_type: Some(description.index_type().to_string()),
1147                    type_url: Some(description.type_url().to_string()),
1148                    num_indexed_rows: Some(description.rows_indexed() as i64),
1149                    num_unindexed_rows: Some(
1150                        total_rows.saturating_sub(description.rows_indexed()) as i64,
1151                    ),
1152                    size_bytes: description.total_size_bytes().map(|size| size as i64),
1153                    num_segments: Some(segments.len() as i32),
1154                    created_at,
1155                    index_version: segments.first().map(|segment| segment.index_version),
1156                    index_details: description.details().ok(),
1157                    ..Default::default()
1158                };
1159                Ok(content)
1160            })
1161            .collect::<Result<Vec<_>>>()?;
1162
1163        let page_token = Self::paginate_indices(&mut indices, request.page_token, request.limit);
1164        Ok(ListTableIndicesResponse {
1165            indexes: indices,
1166            page_token,
1167            ..Default::default()
1168        })
1169    }
1170
1171    async fn describe_table_index_stats(
1172        &self,
1173        request: DescribeTableIndexStatsRequest,
1174    ) -> Result<DescribeTableIndexStatsResponse> {
1175        self.record_op("describe_table_index_stats");
1176        let table_uri = self.resolve_table_location(&request.id).await?;
1177        let dataset = self
1178            .load_dataset(&table_uri, request.version, "describe_table_index_stats")
1179            .await?;
1180        let index_name = request.index_name.as_deref().ok_or_else(|| {
1181            lance_core::Error::from(NamespaceError::InvalidInput {
1182                message: "Index name is required for describe_table_index_stats".to_string(),
1183            })
1184        })?;
1185        let metadatas = dataset
1186            .load_indices_by_name(index_name)
1187            .await
1188            .map_err(|e| {
1189                lance_core::Error::from(NamespaceError::TableIndexNotFound {
1190                    message: format!(
1191                        "Failed to load index '{}' metadata for table '{}': {}",
1192                        index_name, table_uri, e
1193                    ),
1194                })
1195            })?;
1196        if metadatas.first().is_some_and(is_system_index) {
1197            return Err(NamespaceError::Unsupported {
1198                message: format!("System index '{}' is not exposed by this API", index_name),
1199            }
1200            .into());
1201        }
1202
1203        let stats = <Dataset as DatasetIndexExt>::index_statistics(&dataset, index_name)
1204            .await
1205            .map_err(|e| {
1206                lance_core::Error::from(NamespaceError::TableIndexNotFound {
1207                    message: format!(
1208                        "Failed to describe index statistics for '{}' on table '{}': {}",
1209                        index_name, table_uri, e
1210                    ),
1211                })
1212            })?;
1213        let stats: serde_json::Value = serde_json::from_str(&stats).map_err(|e| {
1214            lance_core::Error::from(NamespaceError::Internal {
1215                message: format!(
1216                    "Failed to parse index statistics for '{}' on table '{}': {}",
1217                    index_name, table_uri, e
1218                ),
1219            })
1220        })?;
1221
1222        Ok(Self::describe_table_index_stats_response(&stats))
1223    }
1224
1225    async fn describe_transaction(
1226        &self,
1227        request: DescribeTransactionRequest,
1228    ) -> Result<DescribeTransactionResponse> {
1229        self.record_op("describe_transaction");
1230        let mut request_id = request.id.ok_or_else(|| {
1231            lance_core::Error::from(NamespaceError::InvalidInput {
1232                message: "Transaction id must include table id and transaction identifier"
1233                    .to_string(),
1234            })
1235        })?;
1236        if request_id.len() < 2 {
1237            return Err(NamespaceError::InvalidInput {
1238                message: format!(
1239                    "Transaction request id must include table id and transaction identifier, got {:?}",
1240                    request_id
1241                ),
1242            }
1243            .into());
1244        }
1245
1246        let id = request_id.pop().expect("request_id len checked above");
1247        let table_id = Some(request_id);
1248        let table_uri = self.resolve_table_location(&table_id).await?;
1249        let dataset = self
1250            .load_dataset(&table_uri, None, "describe_transaction")
1251            .await?;
1252        let (version, transaction) = self.find_transaction(&dataset, &id).await?;
1253
1254        // Merge any persisted alter_transaction changes stored in the sidecar
1255        // so that describe_transaction reflects the latest altered state.
1256        let sidecar = self
1257            .load_transaction_alteration(&table_uri, &transaction.uuid)
1258            .await?;
1259
1260        Ok(Self::transaction_response(version, &transaction, sidecar))
1261    }
1262
1263    async fn alter_transaction(
1264        &self,
1265        request: AlterTransactionRequest,
1266    ) -> Result<AlterTransactionResponse> {
1267        self.record_op("alter_transaction");
1268
1269        // Parse the request ID: must include table id and transaction identifier
1270        let mut request_id = request.id.ok_or_else(|| {
1271            lance_core::Error::from(NamespaceError::InvalidInput {
1272                message: "Transaction id must include table id and transaction identifier"
1273                    .to_string(),
1274            })
1275        })?;
1276        if request_id.len() < 2 {
1277            return Err(NamespaceError::InvalidInput {
1278                message: format!(
1279                    "Transaction request id must include table id and transaction identifier, got {:?}",
1280                    request_id
1281                ),
1282            }
1283            .into());
1284        }
1285
1286        let txn_id = request_id.pop().expect("request_id len checked above");
1287        let table_id = Some(request_id);
1288        let table_uri = self.resolve_table_location(&table_id).await?;
1289        let dataset = self
1290            .load_dataset(&table_uri, None, "alter_transaction")
1291            .await?;
1292        let (version, transaction) = self.find_transaction(&dataset, &txn_id).await?;
1293
1294        // Reserved keys are derived from the immutable Transaction metadata and
1295        // must not be modified via alter_transaction. They are only surfaced in
1296        // the response for the caller's convenience.
1297        const RESERVED_KEYS: &[&str] = &["uuid", "version", "read_version", "operation", "tag"];
1298        let is_reserved = |key: &str| RESERVED_KEYS.contains(&key);
1299
1300        // Load the existing sidecar (if any) so alterations accumulate across
1301        // successive alter_transaction calls.
1302        let mut sidecar = self
1303            .load_transaction_alteration(&table_uri, &transaction.uuid)
1304            .await?
1305            .unwrap_or_default();
1306
1307        for action in &request.actions {
1308            if let Some(ref set_status) = action.set_status_action
1309                && let Some(ref status) = set_status.status
1310            {
1311                // Validate the status value (case-insensitive)
1312                let normalized = status.to_lowercase().replace('_', "");
1313                match normalized.as_str() {
1314                    "queued" | "running" | "succeeded" | "failed" | "canceled" => {
1315                        sidecar.status = Some(status.clone());
1316                    }
1317                    _ => {
1318                        return Err(NamespaceError::InvalidInput {
1319                            message: format!(
1320                                "Invalid transaction status '{}'. Valid values are: Queued, Running, Succeeded, Failed, Canceled",
1321                                status
1322                            ),
1323                        }
1324                        .into());
1325                    }
1326                }
1327            }
1328
1329            if let Some(ref set_property) = action.set_property_action
1330                && let (Some(key), Some(value)) = (&set_property.key, &set_property.value)
1331            {
1332                if is_reserved(key) {
1333                    return Err(NamespaceError::InvalidInput {
1334                        message: format!("Property '{}' is reserved and cannot be modified", key),
1335                    }
1336                    .into());
1337                }
1338                let mode = set_property
1339                    .mode
1340                    .as_deref()
1341                    .unwrap_or("Overwrite")
1342                    .to_lowercase();
1343                match mode.as_str() {
1344                    "overwrite" => {
1345                        sidecar.properties.insert(key.clone(), value.clone());
1346                    }
1347                    "fail" => {
1348                        // Consider both the immutable transaction properties
1349                        // and any values previously written to the sidecar.
1350                        let exists = sidecar.properties.contains_key(key)
1351                            || transaction
1352                                .transaction_properties
1353                                .as_ref()
1354                                .is_some_and(|props| props.contains_key(key));
1355                        if exists {
1356                            return Err(NamespaceError::ConcurrentModification {
1357                                message: format!(
1358                                    "Property '{}' already exists and mode is 'Fail'",
1359                                    key
1360                                ),
1361                            }
1362                            .into());
1363                        }
1364                        sidecar.properties.insert(key.clone(), value.clone());
1365                    }
1366                    "skip" => {
1367                        let exists = sidecar.properties.contains_key(key)
1368                            || transaction
1369                                .transaction_properties
1370                                .as_ref()
1371                                .is_some_and(|props| props.contains_key(key));
1372                        if !exists {
1373                            sidecar.properties.insert(key.clone(), value.clone());
1374                        }
1375                    }
1376                    _ => {
1377                        return Err(NamespaceError::InvalidInput {
1378                            message: format!(
1379                                "Invalid set_property mode '{}'. Valid values are: Overwrite, Fail, Skip",
1380                                mode
1381                            ),
1382                        }
1383                        .into());
1384                    }
1385                }
1386            }
1387
1388            if let Some(ref unset_property) = action.unset_property_action
1389                && let Some(ref key) = unset_property.key
1390            {
1391                if is_reserved(key) {
1392                    return Err(NamespaceError::InvalidInput {
1393                        message: format!("Property '{}' is reserved and cannot be modified", key),
1394                    }
1395                    .into());
1396                }
1397                let mode = unset_property
1398                    .mode
1399                    .as_deref()
1400                    .unwrap_or("Skip")
1401                    .to_lowercase();
1402                let exists_in_transaction = transaction
1403                    .transaction_properties
1404                    .as_ref()
1405                    .is_some_and(|props| props.contains_key(key));
1406                match mode.as_str() {
1407                    "skip" => {
1408                        sidecar.properties.remove(key);
1409                        if exists_in_transaction {
1410                            // Track a tombstone so describe_transaction can
1411                            // hide the immutable property from the response.
1412                            sidecar.removed_properties.insert(key.clone());
1413                        }
1414                    }
1415                    "fail" => {
1416                        if !sidecar.properties.contains_key(key) && !exists_in_transaction {
1417                            return Err(NamespaceError::InvalidInput {
1418                                message: format!(
1419                                    "Property '{}' does not exist and mode is 'Fail'",
1420                                    key
1421                                ),
1422                            }
1423                            .into());
1424                        }
1425                        sidecar.properties.remove(key);
1426                        if exists_in_transaction {
1427                            sidecar.removed_properties.insert(key.clone());
1428                        }
1429                    }
1430                    _ => {
1431                        return Err(NamespaceError::InvalidInput {
1432                            message: format!(
1433                                "Invalid unset_property mode '{}'. Valid values are: Skip, Fail",
1434                                mode
1435                            ),
1436                        }
1437                        .into());
1438                    }
1439                }
1440            }
1441        }
1442
1443        // Persist the accumulated alterations so subsequent calls observe
1444        // them. The transaction file itself is immutable in Lance, so we
1445        // record alter_transaction outcomes in a namespace-owned sidecar.
1446        self.save_transaction_alteration(&table_uri, &transaction.uuid, &sidecar)
1447            .await?;
1448
1449        // Assemble the response by merging the immutable transaction metadata
1450        // with the persisted alterations.
1451        let final_status = sidecar
1452            .status
1453            .clone()
1454            .unwrap_or_else(|| "SUCCEEDED".to_string());
1455        let response = Self::transaction_response(version, &transaction, Some(sidecar));
1456        Ok(AlterTransactionResponse {
1457            status: final_status,
1458            properties: response.properties,
1459            ..Default::default()
1460        })
1461    }
1462
1463    async fn create_table_scalar_index(
1464        &self,
1465        request: CreateTableIndexRequest,
1466    ) -> Result<CreateTableScalarIndexResponse> {
1467        self.record_op("create_table_scalar_index");
1468        let index_type = Self::parse_index_type(&request.index_type)?;
1469        if !index_type.is_scalar() {
1470            return Err(NamespaceError::InvalidInput {
1471                message: format!(
1472                    "create_table_scalar_index only supports scalar index types, got {}",
1473                    request.index_type
1474                ),
1475            }
1476            .into());
1477        }
1478
1479        let response = self.create_table_index(request).await?;
1480        Ok(CreateTableScalarIndexResponse {
1481            transaction_id: response.transaction_id,
1482            ..Default::default()
1483        })
1484    }
1485
1486    async fn drop_table_index(
1487        &self,
1488        request: DropTableIndexRequest,
1489    ) -> Result<DropTableIndexResponse> {
1490        self.record_op("drop_table_index");
1491        let table_uri = self.resolve_table_location(&request.id).await?;
1492        let index_name = request.index_name.as_deref().ok_or_else(|| {
1493            lance_core::Error::from(NamespaceError::InvalidInput {
1494                message: "Index name is required for drop_table_index".to_string(),
1495            })
1496        })?;
1497        let mut dataset = self
1498            .load_dataset(&table_uri, None, "drop_table_index")
1499            .await?;
1500        let metadatas = dataset
1501            .load_indices_by_name(index_name)
1502            .await
1503            .map_err(|e| {
1504                lance_core::Error::from(NamespaceError::TableIndexNotFound {
1505                    message: format!(
1506                        "Failed to load index '{}' before dropping it from table '{}': {}",
1507                        index_name, table_uri, e
1508                    ),
1509                })
1510            })?;
1511        if metadatas.first().is_some_and(is_system_index) {
1512            return Err(NamespaceError::Unsupported {
1513                message: format!(
1514                    "System index '{}' cannot be dropped via this API",
1515                    index_name
1516                ),
1517            }
1518            .into());
1519        }
1520
1521        dataset.drop_index(index_name).await.map_err(|e| {
1522            lance_core::Error::from(NamespaceError::TableIndexNotFound {
1523                message: format!(
1524                    "Failed to drop index '{}' from table '{}': {}",
1525                    index_name, table_uri, e
1526                ),
1527            })
1528        })?;
1529
1530        let transaction_id = dataset
1531            .read_transaction()
1532            .await
1533            .map_err(|e| {
1534                lance_core::Error::from(NamespaceError::Internal {
1535                    message: format!(
1536                        "Failed to read committed transaction after dropping index '{}' from '{}': {}",
1537                        index_name, table_uri, e
1538                    ),
1539                })
1540            })?
1541            .map(|transaction| transaction.uuid);
1542
1543        Ok(DropTableIndexResponse {
1544            transaction_id,
1545            ..Default::default()
1546        })
1547    }
1548
1549    async fn list_all_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
1550        // In dir-only mode there are no child namespaces, so all tables live in the
1551        // root directory. This is equivalent to listing the root namespace.
1552        let mut tables = self.list_directory_tables().await?;
1553        tables = self
1554            .filter_declared_tables(tables, request.include_declared.unwrap_or(true))
1555            .await?;
1556        Self::apply_pagination(&mut tables, request.page_token, request.limit);
1557        Ok(ListTablesResponse::new(tables))
1558    }
1559
1560    async fn restore_table(&self, request: RestoreTableRequest) -> Result<RestoreTableResponse> {
1561        let version = request.version;
1562        if version < 0 {
1563            return Err(Error::invalid_input_source(
1564                format!(
1565                    "Table version for restore_table must be non-negative, got {}",
1566                    version
1567                )
1568                .into(),
1569            ));
1570        }
1571
1572        let branch = Self::normalized_branch(request.branch.as_deref())?;
1573        let table_uri = self.resolve_table_location(&request.id).await?;
1574        let mut dataset = match branch {
1575            Some(branch) => self.open_validated_branch(&table_uri, branch).await?,
1576            None => self.load_dataset(&table_uri, None, "restore_table").await?,
1577        };
1578
1579        dataset = dataset
1580            .checkout_version(version as u64)
1581            .await
1582            .map_err(|e| {
1583                Error::namespace_source(
1584                    format!(
1585                        "Failed to checkout version {} for restore at '{}': {}",
1586                        version, table_uri, e
1587                    )
1588                    .into(),
1589                )
1590            })?;
1591
1592        dataset.restore().await.map_err(|e| {
1593            Error::namespace_source(
1594                format!(
1595                    "Failed to restore table at '{}' to version {}: {}",
1596                    table_uri, version, e
1597                )
1598                .into(),
1599            )
1600        })?;
1601
1602        let transaction_id = dataset
1603            .read_transaction()
1604            .await
1605            .map_err(|e| {
1606                Error::namespace_source(
1607                    format!(
1608                        "Failed to read transaction after restoring '{}': {}",
1609                        table_uri, e
1610                    )
1611                    .into(),
1612                )
1613            })?
1614            .map(|t| t.uuid);
1615
1616        Ok(RestoreTableResponse {
1617            transaction_id,
1618            ..Default::default()
1619        })
1620    }
1621
1622    async fn update_table_schema_metadata(
1623        &self,
1624        request: UpdateTableSchemaMetadataRequest,
1625    ) -> Result<UpdateTableSchemaMetadataResponse> {
1626        let table_uri = self.resolve_table_location(&request.id).await?;
1627        let mut dataset = self
1628            .load_dataset(&table_uri, None, "update_table_schema_metadata")
1629            .await?;
1630
1631        let new_metadata = request.metadata.unwrap_or_default();
1632        let updated_metadata = dataset
1633            .update_schema_metadata(new_metadata.iter().map(|(k, v)| (k.as_str(), v.as_str())))
1634            .await
1635            .map_err(|e| {
1636                Error::namespace_source(
1637                    format!(
1638                        "Failed to update schema metadata for table at '{}': {}",
1639                        table_uri, e
1640                    )
1641                    .into(),
1642                )
1643            })?;
1644
1645        let transaction_id = dataset
1646            .read_transaction()
1647            .await
1648            .map_err(|e| {
1649                Error::namespace_source(
1650                    format!(
1651                        "Failed to read transaction after updating metadata for '{}': {}",
1652                        table_uri, e
1653                    )
1654                    .into(),
1655                )
1656            })?
1657            .map(|t| t.uuid);
1658
1659        Ok(UpdateTableSchemaMetadataResponse {
1660            metadata: Some(updated_metadata),
1661            transaction_id,
1662            ..Default::default()
1663        })
1664    }
1665
1666    async fn get_table_stats(
1667        &self,
1668        request: GetTableStatsRequest,
1669    ) -> Result<GetTableStatsResponse> {
1670        let table_uri = self.resolve_table_location(&request.id).await?;
1671        let branch = request.branch.as_deref();
1672        let dataset = Arc::new(
1673            self.load_branch_dataset(&table_uri, branch, None, "get_table_stats")
1674                .await?,
1675        );
1676
1677        // Compute total bytes on disk using field-level statistics
1678        let data_stats = dataset.calculate_data_stats().await.map_err(|e| {
1679            Error::namespace_source(
1680                format!(
1681                    "Failed to calculate data statistics for table at '{}': {}",
1682                    table_uri, e
1683                )
1684                .into(),
1685            )
1686        })?;
1687        let total_bytes: i64 = data_stats
1688            .fields
1689            .iter()
1690            .map(|f| f.bytes_on_disk as i64)
1691            .sum();
1692
1693        // Collect per-fragment row counts
1694        let fragment_row_futures: Vec<_> = dataset
1695            .get_fragments()
1696            .into_iter()
1697            .map(|f| async move { f.physical_rows().await })
1698            .collect();
1699        let fragment_row_results = futures::future::join_all(fragment_row_futures).await;
1700        let mut fragment_row_counts: Vec<i64> = fragment_row_results
1701            .into_iter()
1702            .filter_map(|r| r.ok())
1703            .map(|r| r as i64)
1704            .collect();
1705
1706        let num_fragments = fragment_row_counts.len() as i64;
1707        let num_rows: i64 = fragment_row_counts.iter().sum();
1708
1709        // Fragments with fewer rows than the compaction target are considered "small",
1710        // consistent with CompactionOptions::target_rows_per_fragment default.
1711        const SMALL_FRAGMENT_THRESHOLD: i64 = 1024 * 1024;
1712        let num_small_fragments = fragment_row_counts
1713            .iter()
1714            .filter(|&&r| r < SMALL_FRAGMENT_THRESHOLD)
1715            .count() as i64;
1716
1717        // Compute length summary statistics
1718        fragment_row_counts.sort_unstable();
1719        let lengths = if fragment_row_counts.is_empty() {
1720            FragmentSummary::new(0, 0, 0, 0, 0, 0, 0)
1721        } else {
1722            let len = fragment_row_counts.len();
1723            let min = fragment_row_counts[0];
1724            let max = fragment_row_counts[len - 1];
1725            let mean = num_rows / num_fragments;
1726            let pct = |p: f64| fragment_row_counts[((len - 1) as f64 * p) as usize];
1727            FragmentSummary::new(min, max, mean, pct(0.25), pct(0.50), pct(0.75), pct(0.99))
1728        };
1729
1730        // Count non-system indices
1731        let indices = dataset.load_indices().await.map_err(|e| {
1732            Error::namespace_source(
1733                format!("Failed to load indices for table at '{}': {}", table_uri, e).into(),
1734            )
1735        })?;
1736        let num_indices = indices.iter().filter(|m| !is_system_index(m)).count() as i64;
1737
1738        let fragment_stats = FragmentStats::new(num_fragments, num_small_fragments, lengths);
1739        Ok(GetTableStatsResponse::new(
1740            total_bytes,
1741            num_rows,
1742            num_indices,
1743            fragment_stats,
1744        ))
1745    }
1746
1747    async fn explain_table_query_plan(
1748        &self,
1749        request: ExplainTableQueryPlanRequest,
1750    ) -> Result<String> {
1751        let table_uri = self.resolve_table_location(&request.id).await?;
1752        let dataset = self
1753            .load_dataset(
1754                &table_uri,
1755                request.query.version,
1756                "explain_table_query_plan",
1757            )
1758            .await?;
1759        let verbose = request.verbose.unwrap_or(false);
1760
1761        let mut scanner = dataset.scan();
1762        Self::apply_query_params_to_scanner(
1763            &mut scanner,
1764            request.query.filter.as_deref(),
1765            request.query.columns.as_deref(),
1766            request.query.vector_column.as_deref(),
1767            &request.query.vector,
1768            request.query.k,
1769            request.query.offset,
1770            request.query.prefilter,
1771            request.query.bypass_vector_index,
1772            request.query.nprobes,
1773            request.query.minimum_nprobes,
1774            request.query.maximum_nprobes,
1775            request.query.ef,
1776            request.query.refine_factor,
1777            request.query.distance_type.as_deref(),
1778            request.query.fast_search,
1779            request.query.with_row_id,
1780            request.query.lower_bound,
1781            request.query.upper_bound,
1782            "explain_table_query_plan",
1783        )?;
1784
1785        scanner.explain_plan(verbose).await.map_err(|e| {
1786            Error::namespace_source(
1787                format!(
1788                    "Failed to explain query plan for table at '{}': {}",
1789                    table_uri, e
1790                )
1791                .into(),
1792            )
1793        })
1794    }
1795
1796    async fn analyze_table_query_plan(
1797        &self,
1798        request: AnalyzeTableQueryPlanRequest,
1799    ) -> Result<String> {
1800        let table_uri = self.resolve_table_location(&request.id).await?;
1801        let dataset = self
1802            .load_dataset(&table_uri, request.version, "analyze_table_query_plan")
1803            .await?;
1804
1805        let mut scanner = dataset.scan();
1806        Self::apply_query_params_to_scanner(
1807            &mut scanner,
1808            request.filter.as_deref(),
1809            request.columns.as_deref(),
1810            request.vector_column.as_deref(),
1811            &request.vector,
1812            request.k,
1813            request.offset,
1814            request.prefilter,
1815            request.bypass_vector_index,
1816            request.nprobes,
1817            request.minimum_nprobes,
1818            request.maximum_nprobes,
1819            request.ef,
1820            request.refine_factor,
1821            request.distance_type.as_deref(),
1822            request.fast_search,
1823            request.with_row_id,
1824            request.lower_bound,
1825            request.upper_bound,
1826            "analyze_table_query_plan",
1827        )?;
1828
1829        scanner.analyze_plan().await.map_err(|e| {
1830            Error::namespace_source(
1831                format!(
1832                    "Failed to analyze query plan for table at '{}': {}",
1833                    table_uri, e
1834                )
1835                .into(),
1836            )
1837        })
1838    }
1839
1840    async fn count_table_rows(&self, request: CountTableRowsRequest) -> Result<i64> {
1841        self.record_op("count_table_rows");
1842        let table_uri = self.resolve_table_location(&request.id).await?;
1843        let branch = request.branch.as_deref();
1844        let dataset = self
1845            .load_branch_dataset(&table_uri, branch, request.version, "count_table_rows")
1846            .await?;
1847
1848        let count =
1849            dataset
1850                .count_rows(request.predicate)
1851                .await
1852                .map_err(|e| NamespaceError::Internal {
1853                    message: format!("Failed to count rows for table at '{}': {:?}", table_uri, e),
1854                })?;
1855
1856        Ok(count as i64)
1857    }
1858
1859    async fn insert_into_table(
1860        &self,
1861        request: InsertIntoTableRequest,
1862        request_data: Bytes,
1863    ) -> Result<InsertIntoTableResponse> {
1864        self.record_op("insert_into_table");
1865        let table_uri = self.resolve_table_location(&request.id).await?;
1866        let (reader, num_rows) =
1867            Self::ipc_reader_from_request_data(&request_data, "insert_into_table")?;
1868
1869        let mode = match request.mode.as_deref() {
1870            Some(m) if m.eq_ignore_ascii_case("overwrite") => WriteMode::Overwrite,
1871            Some(m) if m.eq_ignore_ascii_case("append") => WriteMode::Append,
1872            None => WriteMode::Append,
1873            Some(m) => {
1874                return Err(lance_namespace::error::NamespaceError::InvalidInput {
1875                    message: format!(
1876                        "Unsupported write mode '{}'. Supported modes are: 'append', 'overwrite'",
1877                        m
1878                    ),
1879                }
1880                .into());
1881            }
1882        };
1883
1884        let dataset = if !self.table_uri_has_actual_manifests(&table_uri).await? {
1885            self.write_reader_to_table(&table_uri, reader, WriteMode::Create, None)
1886                .await?
1887        } else {
1888            self.write_reader_to_table(&table_uri, reader, mode, None)
1889                .await?
1890        };
1891
1892        Ok(InsertIntoTableResponse {
1893            transaction_id: None,
1894            num_inserted_rows: Some(num_rows as i64),
1895            version: Some(dataset.version().version as i64),
1896            ..Default::default()
1897        })
1898    }
1899
1900    async fn merge_insert_into_table(
1901        &self,
1902        request: MergeInsertIntoTableRequest,
1903        request_data: Bytes,
1904    ) -> Result<MergeInsertIntoTableResponse> {
1905        self.record_op("merge_insert_into_table");
1906        let table_uri = self.resolve_table_location(&request.id).await?;
1907        let on = merge_insert_on_columns(request.on.as_deref(), "merge_insert_into_table")?;
1908
1909        let table_has_manifests = self.table_uri_has_actual_manifests(&table_uri).await?;
1910        let (reader, num_rows) =
1911            Self::ipc_reader_from_request_data(&request_data, "merge_insert_into_table")?;
1912
1913        if !table_has_manifests {
1914            let dataset = self
1915                .write_reader_to_table(&table_uri, reader, WriteMode::Create, None)
1916                .await?;
1917            let version = dataset.version().version as i64;
1918            return Ok(MergeInsertIntoTableResponse {
1919                transaction_id: None,
1920                num_updated_rows: Some(0),
1921                num_inserted_rows: Some(num_rows as i64),
1922                num_deleted_rows: Some(0),
1923                version: Some(version),
1924                ..Default::default()
1925            });
1926        }
1927
1928        let dataset = Arc::new(
1929            self.load_dataset(&table_uri, None, "merge_insert_into_table")
1930                .await?,
1931        );
1932
1933        let mut merge_builder =
1934            MergeInsertBuilder::try_new(dataset.clone(), on.to_vec()).map_err(|e| {
1935                lance_core::Error::from(NamespaceError::InvalidInput {
1936                    message: format!("Failed to create merge_insert_into_table builder: {}", e),
1937                })
1938            })?;
1939
1940        if let Some(filter) = request.when_matched_update_all_filt.as_deref() {
1941            let behavior = WhenMatched::update_if(dataset.as_ref(), filter).map_err(|e| {
1942                lance_core::Error::from(NamespaceError::InvalidInput {
1943                    message: format!(
1944                        "Invalid when_matched_update_all_filt for merge_insert_into_table: {}",
1945                        e
1946                    ),
1947                })
1948            })?;
1949            merge_builder.when_matched(behavior);
1950        } else if request.when_matched_update_all.unwrap_or(false) {
1951            merge_builder.when_matched(WhenMatched::UpdateAll);
1952        }
1953
1954        if matches!(request.when_not_matched_insert_all, Some(false)) {
1955            merge_builder.when_not_matched(WhenNotMatched::DoNothing);
1956        } else {
1957            merge_builder.when_not_matched(WhenNotMatched::InsertAll);
1958        }
1959
1960        if let Some(filter) = request.when_not_matched_by_source_delete_filt.as_deref() {
1961            let behavior = WhenNotMatchedBySource::delete_if(dataset.as_ref(), filter).map_err(|e| {
1962                lance_core::Error::from(NamespaceError::InvalidInput {
1963                    message: format!(
1964                        "Invalid when_not_matched_by_source_delete_filt for merge_insert_into_table: {}",
1965                        e
1966                    ),
1967                })
1968            })?;
1969            merge_builder.when_not_matched_by_source(behavior);
1970        } else if request.when_not_matched_by_source_delete.unwrap_or(false) {
1971            merge_builder.when_not_matched_by_source(WhenNotMatchedBySource::Delete);
1972        }
1973
1974        if let Some(use_index) = request.use_index {
1975            merge_builder.use_index(use_index);
1976        }
1977
1978        let (dataset, stats) = merge_builder
1979            .try_build()
1980            .map_err(|e| {
1981                lance_core::Error::from(NamespaceError::InvalidInput {
1982                    message: format!("Failed to build merge_insert_into_table job: {}", e),
1983                })
1984            })?
1985            .execute_reader(reader)
1986            .await
1987            .map_err(|e| Self::map_mutation_error(e, "merge_insert_into_table", &table_uri))?;
1988
1989        Ok(MergeInsertIntoTableResponse {
1990            transaction_id: None,
1991            num_updated_rows: Some(stats.num_updated_rows as i64),
1992            num_inserted_rows: Some(stats.num_inserted_rows as i64),
1993            num_deleted_rows: Some(stats.num_deleted_rows as i64),
1994            version: Some(dataset.version().version as i64),
1995            ..Default::default()
1996        })
1997    }
1998
1999    async fn update_table(&self, request: UpdateTableRequest) -> Result<UpdateTableResponse> {
2000        self.record_op("update_table");
2001
2002        if request.updates.is_empty() {
2003            return Err(NamespaceError::InvalidInput {
2004                message: "update_table requires at least one [column, expression] pair".to_string(),
2005            }
2006            .into());
2007        }
2008
2009        // Validate every update pair shape and detect duplicate columns up front so we
2010        // surface a clean error instead of failing deep inside the planner.
2011        let mut seen_columns: HashMap<String, ()> = HashMap::with_capacity(request.updates.len());
2012        for (idx, pair) in request.updates.iter().enumerate() {
2013            if pair.len() != 2 {
2014                return Err(NamespaceError::InvalidInput {
2015                    message: format!(
2016                        "update_table updates[{}] must be a [column, expression] pair, got {} elements",
2017                        idx,
2018                        pair.len()
2019                    ),
2020                }
2021                .into());
2022            }
2023            let column = &pair[0];
2024            if column.trim().is_empty() {
2025                return Err(NamespaceError::InvalidInput {
2026                    message: format!("update_table updates[{}] has an empty column name", idx),
2027                }
2028                .into());
2029            }
2030            if seen_columns.insert(column.clone(), ()).is_some() {
2031                return Err(NamespaceError::InvalidInput {
2032                    message: format!(
2033                        "update_table cannot update column '{}' more than once",
2034                        column
2035                    ),
2036                }
2037                .into());
2038            }
2039        }
2040
2041        let table_uri = self.resolve_table_location(&request.id).await?;
2042        let dataset = Arc::new(self.load_dataset(&table_uri, None, "update_table").await?);
2043
2044        let mut builder = UpdateBuilder::new(dataset);
2045        for pair in &request.updates {
2046            // Indexing by 0/1 is safe due to the length check above.
2047            builder = builder.set(&pair[0], &pair[1]).map_err(|e| {
2048                lance_core::Error::from(NamespaceError::InvalidInput {
2049                    message: format!("Invalid update expression for column '{}': {}", pair[0], e),
2050                })
2051            })?;
2052        }
2053        if let Some(predicate) = request.predicate.as_deref()
2054            && !predicate.trim().is_empty()
2055        {
2056            builder = builder.update_where(predicate).map_err(|e| {
2057                lance_core::Error::from(NamespaceError::InvalidInput {
2058                    message: format!("Invalid update_table predicate '{}': {}", predicate, e),
2059                })
2060            })?;
2061        }
2062
2063        let job = builder.build().map_err(|e| {
2064            lance_core::Error::from(NamespaceError::InvalidInput {
2065                message: format!("Failed to build update_table job: {}", e),
2066            })
2067        })?;
2068
2069        let result = job
2070            .execute()
2071            .await
2072            .map_err(|e| Self::map_mutation_error(e, "update_table", &table_uri))?;
2073
2074        let version = result.new_dataset.version().version as i64;
2075        Ok(UpdateTableResponse {
2076            transaction_id: None,
2077            updated_rows: result.rows_updated as i64,
2078            version,
2079            properties: None,
2080            ..Default::default()
2081        })
2082    }
2083
2084    async fn delete_from_table(
2085        &self,
2086        request: DeleteFromTableRequest,
2087    ) -> Result<DeleteFromTableResponse> {
2088        self.record_op("delete_from_table");
2089
2090        if request.predicate.trim().is_empty() {
2091            return Err(NamespaceError::InvalidInput {
2092                message: "delete_from_table requires a non-empty predicate".to_string(),
2093            }
2094            .into());
2095        }
2096
2097        let table_uri = self.resolve_table_location(&request.id).await?;
2098        let mut dataset = self
2099            .load_dataset(&table_uri, None, "delete_from_table")
2100            .await?;
2101
2102        let result = dataset
2103            .delete(&request.predicate)
2104            .await
2105            .map_err(|e| Self::map_mutation_error(e, "delete_from_table", &table_uri))?;
2106
2107        Ok(DeleteFromTableResponse {
2108            transaction_id: None,
2109            version: Some(result.new_dataset.version().version as i64),
2110            ..Default::default()
2111        })
2112    }
2113
2114    async fn query_table(&self, request: QueryTableRequest) -> Result<Bytes> {
2115        use arrow::ipc::writer::FileWriter;
2116
2117        self.record_op("query_table");
2118        let table_uri = self.resolve_table_location(&request.id).await?;
2119        let dataset = self
2120            .load_dataset(&table_uri, request.version, "query_table")
2121            .await?;
2122
2123        // Build scanner
2124        let mut scanner = dataset.scan();
2125
2126        // Check if this is a vector search query
2127        // vector is Box<QueryTableRequestVector>, not Option
2128        let has_vector_query = request
2129            .vector
2130            .single_vector
2131            .as_ref()
2132            .map(|sv| !sv.is_empty())
2133            .unwrap_or(false)
2134            || request
2135                .vector
2136                .multi_vector
2137                .as_ref()
2138                .map(|mv| !mv.is_empty())
2139                .unwrap_or(false);
2140
2141        // Apply prefilter setting (must be set before nearest)
2142        if let Some(prefilter) = request.prefilter {
2143            scanner.prefilter(prefilter);
2144        }
2145
2146        // Apply vector search if query vector is provided
2147        if has_vector_query {
2148            let vector_column = request.vector_column.as_deref().unwrap_or("vector");
2149
2150            // Get the query vector(s)
2151            let query_vector: Vec<f32> = request
2152                .vector
2153                .single_vector
2154                .clone()
2155                .or_else(|| {
2156                    request
2157                        .vector
2158                        .multi_vector
2159                        .as_ref()
2160                        .and_then(|mv| mv.first().cloned())
2161                })
2162                .unwrap_or_default();
2163
2164            if !query_vector.is_empty() {
2165                let k = if request.k > 0 {
2166                    request.k as usize
2167                } else {
2168                    10
2169                };
2170                let query_array = Float32Array::from(query_vector);
2171                scanner
2172                    .nearest(vector_column, &query_array, k)
2173                    .map_err(|e| NamespaceError::InvalidInput {
2174                        message: format!("Invalid vector search: {:?}", e),
2175                    })?;
2176
2177                // Apply distance type if specified
2178                if let Some(ref distance_type) = request.distance_type {
2179                    let metric = match distance_type.to_lowercase().as_str() {
2180                        "l2" | "euclidean" => MetricType::L2,
2181                        "cosine" => MetricType::Cosine,
2182                        "dot" | "inner_product" => MetricType::Dot,
2183                        "hamming" => MetricType::Hamming,
2184                        _ => {
2185                            return Err(NamespaceError::InvalidInput {
2186                                message: format!("Unknown distance type: {}", distance_type),
2187                            }
2188                            .into());
2189                        }
2190                    };
2191                    scanner.distance_metric(metric);
2192                }
2193
2194                apply_probe_bounds(
2195                    &mut scanner,
2196                    request.nprobes,
2197                    request.minimum_nprobes,
2198                    request.maximum_nprobes,
2199                )?;
2200
2201                // Apply ef (HNSW search effort) if specified
2202                if let Some(ef) = request.ef {
2203                    scanner.ef(ef as usize);
2204                }
2205
2206                // Apply refine_factor if specified
2207                if let Some(refine_factor) = request.refine_factor {
2208                    scanner.refine(refine_factor as u32);
2209                }
2210
2211                // Apply distance bounds if specified
2212                if request.lower_bound.is_some() || request.upper_bound.is_some() {
2213                    scanner.distance_range(request.lower_bound, request.upper_bound);
2214                }
2215
2216                // Apply use_index (inverse of bypass_vector_index)
2217                if let Some(bypass) = request.bypass_vector_index {
2218                    scanner.use_index(!bypass);
2219                }
2220
2221                // Apply fast_search if specified
2222                if request.fast_search == Some(true) {
2223                    scanner.fast_search();
2224                }
2225            }
2226        }
2227
2228        // Apply full text search if specified
2229        if let Some(ref fts_query) = request.full_text_query {
2230            // Handle string_query (simple string FTS)
2231            if let Some(ref string_query) = fts_query.string_query {
2232                let mut fts = FullTextSearchQuery::new(string_query.query.clone());
2233
2234                // Apply column filter if specified
2235                if let Some(ref columns) = string_query.columns
2236                    && !columns.is_empty()
2237                {
2238                    fts = fts
2239                        .with_columns(columns)
2240                        .map_err(|e| NamespaceError::InvalidInput {
2241                            message: format!("Invalid FTS columns: {:?}", e),
2242                        })?;
2243                }
2244
2245                scanner
2246                    .full_text_search(fts)
2247                    .map_err(|e| NamespaceError::InvalidInput {
2248                        message: format!("Invalid full text search: {:?}", e),
2249                    })?;
2250            } else if let Some(ref structured_query) = fts_query.structured_query {
2251                // Structured FTS: map the namespace query model into the engine FtsQuery.
2252                let engine_query = build_engine_fts_query(&structured_query.query)?;
2253                let fts = FullTextSearchQuery::new_query(engine_query);
2254                scanner
2255                    .full_text_search(fts)
2256                    .map_err(|e| NamespaceError::InvalidInput {
2257                        message: format!("Invalid full text search: {:?}", e),
2258                    })?;
2259            }
2260        }
2261
2262        // Apply column projection if specified
2263        if let Some(ref columns) = request.columns {
2264            if let Some(ref column_names) = columns.column_names
2265                && !column_names.is_empty()
2266            {
2267                scanner
2268                    .project(column_names)
2269                    .map_err(|e| NamespaceError::InvalidInput {
2270                        message: format!("Invalid column projection: {:?}", e),
2271                    })?;
2272            } else if let Some(ref column_aliases) = columns.column_aliases
2273                && !column_aliases.is_empty()
2274            {
2275                // column_aliases is HashMap<String, String> where key is alias, value is SQL expression
2276                let transform_pairs: Vec<(String, String)> = column_aliases
2277                    .iter()
2278                    .map(|(alias, sql)| (alias.clone(), sql.clone()))
2279                    .collect();
2280                scanner
2281                    .project_with_transform(
2282                        &transform_pairs
2283                            .iter()
2284                            .map(|(a, s)| (a.as_str(), s.as_str()))
2285                            .collect::<Vec<_>>(),
2286                    )
2287                    .map_err(|e| NamespaceError::InvalidInput {
2288                        message: format!("Invalid column alias expression: {:?}", e),
2289                    })?;
2290            }
2291        }
2292
2293        // Apply filter if specified
2294        if let Some(ref filter) = request.filter
2295            && !filter.is_empty()
2296        {
2297            scanner
2298                .filter(filter)
2299                .map_err(|e| NamespaceError::InvalidInput {
2300                    message: format!("Invalid filter expression: {:?}", e),
2301                })?;
2302        }
2303
2304        // Apply with_row_id if requested
2305        if request.with_row_id == Some(true) {
2306            scanner.with_row_id();
2307        }
2308
2309        // Apply limit if specified (k is the number of results to return)
2310        // k == 0 means no limit
2311        // Note: For vector search, limit is already applied via nearest()
2312        if !has_vector_query && request.k > 0 {
2313            let offset = request.offset.map(|o| o as i64);
2314            scanner.limit(Some(request.k as i64), offset).map_err(|e| {
2315                NamespaceError::InvalidInput {
2316                    message: format!("Invalid limit/offset: {:?}", e),
2317                }
2318            })?;
2319        } else if has_vector_query && request.offset.is_some() {
2320            // For vector search, offset is handled separately
2321            let offset = request.offset.map(|o| o as i64);
2322            scanner
2323                .limit(None, offset)
2324                .map_err(|e| NamespaceError::InvalidInput {
2325                    message: format!("Invalid offset: {:?}", e),
2326                })?;
2327        }
2328
2329        // Execute the scan and collect results
2330        let batch = scanner
2331            .try_into_batch()
2332            .await
2333            .map_err(|e| NamespaceError::Internal {
2334                message: format!("Failed to execute query: {:?}", e),
2335            })?;
2336
2337        // Serialize to Arrow IPC file format
2338        let schema = batch.schema();
2339        let mut buffer = Vec::new();
2340        {
2341            let mut writer = FileWriter::try_new(&mut buffer, &schema).map_err(|e| {
2342                NamespaceError::Internal {
2343                    message: format!("Failed to create IPC writer: {:?}", e),
2344                }
2345            })?;
2346            writer.write(&batch).map_err(|e| NamespaceError::Internal {
2347                message: format!("Failed to write batch to IPC: {:?}", e),
2348            })?;
2349            writer.finish().map_err(|e| NamespaceError::Internal {
2350                message: format!("Failed to finish IPC writer: {:?}", e),
2351            })?;
2352        }
2353
2354        Ok(Bytes::from(buffer))
2355    }
2356
2357    async fn list_table_tags(
2358        &self,
2359        request: ListTableTagsRequest,
2360    ) -> Result<ListTableTagsResponse> {
2361        self.record_op("list_table_tags");
2362        let table_uri = self.resolve_table_location(&request.id).await?;
2363        let dataset = self
2364            .load_dataset(&table_uri, None, "list_table_tags")
2365            .await?;
2366
2367        let raw_tags = dataset.tags().list().await.map_err(|e| {
2368            lance_core::Error::from(NamespaceError::Internal {
2369                message: format!("Failed to list tags for table at '{}': {}", table_uri, e),
2370            })
2371        })?;
2372
2373        let tags = raw_tags
2374            .into_iter()
2375            .map(|(name, contents)| {
2376                let mut tag_model =
2377                    ModelTagContents::new(contents.version as i64, contents.manifest_size as i64);
2378                tag_model.branch = contents.branch;
2379                (name, tag_model)
2380            })
2381            .collect();
2382
2383        Ok(ListTableTagsResponse {
2384            tags,
2385            page_token: None,
2386            ..Default::default()
2387        })
2388    }
2389
2390    async fn get_table_tag_version(
2391        &self,
2392        request: GetTableTagVersionRequest,
2393    ) -> Result<GetTableTagVersionResponse> {
2394        self.record_op("get_table_tag_version");
2395        if request.tag.is_empty() {
2396            return Err(NamespaceError::InvalidInput {
2397                message: "tag name must not be empty for get_table_tag_version".to_string(),
2398            }
2399            .into());
2400        }
2401
2402        let table_uri = self.resolve_table_location(&request.id).await?;
2403        let dataset = self
2404            .load_dataset(&table_uri, None, "get_table_tag_version")
2405            .await?;
2406
2407        let contents = dataset
2408            .tags()
2409            .get(&request.tag)
2410            .await
2411            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
2412
2413        Ok(GetTableTagVersionResponse {
2414            version: contents.version as i64,
2415            branch: contents.branch,
2416            ..Default::default()
2417        })
2418    }
2419
2420    async fn create_table_tag(
2421        &self,
2422        request: CreateTableTagRequest,
2423    ) -> Result<CreateTableTagResponse> {
2424        self.record_op("create_table_tag");
2425        if request.tag.is_empty() {
2426            return Err(NamespaceError::InvalidInput {
2427                message: "tag name must not be empty for create_table_tag".to_string(),
2428            }
2429            .into());
2430        }
2431        if request.version <= 0 {
2432            return Err(NamespaceError::InvalidInput {
2433                message: format!(
2434                    "tag version must be a positive integer, got {} for create_table_tag",
2435                    request.version
2436                ),
2437            }
2438            .into());
2439        }
2440
2441        let table_uri = self.resolve_table_location(&request.id).await?;
2442        let dataset = self
2443            .load_dataset(&table_uri, None, "create_table_tag")
2444            .await?;
2445
2446        dataset
2447            .tags()
2448            .create(&request.tag, request.version as u64)
2449            .await
2450            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
2451
2452        Ok(CreateTableTagResponse {
2453            transaction_id: None,
2454            ..Default::default()
2455        })
2456    }
2457
2458    async fn delete_table_tag(
2459        &self,
2460        request: DeleteTableTagRequest,
2461    ) -> Result<DeleteTableTagResponse> {
2462        self.record_op("delete_table_tag");
2463        if request.tag.is_empty() {
2464            return Err(NamespaceError::InvalidInput {
2465                message: "tag name must not be empty for delete_table_tag".to_string(),
2466            }
2467            .into());
2468        }
2469
2470        let table_uri = self.resolve_table_location(&request.id).await?;
2471        let dataset = self
2472            .load_dataset(&table_uri, None, "delete_table_tag")
2473            .await?;
2474
2475        dataset
2476            .tags()
2477            .delete(&request.tag)
2478            .await
2479            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
2480
2481        Ok(DeleteTableTagResponse {
2482            transaction_id: None,
2483            ..Default::default()
2484        })
2485    }
2486
2487    async fn update_table_tag(
2488        &self,
2489        request: UpdateTableTagRequest,
2490    ) -> Result<UpdateTableTagResponse> {
2491        self.record_op("update_table_tag");
2492        if request.tag.is_empty() {
2493            return Err(NamespaceError::InvalidInput {
2494                message: "tag name must not be empty for update_table_tag".to_string(),
2495            }
2496            .into());
2497        }
2498        if request.version <= 0 {
2499            return Err(NamespaceError::InvalidInput {
2500                message: format!(
2501                    "tag version must be a positive integer, got {} for update_table_tag",
2502                    request.version
2503                ),
2504            }
2505            .into());
2506        }
2507
2508        let table_uri = self.resolve_table_location(&request.id).await?;
2509        let dataset = self
2510            .load_dataset(&table_uri, None, "update_table_tag")
2511            .await?;
2512
2513        dataset
2514            .tags()
2515            .update(&request.tag, request.version as u64)
2516            .await
2517            .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
2518
2519        Ok(UpdateTableTagResponse {
2520            transaction_id: None,
2521            ..Default::default()
2522        })
2523    }
2524
2525    async fn create_table_branch(
2526        &self,
2527        request: CreateTableBranchRequest,
2528    ) -> Result<CreateTableBranchResponse> {
2529        self.record_op("create_table_branch");
2530        if request.name.is_empty() {
2531            return Err(NamespaceError::InvalidInput {
2532                message: "branch name must not be empty for create_table_branch".to_string(),
2533            }
2534            .into());
2535        }
2536        let from_version = match request.from_version {
2537            Some(v) if v <= 0 => {
2538                return Err(NamespaceError::InvalidInput {
2539                    message: format!(
2540                        "from_version must be a positive integer, got {} for create_table_branch",
2541                        v
2542                    ),
2543                }
2544                .into());
2545            }
2546            Some(v) => Some(v as u64),
2547            None => None,
2548        };
2549
2550        let table_uri = self.resolve_table_location(&request.id).await?;
2551        let mut dataset = self
2552            .load_dataset(&table_uri, None, "create_table_branch")
2553            .await?;
2554
2555        // Best-effort pre-check: a duplicate returns a clean TableBranchAlreadyExists conflict
2556        // instead of the opaque Internal error create_branch raises on a pre-existing branch. A
2557        // concurrent create can still race past this window. Remove once lance-core create_branch
2558        // returns RefConflict up front.
2559        if dataset.branches().get(&request.name).await.is_ok() {
2560            return Err(NamespaceError::TableBranchAlreadyExists {
2561                message: format!("branch '{}' for table at '{}'", request.name, table_uri),
2562            }
2563            .into());
2564        }
2565
2566        dataset
2567            .create_branch(
2568                &request.name,
2569                (request.from_branch.as_deref(), from_version),
2570                None,
2571            )
2572            .await
2573            .map_err(|e| {
2574                // After load_dataset + the dup pre-check, a DatasetNotFound from create_branch
2575                // means the requested fork source (from_branch/from_version) doesn't exist.
2576                if matches!(e, lance_core::Error::DatasetNotFound { .. }) {
2577                    NamespaceError::InvalidInput {
2578                        message: format!(
2579                            "from_branch/from_version for branch '{}' refers to a source that does not exist: {}",
2580                            request.name, e
2581                        ),
2582                    }
2583                    .into()
2584                } else {
2585                    Self::map_branch_error(e, &request.name, &table_uri)
2586                }
2587            })?;
2588
2589        Ok(CreateTableBranchResponse {
2590            transaction_id: None,
2591            ..Default::default()
2592        })
2593    }
2594
2595    async fn list_table_branches(
2596        &self,
2597        request: ListTableBranchesRequest,
2598    ) -> Result<ListTableBranchesResponse> {
2599        self.record_op("list_table_branches");
2600        let table_uri = self.resolve_table_location(&request.id).await?;
2601        let dataset = self
2602            .load_dataset(&table_uri, None, "list_table_branches")
2603            .await?;
2604
2605        let raw_branches = dataset.list_branches().await.map_err(|e| {
2606            lance_core::Error::from(NamespaceError::Internal {
2607                message: format!(
2608                    "Failed to list branches for table at '{}': {}",
2609                    table_uri, e
2610                ),
2611            })
2612        })?;
2613
2614        let branches = raw_branches
2615            .into_iter()
2616            .map(|(name, contents)| {
2617                // The namespace `BranchContents` model has no `identifier` field, so the
2618                // lance-core branch identifier is intentionally dropped here.
2619                let mut branch_model = ModelBranchContents::new(
2620                    contents.parent_version as i64,
2621                    contents.create_at as i64,
2622                    contents.manifest_size as i64,
2623                );
2624                branch_model.parent_branch = contents.parent_branch;
2625                branch_model.metadata = if contents.metadata.is_empty() {
2626                    None
2627                } else {
2628                    Some(contents.metadata)
2629                };
2630                (name, branch_model)
2631            })
2632            .collect();
2633
2634        Ok(ListTableBranchesResponse {
2635            branches,
2636            page_token: None,
2637            ..Default::default()
2638        })
2639    }
2640
2641    async fn delete_table_branch(
2642        &self,
2643        request: DeleteTableBranchRequest,
2644    ) -> Result<DeleteTableBranchResponse> {
2645        self.record_op("delete_table_branch");
2646        if request.name.is_empty() {
2647            return Err(NamespaceError::InvalidInput {
2648                message: "branch name must not be empty for delete_table_branch".to_string(),
2649            }
2650            .into());
2651        }
2652
2653        let table_uri = self.resolve_table_location(&request.id).await?;
2654        let mut dataset = self
2655            .load_dataset(&table_uri, None, "delete_table_branch")
2656            .await?;
2657
2658        dataset
2659            .delete_branch(&request.name)
2660            .await
2661            .map_err(|e| match e {
2662                lance_core::Error::RefConflict { message } => NamespaceError::InvalidInput {
2663                    message: format!(
2664                        "branch '{}' for table at '{}': {}",
2665                        request.name, table_uri, message
2666                    ),
2667                }
2668                .into(),
2669                other => Self::map_branch_error(other, &request.name, &table_uri),
2670            })?;
2671
2672        Ok(DeleteTableBranchResponse {
2673            transaction_id: None,
2674            ..Default::default()
2675        })
2676    }
2677
2678    fn namespace_id(&self) -> String {
2679        format!("DirectoryNamespace {{ root: {:?} }}", self.root)
2680    }
2681}